Skip to main content

forge/
args.rs

1use crate::{
2    cmd::{cache::CacheSubcommands, generate::GenerateSubcommands, test::TestSummaryData, watch},
3    introspect::REGISTRY,
4    opts::{Forge, ForgeSubcommand},
5    result::TestOutcome,
6};
7use clap::CommandFactory;
8use clap_complete::generate;
9use eyre::Result;
10use foundry_cli::{
11    json::{JsonEnvelope, JsonMessage, print_json},
12    utils,
13};
14use foundry_common::{sh_warn, shell};
15use foundry_evm::inspectors::cheatcodes::{ForgeContext, set_execution_context};
16
17/// Run the `forge` command line interface.
18pub fn run() -> Result<()> {
19    // Pre-parse discovery flags run before `setup()` so they cannot be blocked
20    // by panic-handler / tracing init failures and avoid that init's cost.
21    foundry_cli::machine::check_machine();
22    foundry_cli::opts::GlobalArgs::check_introspect_with(Forge::command, &REGISTRY);
23    foundry_cli::opts::GlobalArgs::check_markdown_help::<Forge>();
24
25    setup()?;
26
27    let args = foundry_cli::parse_or_exit::<Forge>();
28    args.global.init()?;
29
30    run_command(args)
31}
32
33/// Setup the global logger and other utilities.
34pub fn setup() -> Result<()> {
35    utils::common_setup();
36    utils::subscriber();
37
38    Ok(())
39}
40
41/// Run the subcommand.
42pub fn run_command(args: Forge) -> Result<()> {
43    // Set the execution context based on the subcommand.
44    let context = match &args.cmd {
45        ForgeSubcommand::Test(_) => ForgeContext::Test,
46        ForgeSubcommand::Coverage(_) => ForgeContext::Coverage,
47        ForgeSubcommand::Snapshot(_) => ForgeContext::Snapshot,
48        ForgeSubcommand::Script(cmd) => {
49            if cmd.broadcast {
50                ForgeContext::ScriptBroadcast
51            } else if cmd.resume {
52                ForgeContext::ScriptResume
53            } else {
54                ForgeContext::ScriptDryRun
55            }
56        }
57        _ => ForgeContext::Unknown,
58    };
59    set_execution_context(context);
60
61    let global = &args.global;
62
63    // Reject `--machine` for forge subcommands not declared adopted in the
64    // introspect registry. Without this, embedders that wrap `TestArgs` (e.g.
65    // `snapshot`, `coverage`) would emit `forge.test` stream events on the
66    // process-global `is_machine()` flag without ever emitting a terminal
67    // envelope — spoofing `command_id` and leaving the stream unterminated.
68    if foundry_cli::is_machine() {
69        let adopted = matches!(args.cmd, ForgeSubcommand::Build(_) | ForgeSubcommand::Test(_));
70        if !adopted {
71            let name = subcommand_name(&args.cmd);
72            foundry_cli::machine::bail_machine_usage_with_details(
73                format!(
74                    "`forge {name}` is not yet adopted for `--machine`; only \
75                     `forge build` and `forge test` are. Run without `--machine` \
76                     or use an adopted subcommand."
77                ),
78                serde_json::json!({ "subcommand": name }),
79            );
80        }
81    }
82
83    // Run the subcommand.
84    match args.cmd {
85        ForgeSubcommand::Test(cmd) => {
86            // Preflight before watcher dispatch so `--watch` is rejected too.
87            cmd.reject_machine_unsupported_flags()?;
88            if cmd.is_watch() {
89                global.block_on(watch::watch_test(cmd))
90            } else {
91                let machine_mode = foundry_cli::is_machine();
92                let silent = machine_mode || cmd.junit || shell::is_json();
93                let started = std::time::Instant::now();
94                let outcome = global.block_on(cmd.run())?;
95                if machine_mode {
96                    return finalize_test_machine_mode(outcome, started.elapsed());
97                }
98                outcome.ensure_ok(silent)
99            }
100        }
101        ForgeSubcommand::Script(cmd) => global.block_on(cmd.run_script()),
102        ForgeSubcommand::Coverage(cmd) => {
103            if cmd.is_watch() {
104                global.block_on(watch::watch_coverage(cmd))
105            } else {
106                global.block_on(cmd.run())
107            }
108        }
109        ForgeSubcommand::Bind(cmd) => cmd.run(),
110        ForgeSubcommand::Build(cmd) => {
111            cmd.ensure_machine_compatible();
112            if cmd.is_watch() {
113                global.block_on(watch::watch_build(cmd))
114            } else {
115                global.block_on(cmd.run()).map(drop)
116            }
117        }
118        ForgeSubcommand::VerifyContract(args) => global.block_on(args.run()),
119        ForgeSubcommand::VerifyCheck(args) => global.block_on(args.run()),
120        ForgeSubcommand::VerifyBytecode(cmd) => global.block_on(cmd.run()),
121        ForgeSubcommand::Clone(cmd) => global.block_on(cmd.run()),
122        ForgeSubcommand::Cache(cmd) => match cmd.sub {
123            CacheSubcommands::Clean(cmd) => cmd.run(),
124            CacheSubcommands::Ls(cmd) => cmd.run(),
125        },
126        ForgeSubcommand::Create(cmd) => global.block_on(cmd.run()),
127        ForgeSubcommand::Update(cmd) => cmd.run(),
128        ForgeSubcommand::Install(cmd) => global.block_on(cmd.run()),
129        ForgeSubcommand::Remove(cmd) => cmd.run(),
130        ForgeSubcommand::Remappings(cmd) => cmd.run(),
131        ForgeSubcommand::Init(cmd) => global.block_on(cmd.run()),
132        ForgeSubcommand::Completions { shell } => {
133            generate(shell, &mut Forge::command(), "forge", &mut std::io::stdout());
134            Ok(())
135        }
136        ForgeSubcommand::Clean { root } => {
137            let config = utils::load_config_with_root(root.as_deref())?;
138            let project = config.project()?;
139            for warning in config.cleanup(&project)? {
140                let _ = sh_warn!("{warning}");
141            }
142            Ok(())
143        }
144        ForgeSubcommand::Snapshot(cmd) => {
145            if cmd.is_watch() {
146                global.block_on(watch::watch_gas_snapshot(cmd))
147            } else {
148                global.block_on(cmd.run())
149            }
150        }
151        ForgeSubcommand::Fmt(cmd) => {
152            if cmd.is_watch() {
153                global.block_on(watch::watch_fmt(cmd))
154            } else {
155                cmd.run()
156            }
157        }
158        ForgeSubcommand::Config(cmd) => cmd.run(),
159        ForgeSubcommand::Flatten(cmd) => cmd.run(),
160        ForgeSubcommand::Inspect(cmd) => cmd.run(),
161        ForgeSubcommand::Tree(cmd) => cmd.run(),
162        ForgeSubcommand::Geiger(cmd) => cmd.run(),
163        ForgeSubcommand::Doc(cmd) => {
164            if cmd.is_watch() {
165                global.block_on(watch::watch_doc(cmd))
166            } else {
167                global.block_on(cmd.run())
168            }
169        }
170        ForgeSubcommand::Selectors { command } => global.block_on(command.run()),
171        ForgeSubcommand::Generate(cmd) => match cmd.sub {
172            GenerateSubcommands::Test(cmd) => cmd.run(),
173        },
174        ForgeSubcommand::Compiler(cmd) => cmd.run(),
175        ForgeSubcommand::Soldeer(cmd) => global.block_on(cmd.run()),
176        ForgeSubcommand::Eip712(cmd) => cmd.run(),
177        ForgeSubcommand::BindJson(cmd) => cmd.run(),
178        ForgeSubcommand::Lint(cmd) => cmd.run(),
179    }
180}
181
182/// Human-readable subcommand name (e.g. `"snapshot"`) for diagnostics.
183const fn subcommand_name(cmd: &ForgeSubcommand) -> &'static str {
184    match cmd {
185        ForgeSubcommand::Test(_) => "test",
186        ForgeSubcommand::Script(_) => "script",
187        ForgeSubcommand::Coverage(_) => "coverage",
188        ForgeSubcommand::Bind(_) => "bind",
189        ForgeSubcommand::Build(_) => "build",
190        ForgeSubcommand::VerifyContract(_) => "verify-contract",
191        ForgeSubcommand::VerifyCheck(_) => "verify-check",
192        ForgeSubcommand::VerifyBytecode(_) => "verify-bytecode",
193        ForgeSubcommand::Clone(_) => "clone",
194        ForgeSubcommand::Cache(_) => "cache",
195        ForgeSubcommand::Create(_) => "create",
196        ForgeSubcommand::Update(_) => "update",
197        ForgeSubcommand::Install(_) => "install",
198        ForgeSubcommand::Remove(_) => "remove",
199        ForgeSubcommand::Remappings(_) => "remappings",
200        ForgeSubcommand::Init(_) => "init",
201        ForgeSubcommand::Completions { .. } => "completions",
202        ForgeSubcommand::Clean { .. } => "clean",
203        ForgeSubcommand::Snapshot(_) => "snapshot",
204        ForgeSubcommand::Fmt(_) => "fmt",
205        ForgeSubcommand::Config(_) => "config",
206        ForgeSubcommand::Flatten(_) => "flatten",
207        ForgeSubcommand::Inspect(_) => "inspect",
208        ForgeSubcommand::Tree(_) => "tree",
209        ForgeSubcommand::Geiger(_) => "geiger",
210        ForgeSubcommand::Doc(_) => "doc",
211        ForgeSubcommand::Selectors { .. } => "selectors",
212        ForgeSubcommand::Generate(_) => "generate",
213        ForgeSubcommand::Compiler(_) => "compiler",
214        ForgeSubcommand::Soldeer(_) => "soldeer",
215        ForgeSubcommand::Eip712(_) => "eip712",
216        ForgeSubcommand::BindJson(_) => "bind-json",
217        ForgeSubcommand::Lint(_) => "lint",
218    }
219}
220
221/// Emit the terminal `forge test` envelope and exit appropriately under
222/// `--machine`. Bypasses [`TestOutcome::ensure_ok`]'s human output.
223fn finalize_test_machine_mode(outcome: TestOutcome, wall_clock: std::time::Duration) -> Result<()> {
224    let summary = TestSummaryData::from_outcome(&outcome, wall_clock);
225    let warnings = aggregate_test_warnings(&outcome);
226
227    // `--allow-failure`: success envelope + exit 0 even if `summary.failed > 0`.
228    if outcome.allow_failure || outcome.failed() == 0 {
229        print_json(&JsonEnvelope::success_with_warnings(summary, warnings))?;
230        return Ok(());
231    }
232    let details = serde_json::to_value(&summary).expect("TestSummaryData is plain scalar fields");
233    let failing_suites = outcome.results.values().filter(|s| s.failed() > 0).count();
234    let message = format!(
235        "{} test(s) failed across {} failing suite(s) (out of {} ran)",
236        outcome.failed(),
237        failing_suites,
238        outcome.results.len(),
239    );
240    let mut envelope = JsonEnvelope::error(
241        JsonMessage::error(foundry_cli::diagnostic::test::FAILED, message).with_details(details),
242    );
243    envelope.warnings = warnings;
244    print_json(&envelope)?;
245    std::process::exit(foundry_cli::ExitCode::TestFailure.to_i32());
246}
247
248/// Flatten per-suite warnings into envelope messages keyed by `test.warning`.
249fn aggregate_test_warnings(outcome: &TestOutcome) -> Vec<JsonMessage> {
250    outcome
251        .results
252        .iter()
253        .flat_map(|(suite, sr)| {
254            sr.warnings.iter().map(move |w| {
255                JsonMessage::warning(foundry_cli::diagnostic::test::WARNING, w.clone())
256                    .with_details(serde_json::json!({ "suite": suite }))
257            })
258        })
259        .collect()
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use foundry_cli::introspect::{
266        CommandRegistry, INTROSPECT_SCHEMA_ID, IntrospectDocument, OutputMode, build_document,
267        capability_violations, duplicate_command_ids, render_introspect_document,
268    };
269
270    /// Every `command_id` exposed by `forge --introspect` MUST be unique.
271    /// This is the foundation of the agent contract — agents key on
272    /// `command_id` to identify commands, and duplicates would silently break
273    /// downstream tooling.
274    #[test]
275    fn introspect_command_ids_are_unique() {
276        let cmd = Forge::command();
277        let doc = build_document(&cmd, &REGISTRY);
278        let dups = duplicate_command_ids(&doc);
279        assert!(dups.is_empty(), "duplicate forge command_ids: {dups:?}");
280    }
281
282    /// `forge --introspect` must produce a JSON document that parses back into
283    /// the canonical `IntrospectDocument` shape.
284    #[test]
285    fn introspect_document_is_valid_json() {
286        let cmd = Forge::command();
287        let json = render_introspect_document(&cmd, &CommandRegistry::EMPTY);
288        let doc: IntrospectDocument = serde_json::from_str(&json).expect("valid JSON");
289        assert_eq!(doc.schema_id, INTROSPECT_SCHEMA_ID);
290        assert_eq!(doc.binary.name, "forge");
291    }
292
293    /// Capability self-consistency: any command declaring an output mode
294    /// must wire the matching schema reference. See
295    /// [`capability_violations`].
296    #[test]
297    fn introspect_capabilities_are_consistent() {
298        let cmd = Forge::command();
299        let doc = build_document(&cmd, &REGISTRY);
300        let v = capability_violations(&doc);
301        assert!(v.is_empty(), "forge capability violations: {v:?}");
302    }
303
304    /// Every adopted command must pin a stable `command_id` matching its
305    /// registry entry. Catches accidental drift between the registry and the
306    /// clap tree across both envelope- and stream-mode commands.
307    #[test]
308    fn registered_commands_pin_stable_ids() {
309        let cmd = Forge::command();
310        let doc = build_document(&cmd, &REGISTRY);
311        fn walk(c: &foundry_cli::introspect::CommandInfo) -> Vec<(&str, OutputMode)> {
312            let mut out = Vec::new();
313            if !matches!(c.capabilities.output_mode, OutputMode::None) {
314                out.push((c.command_id.as_str(), c.capabilities.output_mode));
315            }
316            for sub in &c.subcommands {
317                out.extend(walk(sub));
318            }
319            out
320        }
321        let pinned: Vec<(&str, OutputMode)> = doc.commands.iter().flat_map(walk).collect();
322        let pinned_ids: Vec<&str> = pinned.iter().map(|(id, _)| *id).collect();
323        for id in ["forge.build", "forge.test"] {
324            assert!(pinned_ids.contains(&id), "{id} missing from pinned ids: {pinned_ids:?}");
325        }
326        assert!(
327            pinned.iter().any(|(id, m)| *id == "forge.test" && matches!(m, OutputMode::Stream)),
328            "forge.test must be Stream: {pinned:?}"
329        );
330    }
331}