1use super::{
2 build::BuildArgs, coverage::CoverageArgs, doc::DocArgs, fmt::FmtArgs,
3 snapshot::GasSnapshotArgs, test::TestArgs,
4};
5use crate::opts::Forge;
6use alloy_primitives::map::{HashMap, HashSet};
7use clap::{CommandFactory, Parser};
8use eyre::Result;
9use foundry_cli::utils::{self, FoundryPathExt, LoadConfig};
10use foundry_config::Config;
11use parking_lot::Mutex;
12use std::{
13 ffi::{OsStr, OsString},
14 io::IsTerminal,
15 path::PathBuf,
16 sync::{
17 Arc, OnceLock, Weak,
18 atomic::{AtomicU8, Ordering},
19 },
20 time::Duration,
21};
22use tokio::process::Command as TokioCommand;
23use watchexec::{
24 Watchexec,
25 action::ActionHandler,
26 command::{Command, Program},
27 job::{CommandState, Job},
28 paths::summarise_events_to_env,
29};
30use watchexec_events::{
31 Event, KeyCode, Keyboard, Priority, ProcessEnd, Tag,
32 filekind::{AccessKind, FileEventKind},
33};
34use watchexec_signals::Signal;
35use yansi::{Color, Paint};
36
37type SpawnHook = Arc<dyn Fn(&[Event], &mut TokioCommand) + Send + Sync + 'static>;
38type KeyboardConfig = Arc<OnceLock<Weak<watchexec::Config>>>;
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41enum KeyboardAction {
42 Rerun,
43 Quit,
44}
45
46fn keyboard_action(events: &[Event]) -> Option<KeyboardAction> {
47 let mut rerun = false;
48
49 for tag in events.iter().flat_map(|event| &event.tags) {
50 match tag {
51 Tag::Keyboard(Keyboard::Eof) => return Some(KeyboardAction::Quit),
52 Tag::Keyboard(Keyboard::Key { key: KeyCode::Char('a'), modifiers })
53 if modifiers.is_empty() =>
54 {
55 rerun = true;
56 }
57 _ => {}
58 }
59 }
60
61 rerun.then_some(KeyboardAction::Rerun)
62}
63
64fn set_keyboard_events(config: &Option<KeyboardConfig>, enable: bool) {
65 if let Some(config) = config.as_ref().and_then(|config| config.get()).and_then(Weak::upgrade) {
66 config.keyboard_events(enable);
67 }
68}
69
70#[derive(Clone, Debug, Default, Parser)]
71#[command(next_help_heading = "Watch options")]
72pub struct WatchArgs {
73 #[arg(long, short, num_args(0..), value_name = "PATH")]
77 pub watch: Option<Vec<PathBuf>>,
78
79 #[arg(long)]
81 pub no_restart: bool,
82
83 #[arg(long)]
87 pub run_all: bool,
88
89 #[arg(long, alias = "rerun-failures")]
94 pub rerun_failed: bool,
95
96 #[arg(long, value_name = "DELAY")]
110 pub watch_delay: Option<String>,
111}
112
113impl WatchArgs {
114 pub fn watchexec_config<PS: IntoIterator<Item = P>, P: Into<PathBuf>>(
119 &self,
120 default_paths: impl FnOnce() -> Result<PS>,
121 ) -> Result<watchexec::Config> {
122 self.watchexec_config_generic(default_paths, None, None)
123 }
124
125 fn watchexec_config_with_override<PS: IntoIterator<Item = P>, P: Into<PathBuf>>(
131 &self,
132 default_paths: impl FnOnce() -> Result<PS>,
133 watch_keyboard: bool,
134 spawn_hook: impl Fn(&[Event], &mut TokioCommand) + Send + Sync + 'static,
135 ) -> Result<(watchexec::Config, Option<KeyboardConfig>)> {
136 let keyboard_config = watch_keyboard.then(|| Arc::new(OnceLock::new()));
137 let config = self.watchexec_config_generic(
138 default_paths,
139 Some(Arc::new(spawn_hook)),
140 keyboard_config.clone(),
141 )?;
142 Ok((config, keyboard_config))
143 }
144
145 fn watchexec_config_generic<PS: IntoIterator<Item = P>, P: Into<PathBuf>>(
146 &self,
147 default_paths: impl FnOnce() -> Result<PS>,
148 spawn_hook: Option<SpawnHook>,
149 keyboard_config: Option<KeyboardConfig>,
150 ) -> Result<watchexec::Config> {
151 let mut paths = self.watch.as_deref().unwrap_or_default();
152 let storage: Vec<_>;
153 if paths.is_empty() {
154 storage = default_paths()?.into_iter().map(Into::into).filter(|p| p.exists()).collect();
155 paths = &storage;
156 }
157 self.watchexec_config_inner(paths, spawn_hook, keyboard_config)
158 }
159
160 fn watchexec_config_inner(
161 &self,
162 paths: &[PathBuf],
163 spawn_hook: Option<SpawnHook>,
164 keyboard_config: Option<KeyboardConfig>,
165 ) -> Result<watchexec::Config> {
166 let config = watchexec::Config::default();
167
168 config.on_error(|err| {
169 let _ = sh_eprintln!("[[{err:?}]]");
170 });
171
172 if let Some(delay) = &self.watch_delay {
173 config.throttle(utils::parse_delay(delay)?);
174 }
175
176 config.pathset(paths.iter().map(|p| p.as_path()));
177
178 let base_command = Arc::new(watch_command(cmd_args())?);
179
180 let id = watchexec::Id::default();
181 let quit_again = Arc::new(AtomicU8::new(0));
182 let stop_timeout = Duration::from_secs(5);
183 let no_restart = self.no_restart;
184 let stop_signal = Signal::Terminate;
185 config.on_action(move |mut action| {
186 let base_command = base_command.clone();
187 let job = action.get_or_create_job(id, move || base_command.clone());
188
189 let events = action.events.clone();
190 let spawn_hook = spawn_hook.clone();
191 job.set_spawn_hook(move |command, _| {
192 let env = summarise_events_to_env(events.iter());
194 for (k, v) in env {
195 command.command_mut().env(format!("WATCHEXEC_{k}_PATH"), v);
196 }
197
198 if let Some(spawn_hook) = &spawn_hook {
199 spawn_hook(&events, command.command_mut());
200 }
201 });
202
203 let clear_screen = || {
204 let _ = clearscreen::clear();
205 };
206
207 let quit = |mut action: ActionHandler| {
208 match quit_again.fetch_add(1, Ordering::Relaxed) {
209 0 => {
210 let _ = sh_eprintln!(
211 "[Waiting {stop_timeout:?} for processes to exit before stopping... \
212 Ctrl-C again to exit faster]"
213 );
214 action.quit_gracefully(stop_signal, stop_timeout);
215 }
216 1 => action.quit_gracefully(Signal::ForceStop, Duration::ZERO),
217 _ => action.quit(),
218 }
219
220 action
221 };
222
223 let signals = action.signals().collect::<Vec<_>>();
224 let keyboard_action = keyboard_action(&action.events);
225
226 if signals.contains(&Signal::Terminate)
227 || signals.contains(&Signal::Interrupt)
228 || keyboard_action == Some(KeyboardAction::Quit)
229 {
230 return quit(action);
231 }
232
233 if action.paths().next().is_none()
235 && keyboard_action != Some(KeyboardAction::Rerun)
236 && !action.events.iter().any(|e| e.is_empty())
237 {
238 debug!("no filesystem, rerun, or synthetic events, skip without doing more");
239 return action;
240 }
241
242 if cfg!(target_os = "linux") && keyboard_action != Some(KeyboardAction::Rerun) {
243 let mut has_file_events = false;
249 let mut has_synthetic_events = false;
250 'outer: for e in action.events.iter() {
251 if e.is_empty() {
252 has_synthetic_events = true;
253 break;
254 }
255 for tag in &e.tags {
256 if let Tag::FileEventKind(kind) = tag
257 && !matches!(kind, FileEventKind::Access(AccessKind::Open(_))) {
258 has_file_events = true;
259 break 'outer;
260 }
261 }
262 }
263 if !has_file_events && !has_synthetic_events {
264 debug!("no filesystem events (other than Access(Open)) or synthetic events, skip without doing more");
265 return action;
266 }
267 }
268
269 set_keyboard_events(&keyboard_config, false);
272
273 job.run({
274 let job = job.clone();
275 let keyboard_config = keyboard_config.clone();
276 move |context| {
277 if context.current.is_running() && no_restart {
278 return;
279 }
280 job.restart_with_signal(stop_signal, stop_timeout);
281 job.run({
282 let job = job.clone();
283 move |context| {
284 clear_screen();
285 setup_process(job, &context.command, keyboard_config)
286 }
287 });
288 }
289 });
290
291 action
292 });
293
294 Ok(config)
295 }
296}
297
298fn setup_process(job: Job, _command: &Command, keyboard_config: Option<KeyboardConfig>) {
299 tokio::spawn(async move {
300 job.to_wait().await;
301 job.run(move |context| end_of_process(context.current, keyboard_config));
302 });
303}
304
305fn end_of_process(state: &CommandState, keyboard_config: Option<KeyboardConfig>) {
306 let CommandState::Finished { status, started, finished } = state else {
307 return;
308 };
309
310 let duration = *finished - *started;
311 let timings = true;
312 let timing = if timings { format!(", lasted {duration:?}") } else { String::new() };
313 let (msg, fg) = match status {
314 ProcessEnd::ExitError(code) => (format!("Command exited with {code}{timing}"), Color::Red),
315 ProcessEnd::ExitSignal(sig) => {
316 (format!("Command killed by {sig:?}{timing}"), Color::Magenta)
317 }
318 ProcessEnd::ExitStop(sig) => (format!("Command stopped by {sig:?}{timing}"), Color::Blue),
319 ProcessEnd::Continued => (format!("Command continued{timing}"), Color::Cyan),
320 ProcessEnd::Exception(ex) => {
321 (format!("Command ended by exception {ex:#x}{timing}"), Color::Yellow)
322 }
323 ProcessEnd::Success => (format!("Command was successful{timing}"), Color::Green),
324 };
325
326 let quiet = false;
327 set_keyboard_events(&keyboard_config, true);
328 if !quiet {
329 let _ = sh_eprintln!("{}", format!("[{msg}]").paint(fg.foreground()));
330 }
331}
332
333pub async fn run(config: watchexec::Config) -> Result<()> {
335 run_inner(config, None).await
336}
337
338async fn run_inner(
339 config: watchexec::Config,
340 keyboard_config: Option<KeyboardConfig>,
341) -> Result<()> {
342 let wx = Watchexec::with_config(config)?;
343 if let Some(config) = keyboard_config {
344 debug_assert!(config.set(Arc::downgrade(&wx.config)).is_ok());
345 }
346 wx.send_event(Event::default(), Priority::Urgent).await?;
347 wx.main().await??;
348 Ok(())
349}
350
351pub async fn watch_build(args: BuildArgs) -> Result<()> {
354 let config = args.watchexec_config()?;
355 run(config).await
356}
357
358pub async fn watch_gas_snapshot(args: GasSnapshotArgs) -> Result<()> {
361 let config = args.watchexec_config()?;
362 run(config).await
363}
364
365pub async fn watch_test(args: TestArgs) -> Result<()> {
368 let config: Config = args.build.load_config()?;
369 let filter = args.filter(&config)?;
370 let no_reconfigure = filter.args().test_pattern.is_some()
372 || filter.args().path_pattern.is_some()
373 || filter.args().contract_pattern.is_some()
374 || args.watch.run_all;
375
376 let last_test_files = Mutex::new(HashSet::<String>::default());
377 let project_root = config.root.to_string_lossy().into_owned();
378 let test_failures_file = config.test_failures_file.clone();
379 let rerun_failed = args.watch.rerun_failed;
380 let watch_keyboard = std::io::stdin().is_terminal();
381
382 let (config, keyboard_config) = args.watch.watchexec_config_with_override(
383 || Ok([&config.test, &config.src]),
384 watch_keyboard,
385 move |events, command| {
386 if keyboard_action(events) == Some(KeyboardAction::Rerun) {
387 return;
388 }
389
390 let has_failures = rerun_failed && test_failures_file.exists();
392
393 if has_failures {
394 trace!("Smart watch mode: will rerun failed tests first");
396 command.arg("--rerun");
397 return;
399 }
400
401 let mut changed_sol_test_files: HashSet<_> = events
402 .iter()
403 .flat_map(|e| e.paths())
404 .filter(|(path, _)| path.is_sol_test())
405 .filter_map(|(path, _)| path.to_str())
406 .map(str::to_string)
407 .collect();
408
409 if changed_sol_test_files.len() > 1 {
410 return;
413 }
414
415 if changed_sol_test_files.is_empty() {
416 let last = last_test_files.lock();
418 if last.is_empty() {
419 return;
420 }
421 changed_sol_test_files = last.clone();
422 }
423
424 let mut file = changed_sol_test_files.iter().next().expect("test file present").clone();
426
427 if let Some(f) = file.strip_prefix(&project_root) {
429 file = f.trim_start_matches('/').to_string();
430 }
431
432 trace!(?file, "reconfigure test command");
433
434 if !no_reconfigure {
436 command.arg("--match-path").arg(file);
437 }
438 },
439 )?;
440
441 if watch_keyboard {
442 let _ = sh_eprintln!("[Press 'a' to rerun all tests]");
443 }
444
445 run_inner(config, keyboard_config).await
446}
447
448pub async fn watch_coverage(args: CoverageArgs) -> Result<()> {
449 args.ensure_mode_compatible()?;
450
451 let config = args.watch().watchexec_config(|| {
452 let config = args.load_config()?;
453 Ok([config.test, config.src])
454 })?;
455 run(config).await
456}
457
458pub async fn watch_fmt(args: FmtArgs) -> Result<()> {
459 let config = args.watch.watchexec_config(|| {
460 let config = args.load_config()?;
461 Ok([config.src, config.test, config.script])
462 })?;
463 run(config).await
464}
465
466pub async fn watch_doc(args: DocArgs) -> Result<()> {
471 let include_libraries = args.include_libraries;
472 let deployments_arg = args.deployments.clone();
473 let config = args.watch.watchexec_config(|| {
474 let config = args.config()?;
475 let root = config.root.clone();
476
477 let mut paths = Vec::new();
478
479 paths.push(config.src.clone());
481
482 if include_libraries {
484 for lib in &config.libs {
485 paths.push(lib.clone());
486 }
487 }
488
489 if let Some(hp) = &config.doc.homepage {
491 let hp_path = if hp.is_absolute() { hp.clone() } else { root.join(hp) };
492 if hp_path.exists() {
493 paths.push(hp_path);
494 }
495 }
496 let src_readme = config.src.join("README.md");
499 if src_readme.exists() {
500 paths.push(src_readme);
501 }
502 let readme = root.join("README.md");
503 if readme.exists() {
504 paths.push(readme);
505 }
506
507 if let Some(dir_opt) = deployments_arg.as_ref() {
509 let dep_dir = match dir_opt {
510 Some(p) if p.is_absolute() => p.clone(),
511 Some(p) => root.join(p),
512 None => root.join("deployments"),
513 };
514 if dep_dir.exists() {
515 paths.push(dep_dir);
516 }
517 }
518
519 let toml = root.join("foundry.toml");
521 if toml.exists() {
522 paths.push(toml);
523 }
524
525 Ok(paths)
526 })?;
527 run(config).await
528}
529
530fn watch_command(mut args: Vec<OsString>) -> Result<Command> {
538 debug_assert!(!args.is_empty());
539 let prog = PathBuf::from(args.remove(0));
540 let args = args
541 .into_iter()
542 .map(|arg| {
543 arg.into_string().map_err(|arg| {
544 eyre::eyre!(
545 "watchexec requires UTF-8 command arguments; {arg:?} is not valid UTF-8"
546 )
547 })
548 })
549 .collect::<Result<Vec<_>>>()?;
550 Ok(Command { program: Program::Exec { prog, args }, options: Default::default() })
551}
552
553fn cmd_args() -> Vec<OsString> {
558 clean_cmd_args(std::env::args_os().collect())
559}
560
561enum WatchFlag {
563 Other,
565 Bare(Option<OsString>),
568 Inline(Option<OsString>),
571}
572
573#[instrument(level = "debug", ret)]
580fn clean_cmd_args(cmd_args: Vec<OsString>) -> Vec<OsString> {
581 let mut cleaned = Vec::with_capacity(cmd_args.len());
582 let (value_less_short_flags, variable_options) = command_options(&cmd_args);
583 let mut args = cmd_args.into_iter().peekable();
584 cleaned.extend(args.next());
585 let mut deferred = Vec::new();
586
587 while let Some(arg) = args.next() {
588 if arg == "--" {
589 cleaned.append(&mut deferred);
590 cleaned.push(arg);
591 cleaned.extend(args);
592 break;
593 }
594
595 match watch_flag(&arg, &value_less_short_flags) {
596 WatchFlag::Other => {
597 let option = arg.as_encoded_bytes().strip_prefix(b"--").map(|bytes| {
598 let end = bytes.iter().position(|&byte| byte == b'=').unwrap_or(bytes.len());
599 (&bytes[..end], end < bytes.len())
600 });
601 if let Some((name, attached)) = option
602 && let Ok(name) = std::str::from_utf8(name)
603 && let Some(&max_values) = variable_options.get(name)
604 {
605 deferred.push(arg);
608 if !attached {
609 for _ in 0..max_values {
610 if !args.peek().is_some_and(|next| !looks_like_option(next)) {
611 break;
612 }
613 deferred.extend(args.next());
614 }
615 }
616 } else {
617 cleaned.push(arg);
618 }
619 }
620 WatchFlag::Inline(kept) => cleaned.extend(kept),
621 WatchFlag::Bare(kept) => {
622 cleaned.extend(kept);
623 while args.peek().is_some_and(|next| !looks_like_option(next)) {
626 args.next();
627 }
628 }
629 }
630 }
631
632 cleaned.extend(deferred);
633 cleaned
634}
635
636fn watch_flag(arg: &OsStr, value_less_short_flags: &HashSet<char>) -> WatchFlag {
638 let bytes = arg.as_encoded_bytes();
639 if bytes == b"--watch" || bytes == b"-w" {
640 return WatchFlag::Bare(None);
641 }
642 if bytes.starts_with(b"--watch=") {
643 return WatchFlag::Inline(None);
644 }
645 let Some(pos) = short_watch_pos(bytes, value_less_short_flags) else {
646 return WatchFlag::Other;
647 };
648 let kept = (pos > 1).then(|| OsString::from(std::str::from_utf8(&bytes[..pos]).unwrap()));
651 if bytes.len() == pos + 1 { WatchFlag::Bare(kept) } else { WatchFlag::Inline(kept) }
652}
653
654fn short_watch_pos(arg: &[u8], value_less_short_flags: &HashSet<char>) -> Option<usize> {
660 let cluster = arg.strip_prefix(b"-")?;
661 if cluster.is_empty() || cluster.starts_with(b"-") {
662 return None;
663 }
664 let pos = cluster.iter().position(|&byte| byte == b'w')?;
665 cluster[..pos]
666 .iter()
667 .all(|&byte| byte.is_ascii() && value_less_short_flags.contains(&(byte as char)))
668 .then_some(pos + 1)
669}
670
671fn looks_like_option(arg: &OsStr) -> bool {
674 let bytes = arg.as_encoded_bytes();
675 bytes.len() > 1 && bytes.starts_with(b"-")
676}
677
678fn command_options(cmd_args: &[OsString]) -> (HashSet<char>, HashMap<String, usize>) {
680 let mut command = Forge::command();
681 let mut flags = HashSet::default();
682 let mut variable_options = HashMap::default();
683 if let Ok(matches) = command.try_get_matches_from_mut(cmd_args)
684 && let Some((name, _)) = matches.subcommand()
685 && let Some(command) = command.find_subcommand(name)
686 {
687 for arg in command.get_arguments() {
689 let range = arg.get_num_args().unwrap_or_default();
690 if range.max_values() == 0 {
691 flags.extend(arg.get_short());
692 flags.extend(arg.get_all_short_aliases().unwrap_or_default());
693 } else if arg.get_id() != "watch"
694 && range.min_values() != range.max_values()
695 && let Some(long) = arg.get_long()
696 {
697 variable_options.insert(long.to_owned(), range.max_values());
698 for alias in arg.get_all_aliases().unwrap_or_default() {
699 variable_options.insert(alias.to_owned(), range.max_values());
700 }
701 }
702 }
703 }
704 (flags, variable_options)
705}
706
707#[cfg(test)]
708mod tests {
709 use super::*;
710 use watchexec_events::Modifiers;
711
712 #[cfg(unix)]
713 use std::os::unix::ffi::OsStrExt;
714
715 fn key_event(key: char, modifiers: Modifiers) -> Event {
716 Event {
717 tags: vec![Tag::Keyboard(Keyboard::Key { key: KeyCode::Char(key), modifiers })],
718 ..Default::default()
719 }
720 }
721
722 fn clean(args: &[&str]) -> Vec<OsString> {
723 clean_cmd_args(args.iter().copied().map(OsString::from).collect())
724 }
725
726 #[test]
727 fn classifies_keyboard_actions() {
728 assert_eq!(
729 keyboard_action(&[key_event('a', Modifiers::default())]),
730 Some(KeyboardAction::Rerun)
731 );
732 assert_eq!(keyboard_action(&[key_event('x', Modifiers::default())]), None);
733 assert_eq!(
734 keyboard_action(&[key_event('a', Modifiers { ctrl: true, ..Default::default() })]),
735 None
736 );
737
738 let eof = Event { tags: vec![Tag::Keyboard(Keyboard::Eof)], ..Default::default() };
739 assert_eq!(
740 keyboard_action(&[key_event('a', Modifiers::default()), eof]),
741 Some(KeyboardAction::Quit)
742 );
743 }
744
745 #[test]
746 fn locked_survives_cleaning_watch_args() {
747 assert_eq!(
748 clean(&["forge", "build", "--locked", "--watch", "src", "test"]),
749 ["forge", "build", "--locked"]
750 );
751
752 assert_eq!(
753 clean(&["forge", "build", "--watch", "src", "--locked"]),
754 ["forge", "build", "--locked"]
755 );
756 }
757
758 #[test]
759 fn watch_spellings_survive_cleaning_watch_args() {
760 assert_eq!(clean(&["forge", "build", "-vw"]), ["forge", "build", "-v"]);
761 assert_eq!(clean(&["forge", "test", "--watch", "src"]), ["forge", "test"]);
762 assert_eq!(clean(&["forge", "test", "--watch=src"]), ["forge", "test"]);
763 assert_eq!(clean(&["forge", "test", "-w", "src"]), ["forge", "test"]);
764 assert_eq!(clean(&["forge", "test", "-wsrc"]), ["forge", "test"]);
765 assert_eq!(clean(&["forge", "test", "-vw", "src"]), ["forge", "test", "-v"]);
766 assert_eq!(clean(&["forge", "test", "-vwsrc"]), ["forge", "test", "-v"]);
767 }
768
769 #[test]
770 fn repeated_watch_survives_cleaning_watch_args() {
771 assert_eq!(
772 clean(&["forge", "test", "--watch", "src", "--watch", "test"]),
773 ["forge", "test"]
774 );
775
776 assert_eq!(clean(&["forge", "test", "--watch=src", "-w"]), ["forge", "test"]);
777 assert_eq!(clean(&["forge", "test", "-w", "--watch=src"]), ["forge", "test"]);
778 }
779
780 #[test]
781 fn other_flags_survive_cleaning_watch_args() {
782 assert_eq!(
784 clean(&["forge", "test", "--watch-delay", "1s"]),
785 ["forge", "test", "--watch-delay", "1s"]
786 );
787
788 assert_eq!(clean(&["forge", "test", "-C./workspace"]), ["forge", "test", "-C./workspace"]);
790
791 assert_eq!(clean(&["forge", "build", "-Dwarnings"]), ["forge", "build", "-Dwarnings"]);
792 }
793
794 #[test]
795 fn command_specific_short_flags_survive_cleaning_watch_args() {
796 assert_eq!(clean(&["forge", "build", "-qw", "src"]), ["forge", "build", "-q"]);
797 assert_eq!(clean(&["forge", "fmt", "-rw", "src"]), ["forge", "fmt", "-r"]);
798 assert_eq!(clean(&["forge", "b", "-vw"]), ["forge", "b", "-v"]);
799 }
800
801 #[test]
802 fn watch_cleanup_stops_after_double_dash() {
803 assert_eq!(
804 clean(&["forge", "fmt", "--watch", "src", "--", "--watch=keep.sol"]),
805 ["forge", "fmt", "--", "--watch=keep.sol"]
806 );
807 }
808
809 #[cfg(unix)]
810 #[test]
811 fn non_unicode_watch_survives_cleaning_watch_args() {
812 let mut args = ["forge", "test", "--watch"].map(OsString::from).to_vec();
813 args.push(OsStr::from_bytes(b"src/\xff").to_os_string());
814
815 assert_eq!(clean_cmd_args(args), ["forge", "test"]);
816 }
817
818 #[cfg(unix)]
819 #[test]
820 fn non_unicode_watch_suffixes_and_options_are_preserved() {
821 for path in [b"--watch=src/\xff".as_slice(), b"-wsrc/\xff".as_slice()] {
822 let args = ["forge", "fmt"]
823 .map(OsString::from)
824 .into_iter()
825 .chain([OsStr::from_bytes(path).to_os_string()])
826 .collect();
827 assert_eq!(clean_cmd_args(args), ["forge", "fmt"]);
828 }
829
830 let args = ["forge", "build", "-w", "src"]
831 .map(OsString::from)
832 .into_iter()
833 .chain([OsStr::from_bytes(b"--out=out/\xff").to_os_string()])
834 .collect();
835 assert_eq!(
836 clean_cmd_args(args),
837 vec![
838 OsString::from("forge"),
839 OsString::from("build"),
840 OsStr::from_bytes(b"--out=out/\xff").to_os_string(),
841 ]
842 );
843 }
844
845 #[cfg(unix)]
846 #[test]
847 fn non_unicode_program_path_is_preserved() {
848 let args = vec![OsStr::from_bytes(b"forge\xff").to_os_string(), OsString::from("build")];
849 let command = watch_command(args).unwrap();
850 let Program::Exec { prog, args } = command.program else { panic!("expected exec program") };
851 assert_eq!(prog, PathBuf::from(OsStr::from_bytes(b"forge\xff")));
852 assert_eq!(args, ["build"]);
853 }
854
855 #[test]
856 fn watch_cleanup_preserves_argument_values() {
857 for args in [
858 vec!["forge", "build", "--optimize", "--watch=src", "src/Counter.sol", "--sizes"],
859 vec!["forge", "snapshot", "--check", "--watch=src", "test/Counter.t.sol"],
860 vec!["forge", "snapshot", "--optimize", "--check", "-wsrc", "test/Counter.t.sol"],
861 vec![
862 "forge",
863 "snapshot",
864 "--check",
865 "--watch=src",
866 "test/Counter.t.sol",
867 "--snap",
868 "custom",
869 ],
870 vec!["forge", "build", "--skip", "test", "script", "--watch=src", "src/Counter.sol"],
871 vec![
872 "forge",
873 "build",
874 "--skip",
875 "test",
876 "--watch=src",
877 "src/Counter.sol",
878 "--skip=script",
879 ],
880 vec!["forge", "build", "--optimize", "--watch=src", "--", "src/Counter.sol"],
881 ] {
882 let mut command = Forge::command();
883 let parent = command.try_get_matches_from_mut(&args).unwrap();
884 let child = Forge::command().try_get_matches_from(clean(&args)).unwrap();
885 let (name, parent) = parent.subcommand().unwrap();
886 let (_, child) = child.subcommand().unwrap();
887 assert!(child.get_raw("watch").is_none(), "{args:?}");
888 for arg in command.find_subcommand(name).unwrap().get_arguments() {
889 let id = arg.get_id();
890 if id == "watch" {
891 continue;
892 }
893 assert_eq!(
894 parent.get_raw(id.as_str()).map(|values| values.collect::<Vec<_>>()),
895 child.get_raw(id.as_str()).map(|values| values.collect::<Vec<_>>()),
896 "{args:?}: {id}"
897 );
898 }
899 }
900 }
901
902 #[test]
903 fn watch_cleanup_preserves_program_name() {
904 assert_eq!(clean(&["--watch=forge", "fmt", "--watch", "src"]), ["--watch=forge", "fmt"]);
905 }
906}