Skip to content

Commit

Permalink
feat(cli): Refine CLI interface; add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jubnzv committed Sep 15, 2024
1 parent 26d8e92 commit 2708891
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 97 deletions.
163 changes: 73 additions & 90 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,71 @@
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";

/**
* A runner object used for this execution.
*/
let RUNNER: Runner | undefined = undefined;

export const DUMP_STDOUT_PATH = "-";

export const cliOptions = [
new Option(
"--dump-cfg <json|dot>",
"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 <PATH>",
"Directory to save the CFG dump. If <PATH> 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>", "Path to the Soufflé binary.").default(
"souffle",
),
new Option(
"--souffle-path <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>", "Path to the Tact standard library."),
new Option("--verbose", "Enable verbose output.").default(false),
new Option("--quiet", "Suppress output.").default(false),
new Option(
"--detectors <name|path:name>",
"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>", "Path to the Misti configuration file."),
new Option("--new-detector <PATH>", "Creates a new custom detector.").default(
undefined,
),
];

/**
* Creates and configures the Misti CLI command.
* @returns The configured commander Command instance.
Expand All @@ -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 <type>",
"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 <path>",
"Directory to save CFG dump. If <path> 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>",
"Path to Soufflé binary. Default: `souffle`.",
undefined,
)
.option(
"--souffle-path <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>",
"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 <name|path:name>",
[
"A comma-separated list of detectors to enable.",
"If set, these detectors will override those specified in the configuration file.",
"Format: `<name>` for built-in detectors (e.g., `ReadOnlyVariables`), and `<path:name>` 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>", "Path to Misti configuration file")
.option(
"--new-detector <path>",
"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(
"`<TACT_CONFIG_PATH|TACT_FILE_PATH>` is required",
);
}
if (!PROJECT_CONFIG_OR_FILE_PATH) {
throw ExecutionException.make(
"`<TACT_CONFIG_PATH|TACT_FILE_PATH>` 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;
}
Expand Down
3 changes: 1 addition & 2 deletions src/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -400,8 +401,6 @@ export class Driver {
}
}

const DUMP_STDOUT_PATH = "-";

/**
* CLI options for configuring the analyzer.
*/
Expand Down
4 changes: 4 additions & 0 deletions src/internals/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ export class MistiConfig {
if (detectors !== undefined) {
const builtinDetectors = new Set(getAllDetectors());
return detectors.reduce<DetectorConfig[]>((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 {
Expand Down
85 changes: 85 additions & 0 deletions test/cli.spec.ts
Original file line number Diff line number Diff line change
@@ -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<Runner> => {
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<Runner> => {
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<Runner> => {
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(
"`<TACT_CONFIG_PATH|TACT_FILE_PATH>` is required",
);
});
});
5 changes: 0 additions & 5 deletions test/mistiConfig.json

This file was deleted.

12 changes: 12 additions & 0 deletions test/tact.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"projects": [
{
"name": "sample",
"path": "good/zero-address.tact",
"output": "./tmp/output",
"options": {
"external": true
}
}
]
}

0 comments on commit 2708891

Please sign in to comment.