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
17pub fn run() -> Result<()> {
19 foundry_cli::machine::check_machine();
22 foundry_cli::opts::GlobalArgs::check_introspect_with(Forge::command, ®ISTRY);
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
33pub fn setup() -> Result<()> {
35 utils::common_setup();
36 utils::subscriber();
37
38 Ok(())
39}
40
41pub fn run_command(args: Forge) -> Result<()> {
43 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 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 match args.cmd {
85 ForgeSubcommand::Test(cmd) => {
86 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
182const 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
221fn 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 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
248fn 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 #[test]
275 fn introspect_command_ids_are_unique() {
276 let cmd = Forge::command();
277 let doc = build_document(&cmd, ®ISTRY);
278 let dups = duplicate_command_ids(&doc);
279 assert!(dups.is_empty(), "duplicate forge command_ids: {dups:?}");
280 }
281
282 #[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 #[test]
297 fn introspect_capabilities_are_consistent() {
298 let cmd = Forge::command();
299 let doc = build_document(&cmd, ®ISTRY);
300 let v = capability_violations(&doc);
301 assert!(v.is_empty(), "forge capability violations: {v:?}");
302 }
303
304 #[test]
308 fn registered_commands_pin_stable_ids() {
309 let cmd = Forge::command();
310 let doc = build_document(&cmd, ®ISTRY);
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}