foundry_test_utils/
ui_runner.rs
1use std::path::Path;
2use ui_test::spanned::Spanned;
3
4pub fn run_tests<'a>(cmd: &str, cmd_path: &'a Path, testdata: &'a Path) -> eyre::Result<()> {
6 ui_test::color_eyre::install()?;
7
8 let mut args = ui_test::Args::test()?;
9
10 {
12 let mut dummy_config = ui_test::Config::dummy();
13 dummy_config.with_args(&args);
14 if ui_test::nextest::emulate(&mut vec![dummy_config]) {
15 return Ok(());
16 }
17 }
18
19 let requested_pretty = || std::env::args().any(|x| x.contains("--format"));
21 if matches!(args.format, ui_test::Format::Pretty) && !requested_pretty() {
22 args.format = ui_test::Format::Terse;
23 }
24
25 let config = config(cmd, cmd_path, &args, testdata);
26
27 let text_emitter = match args.format {
28 ui_test::Format::Terse => ui_test::status_emitter::Text::quiet(),
29 ui_test::Format::Pretty => ui_test::status_emitter::Text::verbose(),
30 };
31 let gha_emitter = ui_test::status_emitter::Gha::<true> { name: "Foundry Lint UI".to_string() };
32 let status_emitter = (text_emitter, gha_emitter);
33
34 ui_test::run_tests_generic(
36 vec![config],
37 move |path, _config| Some(path.extension().is_some_and(|ext| ext == "sol")),
38 per_file_config,
39 status_emitter,
40 )?;
41
42 Ok(())
43}
44
45fn config<'a>(
46 cmd: &str,
47 cmd_path: &'a Path,
48 args: &ui_test::Args,
49 testdata: &'a Path,
50) -> ui_test::Config {
51 let root = testdata.parent().unwrap();
52 assert!(
53 testdata.exists(),
54 "testdata directory does not exist: {};\n\
55 you may need to initialize submodules: `git submodule update --init --checkout`",
56 testdata.display()
57 );
58
59 let mut config = ui_test::Config {
60 host: Some(get_host().to_string()),
61 target: None,
62 root_dir: testdata.into(),
63 program: ui_test::CommandBuilder {
64 program: cmd_path.into(),
65 args: {
66 let args = vec![cmd, "--json"];
67 args.into_iter().map(Into::into).collect()
68 },
69 out_dir_flag: None,
70 input_file_flag: None,
71 envs: vec![],
72 cfg_flag: None,
73 },
74 output_conflict_handling: ui_test::error_on_output_conflict,
75 bless_command: Some(format!("cargo nextest run {} -- --bless", module_path!())),
76 out_dir: root.join("target").join("ui"),
77 comment_start: "//",
78 diagnostic_extractor: ui_test::diagnostics::rustc::rustc_diagnostics_extractor,
79 ..ui_test::Config::dummy()
80 };
81
82 macro_rules! register_custom_flags {
83 ($($ty:ty),* $(,)?) => {
84 $(
85 config.custom_comments.insert(<$ty>::NAME, <$ty>::parse);
86 if let Some(default) = <$ty>::DEFAULT {
87 config.comment_defaults.base().add_custom(<$ty>::NAME, default);
88 }
89 )*
90 };
91 }
92 register_custom_flags![];
93
94 config.comment_defaults.base().exit_status = None.into();
95 config.comment_defaults.base().require_annotations = Spanned::dummy(true).into();
96 config.comment_defaults.base().require_annotations_for_level =
97 Spanned::dummy(ui_test::diagnostics::Level::Warn).into();
98
99 let filters = [
100 (ui_test::Match::PathBackslash, b"/".to_vec()),
101 #[cfg(windows)]
102 (ui_test::Match::Exact(vec![b'\r']), b"".to_vec()),
103 #[cfg(windows)]
104 (ui_test::Match::Exact(br"\\?\".to_vec()), b"".to_vec()),
105 (root.into(), b"ROOT".to_vec()),
106 ];
107 config.comment_defaults.base().normalize_stderr.extend(filters.iter().cloned());
108 config.comment_defaults.base().normalize_stdout.extend(filters);
109
110 let filters: &[(&str, &str)] = &[
111 (r"\.(\w+):[0-9]+:[0-9]+(: [0-9]+:[0-9]+)?", ".$1:LL:CC"),
113 ];
114 for &(pattern, replacement) in filters {
115 config.filter(pattern, replacement);
116 }
117
118 let stdout_filters: &[(&str, &str)] =
119 &[(&env!("CARGO_PKG_VERSION").replace(".", r"\."), "VERSION")];
120 for &(pattern, replacement) in stdout_filters {
121 config.stdout_filter(pattern, replacement);
122 }
123 let stderr_filters: &[(&str, &str)] = &[];
124 for &(pattern, replacement) in stderr_filters {
125 config.stderr_filter(pattern, replacement);
126 }
127
128 config.with_args(args);
129 config
130}
131
132fn per_file_config(config: &mut ui_test::Config, file: &Spanned<Vec<u8>>) {
133 let Ok(src) = std::str::from_utf8(&file.content) else {
134 return;
135 };
136
137 assert_eq!(config.comment_start, "//");
138 let has_annotations = src.contains("//~");
139 config.comment_defaults.base().require_annotations = Spanned::dummy(has_annotations).into();
140 let code = if has_annotations && src.contains("ERROR:") { 1 } else { 0 };
141 config.comment_defaults.base().exit_status = Spanned::dummy(code).into();
142}
143
144fn get_host() -> &'static str {
145 static CACHE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
146 CACHE.get_or_init(|| {
147 let mut config = ui_test::Config::dummy();
148 config.program = ui_test::CommandBuilder::rustc();
149 config.fill_host_and_target().unwrap();
150 config.host.unwrap()
151 })
152}