ui_test/custom_flags/
run.rs

1//! Types used for running tests after they pass compilation
2
3use super::Flag;
4use crate::{
5    build_manager::BuildManager, display, per_test_config::TestConfig,
6    status_emitter::RevisionStyle, CommandBuilder, Error, Errored, OutputConflictHandling, TestOk,
7};
8use bstr::ByteSlice;
9use spanned::Spanned;
10use std::{path::Path, process::Output};
11
12#[derive(Debug, Copy, Clone)]
13/// Run a test after successfully compiling it
14pub struct Run {
15    /// The exit code that the test is expected to emit.
16    pub exit_code: i32,
17    /// How to handle output conflicts
18    pub output_conflict_handling: Option<OutputConflictHandling>,
19}
20
21impl Flag for Run {
22    fn must_be_unique(&self) -> bool {
23        true
24    }
25    fn clone_inner(&self) -> Box<dyn Flag> {
26        Box::new(*self)
27    }
28
29    fn post_test_action(
30        &self,
31        config: &TestConfig,
32        _output: &Output,
33        build_manager: &BuildManager,
34    ) -> Result<(), Errored> {
35        let mut cmd = config.build_command(build_manager)?;
36        let exit_code = self.exit_code;
37        let revision = config.extension("run");
38        let mut config = TestConfig {
39            config: config.config.clone(),
40            comments: config.comments.clone(),
41            aux_dir: config.aux_dir.clone(),
42            status: config.status.for_revision(&revision, RevisionStyle::Show),
43        };
44        if let Some(och) = self.output_conflict_handling {
45            config.config.output_conflict_handling = och;
46        }
47        build_manager.add_new_job(config, move |config| {
48            cmd.arg("--print").arg("file-names");
49            let output = cmd.output().unwrap();
50            config.aborted()?;
51            assert!(output.status.success(), "{cmd:#?}: {output:#?}");
52
53            let mut files = output.stdout.lines();
54            let file = files.next().unwrap();
55            assert_eq!(files.next(), None);
56            let file = std::str::from_utf8(file).unwrap();
57            let mut envs = std::mem::take(&mut config.config.program.envs);
58            config.config.program = CommandBuilder::cmd(config.config.out_dir.join(file));
59            envs.extend(config.envs().map(|(k, v)| (k.into(), Some(v.into()))));
60            config.config.program.envs = envs;
61
62            let mut exe = config.config.program.build(Path::new(""));
63            let stdin = config
64                .status
65                .path()
66                .with_extension(format!("{revision}.stdin"));
67            if stdin.exists() {
68                exe.stdin(std::fs::File::open(stdin).unwrap());
69            }
70            let output = exe.output().unwrap_or_else(|err| {
71                panic!(
72                    "exe file: {}: {err}",
73                    display(&config.config.program.program)
74                )
75            });
76
77            config.aborted()?;
78
79            let mut errors = vec![];
80
81            config.check_test_output(&mut errors, &output.stdout, &output.stderr);
82
83            let status = output.status;
84            if status.code() != Some(exit_code) {
85                errors.push(Error::ExitStatus {
86                    status,
87                    expected: exit_code,
88                    reason: match (exit_code, status.code()) {
89                        (_, Some(101)) => get_panic_span(&output.stderr),
90                        (0, _) => {
91                            Spanned::dummy("the test was expected to run successfully".into())
92                        }
93                        (101, _) => Spanned::dummy("the test was expected to panic".into()),
94                        _ => Spanned::dummy(String::new()),
95                    },
96                })
97            }
98            if errors.is_empty() {
99                Ok(TestOk::Ok)
100            } else {
101                Err(Errored {
102                    command: format!("{exe:?}"),
103                    errors,
104                    stderr: output.stderr,
105                    stdout: output.stdout,
106                })
107            }
108        });
109        Ok(())
110    }
111}
112
113fn get_panic_span(stderr: &[u8]) -> Spanned<String> {
114    let mut lines = stderr.lines();
115    while let Some(line) = lines.next() {
116        if let Some((_, location)) = line.split_once_str(b"panicked at ") {
117            let mut parts = location.split(|&c| c == b':');
118            let Some(filename) = parts.next() else {
119                continue;
120            };
121            let Some(line) = parts.next() else { continue };
122            let Some(col) = parts.next() else { continue };
123            let message = lines
124                .next()
125                .and_then(|msg| msg.to_str().ok())
126                .unwrap_or("the test panicked during execution");
127            let Ok(line) = line.to_str() else { continue };
128            let Ok(col) = col.to_str() else { continue };
129            let Ok(filename) = filename.to_str() else {
130                continue;
131            };
132            let Ok(line) = line.parse::<usize>() else {
133                continue;
134            };
135            let Ok(col) = col.parse::<usize>() else {
136                continue;
137            };
138            let Ok(file) = Spanned::read_from_file(filename) else {
139                continue;
140            };
141            let Some(line) = line.checked_sub(1) else {
142                continue;
143            };
144            let Some(line) = file.lines().nth(line) else {
145                continue;
146            };
147            let Some(col) = col.checked_sub(1) else {
148                continue;
149            };
150            let span = line.span.inc_col_start(col);
151            return Spanned::new(message.into(), span);
152        }
153    }
154    Spanned::dummy("".into())
155}