Skip to main content

whitaker_common/test_support/
ui.rs

1//! Shared UI harness helpers for dylint fixture runners.
2//!
3//! These utilities encapsulate the boilerplate required to discover fixtures,
4//! clone them into an isolated workspace, and execute each case via
5//! `dylint_testing` while capturing panics into deterministic error messages.
6
7use crate::test_support::copy_fixture;
8use camino::Utf8Path;
9use glob::glob;
10use std::fs;
11use std::io;
12use std::path::{Path, PathBuf};
13use tempfile::{TempDir, tempdir};
14
15/// Temporary workspace prepared for a single UI fixture run.
16pub struct FixtureEnvironment {
17    _tempdir: TempDir,
18    workdir: PathBuf,
19    config: Option<String>,
20}
21
22impl FixtureEnvironment {
23    /// Returns the root directory containing the cloned fixture files.
24    pub fn workdir(&self) -> &Path {
25        &self.workdir
26    }
27
28    /// Moves the optional `dylint.toml` contents out of the environment.
29    pub fn take_config(&mut self) -> Option<String> {
30        self.config.take()
31    }
32}
33
34/// Discovers `.rs` fixtures inside `directory`, returning the paths unsorted.
35pub fn discover_fixtures(directory: &Utf8Path) -> io::Result<Vec<PathBuf>> {
36    let pattern = directory.join("*.rs").to_string();
37    let walker = glob(&pattern).map_err(|error| io::Error::other(error.to_string()))?;
38    let mut fixtures = Vec::new();
39
40    for entry in walker {
41        let path = entry.map_err(|error| io::Error::other(error.to_string()))?;
42        if path.is_file() {
43            fixtures.push(path);
44        }
45    }
46
47    Ok(fixtures)
48}
49
50/// Runs fixtures discovered under `directory` using the provided `runner`.
51pub fn run_fixtures_with<F>(
52    crate_name: &str,
53    directory: &Utf8Path,
54    mut runner: F,
55) -> Result<(), String>
56where
57    F: FnMut(&str, &Utf8Path, &Path) -> Result<(), String>,
58{
59    let mut fixtures = discover_fixtures(directory).map_err(|error| error.to_string())?;
60    fixtures.sort();
61
62    for source in fixtures {
63        runner(crate_name, directory, &source)?;
64    }
65
66    Ok(())
67}
68
69/// Copies `source` into a temporary directory, including stderr/config files.
70pub fn prepare_fixture(directory: &Utf8Path, source: &Path) -> io::Result<FixtureEnvironment> {
71    let tempdir = tempdir()?;
72    copy_fixture(directory.as_std_path(), source, tempdir.path())?;
73    let config = resolve_fixture_config(directory, source)?;
74    Ok(FixtureEnvironment {
75        workdir: tempdir.path().to_path_buf(),
76        _tempdir: tempdir,
77        config,
78    })
79}
80
81/// Executes `runner`, capturing unwinds into deterministic error strings.
82pub fn run_test_runner<F>(fixture_name: &str, runner: F) -> Result<(), String>
83where
84    F: FnOnce(),
85{
86    std::panic::catch_unwind(std::panic::AssertUnwindSafe(runner)).map_err(|payload| match payload
87        .downcast::<String>(
88    ) {
89        Ok(message) => format!("{fixture_name}: {message}"),
90        Err(payload) => match payload.downcast::<&'static str>() {
91            Ok(message) => format!("{fixture_name}: {message}"),
92            Err(_) => format!("{fixture_name}: dylint UI tests panicked without a message"),
93        },
94    })
95}
96
97/// Resolves the configuration content for `source`, preferring per-fixture files.
98pub fn resolve_fixture_config(directory: &Utf8Path, source: &Path) -> io::Result<Option<String>> {
99    if let Some(config) = read_fixture_config(source)? {
100        Ok(Some(config))
101    } else {
102        read_directory_config(directory)
103    }
104}
105
106/// Loads `case.dylint.toml` for a fixture when present.
107pub fn read_fixture_config(source: &Path) -> io::Result<Option<String>> {
108    let stem = source
109        .file_stem()
110        .and_then(|value| value.to_str())
111        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "fixture missing name"))?;
112    let config_path = source.with_file_name(format!("{stem}.dylint.toml"));
113
114    if config_path.exists() {
115        fs::read_to_string(config_path).map(Some)
116    } else {
117        Ok(None)
118    }
119}
120
121/// Loads `ui/dylint.toml` style directory-level configuration when present.
122pub fn read_directory_config(directory: &Utf8Path) -> io::Result<Option<String>> {
123    let path = directory.as_std_path().join("dylint.toml");
124    if path.exists() {
125        fs::read_to_string(path).map(Some)
126    } else {
127        Ok(None)
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use camino::Utf8PathBuf;
135    use std::fs;
136
137    fn utf8_path(buf: &Path) -> Utf8PathBuf {
138        Utf8PathBuf::from_path_buf(buf.to_path_buf()).expect("utf8 path")
139    }
140
141    #[test]
142    fn run_fixtures_sorts_and_runs_all_cases() {
143        let dir = tempdir().expect("fixture directory");
144        fs::write(dir.path().join("b.rs"), "fn main() {}").expect("write first fixture");
145        fs::write(dir.path().join("a.rs"), "fn main() {}").expect("write second fixture");
146        let directory = utf8_path(dir.path());
147        let mut visited = Vec::new();
148
149        run_fixtures_with("crate", &directory, |_, _, source| {
150            let name = source
151                .file_name()
152                .and_then(|value| value.to_str())
153                .ok_or_else(|| "utf8 file name".to_string())?
154                .to_owned();
155            visited.push(name);
156            Ok(())
157        })
158        .expect("fixtures run");
159
160        assert_eq!(visited, vec!["a.rs".to_string(), "b.rs".to_string()]);
161    }
162
163    #[test]
164    fn discover_fixtures_filters_rust_files() {
165        let dir = tempdir().expect("fixture directory");
166        fs::write(dir.path().join("first.rs"), "").expect("first fixture");
167        fs::write(dir.path().join("second.txt"), "").expect("second fixture");
168        let directory = utf8_path(dir.path());
169
170        let mut fixtures = discover_fixtures(&directory).expect("discover fixtures");
171        fixtures.sort();
172
173        assert_eq!(fixtures.len(), 1);
174        assert!(fixtures[0].ends_with("first.rs"));
175    }
176
177    #[test]
178    fn discover_fixtures_returns_empty_directory() {
179        let dir = tempdir().expect("fixture directory");
180        let directory = utf8_path(dir.path());
181
182        let fixtures = discover_fixtures(&directory).expect("discover fixtures");
183
184        assert!(fixtures.is_empty());
185    }
186
187    #[test]
188    fn read_fixture_config_loads_optional_file() {
189        let dir = tempdir().expect("fixture directory");
190        let fixture = dir.path().join("case.rs");
191        fs::write(&fixture, "").expect("fixture file");
192        let config = dir.path().join("case.dylint.toml");
193        fs::write(&config, "key = 1").expect("config file");
194
195        let contents = read_fixture_config(&fixture).expect("config contents");
196        assert_eq!(contents.as_deref(), Some("key = 1"));
197    }
198
199    #[test]
200    fn read_directory_config_loads_global_file() {
201        let dir = tempdir().expect("fixture directory");
202        let directory = utf8_path(dir.path());
203        fs::write(directory.as_std_path().join("dylint.toml"), "max_lines = 5")
204            .expect("global config");
205
206        let contents = read_directory_config(&directory).expect("config contents");
207        assert_eq!(contents.as_deref(), Some("max_lines = 5"));
208    }
209
210    #[test]
211    fn resolve_fixture_config_prefers_fixture_specific_file() {
212        let dir = tempdir().expect("fixture directory");
213        let directory = utf8_path(dir.path());
214        let fixture = directory.as_std_path().join("case.rs");
215        fs::write(&fixture, "").expect("fixture");
216        fs::write(
217            directory.as_std_path().join("case.dylint.toml"),
218            "fixture = true",
219        )
220        .expect("fixture config");
221        fs::write(directory.as_std_path().join("dylint.toml"), "global = true")
222            .expect("global config");
223
224        let contents = resolve_fixture_config(&directory, &fixture).expect("config contents");
225        assert_eq!(contents.as_deref(), Some("fixture = true"));
226    }
227}