From 270889129f74e1f7660686f7c36e30e7f17c5eaf Mon Sep 17 00:00:00 2001 From: Georgiy Komarov Date: Sun, 15 Sep 2024 01:33:40 +0000 Subject: [PATCH] feat(cli): Refine CLI interface; add tests --- src/cli.ts | 163 ++++++++++++++++++---------------------- src/driver.ts | 3 +- src/internals/config.ts | 4 + test/cli.spec.ts | 85 +++++++++++++++++++++ test/mistiConfig.json | 5 -- test/tact.config.json | 12 +++ 6 files changed, 175 insertions(+), 97 deletions(-) create mode 100644 test/cli.spec.ts delete mode 100644 test/mistiConfig.json create mode 100644 test/tact.config.json diff --git a/src/cli.ts b/src/cli.ts index 66eccace..87491054 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,7 +1,7 @@ import { Runner, MistiResult } from "./driver"; import { ExecutionException } from "./internals/exceptions"; import { MISTI_VERSION, TACT_VERSION } from "./version"; -import { Command } from "commander"; +import { Command, Option } from "commander"; import { createDetector } from "./createDetector"; /** @@ -9,6 +9,63 @@ import { createDetector } from "./createDetector"; */ let RUNNER: Runner | undefined = undefined; +export const DUMP_STDOUT_PATH = "-"; + +export const cliOptions = [ + new Option( + "--dump-cfg ", + "Print Control Flow Graph in the requested format: JSON or Graphviz Dot", + ).default(undefined), + new Option( + "--dump-cfg-stdlib", + "Include standard library components in the CFG dump", + ).default(false), + new Option( + "--dump-cfg-output ", + "Directory to save the CFG dump. If is `-`, then stdout is used.", + ).default(DUMP_STDOUT_PATH), + new Option( + "--dump-config", + "Dump the Misti JSON configuration file in use.", + ).default(false), + new Option("--souffle-binary ", "Path to the Soufflé binary.").default( + "souffle", + ), + new Option( + "--souffle-path ", + "Directory to save generated Soufflé files.", + ).default("/tmp/misti/souffle"), + new Option( + "--souffle-verbose", + "Generate human-readable, but more verbose, Soufflé files.", + ).default(false), + new Option("--tact-stdlib-path ", "Path to the Tact standard library."), + new Option("--verbose", "Enable verbose output.").default(false), + new Option("--quiet", "Suppress output.").default(false), + new Option( + "--detectors ", + "A comma-separated list of detectors to enable.", + ) + .argParser((value) => { + const detectors = value.split(",").map((detector) => detector.trim()); + if (detectors.length === 0) { + throw new Error( + "The --detectors option requires a non-empty list of detector names.", + ); + } + return detectors; + }) + .default(undefined), + new Option( + "--all-detectors", + "Enable all the available built-in detectors.", + ).default(false), + new Option("--config ", "Path to the Misti configuration file."), + new Option("--new-detector ", "Creates a new custom detector.").default( + undefined, + ), +]; + /** * Creates and configures the Misti CLI command. * @returns The configured commander Command instance. @@ -20,97 +77,23 @@ export function createMistiCommand(): Command { .name("misti") .description("TON Static Analyzer") .version(`${MISTI_VERSION}\n\nSupported Tact version: ${TACT_VERSION}`) - .arguments("[TACT_CONFIG_PATH|TACT_FILE_PATH]") - .option( - "--dump-cfg ", - "Dump CFG in format: 'json' or 'dot'", - undefined, - ) - .option( - "--dump-cfg-stdlib", - "Include standard library components in the CFG dump", - false, - ) - .option( - "--dump-cfg-output ", - "Directory to save CFG dump. If is `-` then stdout is used.", - "-", - ) - .option( - "--dump-config", - "Dump the used Misti JSON configuration file. If no custom configuration available, dumps the default config.", - false, - ) - .option( - "--souffle-binary ", - "Path to Soufflé binary. Default: `souffle`.", - undefined, - ) - .option( - "--souffle-path ", - "Directory to save generated Soufflé files. If not set, a temporary directory will be used. Default: `/tmp/misti/souffle`", - undefined, - ) - .option( - "--souffle-verbose", - "If set, generates more readable Soufflé files instead of making the result source code smaller.", - undefined, - ) - .option( - "--tact-stdlib-path ", - "Path to Tact standard library. If not set, the default stdlib from the actual Tact setup will be used.", - undefined, - ) - .option("--verbose", "Enable verbose output.", false) - .option("--quiet", "Suppress output.", false) - .option( - "--detectors ", - [ - "A comma-separated list of detectors to enable.", - "If set, these detectors will override those specified in the configuration file.", - "Format: `` for built-in detectors (e.g., `ReadOnlyVariables`), and `` for custom detectors (e.g., `./examples/implicit-init/implicitInit.ts:ImplicitInit`).", - ].join(" "), - (value) => { - const detectors = value - .split(",") - .filter((detector) => detector.trim() !== ""); - if (detectors.length === 0) { - throw ExecutionException.make( - "The --detectors option requires a non-empty list of detector names.", - ); - } - return detectors; - }, - ) - .option( - "--all-detectors", - [ - "Enable all the available built-in detectors.", - "If set, this option will override those detectors specified in the configuration file.", - ].join(" "), - false, - ) - .option("--config ", "Path to Misti configuration file") - .option( - "--new-detector ", - "Creates a new custom detector.", - undefined, - ) - .action(async (PROJECT_CONFIG_OR_FILE_PATH, options) => { - if (options.newDetector) { - createDetector(options.newDetector); - return; - } + .arguments("[TACT_CONFIG_PATH|TACT_FILE_PATH]"); + cliOptions.forEach((option) => command.addOption(option)); + command.action(async (PROJECT_CONFIG_OR_FILE_PATH, options) => { + if (options.newDetector) { + createDetector(options.newDetector); + return; + } - if (!PROJECT_CONFIG_OR_FILE_PATH) { - throw ExecutionException.make( - "`` is required", - ); - } + if (!PROJECT_CONFIG_OR_FILE_PATH) { + throw ExecutionException.make( + "`` is required", + ); + } - RUNNER = await Runner.make(PROJECT_CONFIG_OR_FILE_PATH, options); - await RUNNER.run(); - }); + RUNNER = await Runner.make(PROJECT_CONFIG_OR_FILE_PATH, options); + await RUNNER.run(); + }); return command; } diff --git a/src/driver.ts b/src/driver.ts index 8f5db391..c5e9552a 100644 --- a/src/driver.ts +++ b/src/driver.ts @@ -10,6 +10,7 @@ import { GraphvizDumper, JSONDumper } from "./internals/irDump"; import { ProjectName, CompilationUnit } from "./internals/ir"; import { MistiTactWarning } from "./internals/warnings"; import { Detector, findBuiltInDetector } from "./detectors/detector"; +import { DUMP_STDOUT_PATH } from "./cli"; import { execSync } from "child_process"; import JSONbig from "json-bigint"; import path from "path"; @@ -400,8 +401,6 @@ export class Driver { } } -const DUMP_STDOUT_PATH = "-"; - /** * CLI options for configuring the analyzer. */ diff --git a/src/internals/config.ts b/src/internals/config.ts index e43ad044..23ff933c 100644 --- a/src/internals/config.ts +++ b/src/internals/config.ts @@ -106,6 +106,10 @@ export class MistiConfig { if (detectors !== undefined) { const builtinDetectors = new Set(getAllDetectors()); return detectors.reduce((acc, detector) => { + if (detector === "") { + // The user has specified the empty value in the config. + return acc; + } if (builtinDetectors.has(detector)) { acc.push({ className: detector }); } else { diff --git a/test/cli.spec.ts b/test/cli.spec.ts new file mode 100644 index 00000000..3fe54131 --- /dev/null +++ b/test/cli.spec.ts @@ -0,0 +1,85 @@ +import { runMistiCommand } from "../src/cli"; +import { Runner } from "../src/driver"; +import path from "path"; + +const TACT_CONFIG_PATH = path.join(__dirname, "./tact.config.json"); + +describe("CLI Argument Parsing", () => { + it("should initialize driver with correct options when --verbose is provided", async () => { + const args = ["--verbose", TACT_CONFIG_PATH]; + const runnerMakeSpy = jest.spyOn(Runner, "make"); + runnerMakeSpy.mockImplementation(async (): Promise => { + return { + run: jest.fn(), + getResult: jest.fn(), + getDriver: jest.fn(), + } as unknown as Runner; + }); + await runMistiCommand(args); + expect(runnerMakeSpy).toHaveBeenCalledWith( + TACT_CONFIG_PATH, + expect.objectContaining({ + verbose: true, + }), + ); + runnerMakeSpy.mockRestore(); // restore the original method + }); + + it("should initialize driver with correct options when --dump-cfg is provided", async () => { + const args = ["--dump-cfg", "json", TACT_CONFIG_PATH]; + const runnerMakeSpy = jest.spyOn(Runner, "make"); + runnerMakeSpy.mockImplementation(async (): Promise => { + return { + run: jest.fn(), + getResult: jest.fn(), + getDriver: jest.fn(), + } as unknown as Runner; + }); + await runMistiCommand(args); + expect(runnerMakeSpy).toHaveBeenCalledWith( + TACT_CONFIG_PATH, + expect.objectContaining({ + dumpCfg: "json", + }), + ); + runnerMakeSpy.mockRestore(); + }); + + it("should initialize driver with default options when no options are provided", async () => { + const args = [TACT_CONFIG_PATH]; + const runnerMakeSpy = jest.spyOn(Runner, "make"); + runnerMakeSpy.mockImplementation(async (): Promise => { + return { + run: jest.fn(), + getResult: jest.fn(), + getDriver: jest.fn(), + } as unknown as Runner; + }); + await runMistiCommand(args); + const actualOptions = runnerMakeSpy.mock.calls[0][1]; + expect(actualOptions).toEqual( + expect.objectContaining({ + verbose: false, + }), + ); + expect(actualOptions!.dumpCfg).toBeUndefined(); + runnerMakeSpy.mockRestore(); + }); + + it("should return an error when invalid --detectors option is provided", async () => { + const args = ["--detectors", "", TACT_CONFIG_PATH]; + const result = await runMistiCommand(args); + expect( + result !== undefined && + result.error !== undefined && + result.error.includes("non-empty list of detectors"), + ); + }); + + it("should throw an error when no Tact project is specified", async () => { + const args = ["--verbose"]; + await expect(runMistiCommand(args)).rejects.toThrow( + "`` is required", + ); + }); +}); diff --git a/test/mistiConfig.json b/test/mistiConfig.json deleted file mode 100644 index 38d4bd70..00000000 --- a/test/mistiConfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "detectors": [], - "ignoredProjects": [], - "verbosity": "quiet" -} diff --git a/test/tact.config.json b/test/tact.config.json new file mode 100644 index 00000000..0dd15aa5 --- /dev/null +++ b/test/tact.config.json @@ -0,0 +1,12 @@ +{ + "projects": [ + { + "name": "sample", + "path": "good/zero-address.tact", + "output": "./tmp/output", + "options": { + "external": true + } + } + ] +}