1 | //! Types used for running tests after they pass compilation |
2 | |
3 | use super::Flag; |
4 | use crate::{ |
5 | build_manager::BuildManager, display, per_test_config::TestConfig, |
6 | status_emitter::RevisionStyle, CommandBuilder, Error, Errored, OutputConflictHandling, TestOk, |
7 | }; |
8 | use bstr::ByteSlice; |
9 | use spanned::Spanned; |
10 | use std::{path::Path, process::Output}; |
11 | |
12 | #[derive (Debug, Copy, Clone)] |
13 | /// Run a test after successfully compiling it |
14 | pub 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 | |
21 | impl 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 | |
113 | fn 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 | } |
156 | |