1use crate::{diagnostics::Message, display, Error, Errors};
2
3use crate::github_actions;
4use bstr::ByteSlice;
5use spanned::{Span, Spanned};
6use std::{
7 fmt::{Debug, Write as _},
8 io::Write as _,
9 num::NonZeroUsize,
10 path::{Path, PathBuf},
11};
12
13use super::{RevisionStyle, StatusEmitter, Summary, TestStatus};
14fn gha_error(error: &Error, test_path: &str, revision: &str) {
15 let file = Spanned::read_from_file(test_path).unwrap();
16 let line = |span: &Span| {
17 let line = file
18 .lines()
19 .position(|line| line.span.bytes.contains(&span.bytes.start))
20 .unwrap();
21 NonZeroUsize::new(line + 1).unwrap()
22 };
23 match error {
24 Error::ExitStatus {
25 status,
26 expected,
27 reason,
28 } => {
29 let mut err = github_actions::error(
30 test_path,
31 format!("test{revision} got {status}, but expected {expected}"),
32 );
33 err.write_str(reason).unwrap();
34 }
35 Error::Command { kind, status } => {
36 github_actions::error(test_path, format!("{kind}{revision} failed with {status}"));
37 }
38 Error::PatternNotFound { pattern, .. } => {
39 github_actions::error(test_path, format!("Pattern not found{revision}"))
40 .line(line(&pattern.span));
41 }
42 Error::CodeNotFound { code, .. } => {
43 github_actions::error(test_path, format!("Diagnostic code not found{revision}"))
44 .line(line(&code.span));
45 }
46 Error::NoPatternsFound => {
47 github_actions::error(
48 test_path,
49 format!("expexted error patterns, but found none{revision}"),
50 );
51 }
52 Error::PatternFoundInPassTest { .. } => {
53 github_actions::error(
54 test_path,
55 format!("error pattern found in pass test{revision}"),
56 );
57 }
58 Error::OutputDiffers {
59 path: output_path,
60 actual,
61 output: _,
62 expected,
63 bless_command,
64 } => {
65 if expected.is_empty() {
66 let mut err = github_actions::error(
67 test_path,
68 "test generated output, but there was no output file",
69 );
70 if let Some(bless_command) = bless_command {
71 writeln!(
72 err,
73 "you likely need to bless the tests with `{bless_command}`"
74 )
75 .unwrap();
76 }
77 return;
78 }
79
80 let mut line = 1;
81 for r in
82 prettydiff::diff_lines(expected.to_str().unwrap(), actual.to_str().unwrap()).diff()
83 {
84 use prettydiff::basic::DiffOp::*;
85 match r {
86 Equal(s) => {
87 line += s.len();
88 continue;
89 }
90 Replace(l, r) => {
91 let mut err = github_actions::error(
92 display(output_path),
93 "actual output differs from expected",
94 )
95 .line(NonZeroUsize::new(line + 1).unwrap());
96 writeln!(err, "this line was expected to be `{}`", r[0]).unwrap();
97 line += l.len();
98 }
99 Remove(l) => {
100 let mut err = github_actions::error(
101 display(output_path),
102 "extraneous lines in output",
103 )
104 .line(NonZeroUsize::new(line + 1).unwrap());
105 writeln!(
106 err,
107 "remove this line and possibly later ones by blessing the test"
108 )
109 .unwrap();
110 line += l.len();
111 }
112 Insert(r) => {
113 let mut err =
114 github_actions::error(display(output_path), "missing line in output")
115 .line(NonZeroUsize::new(line + 1).unwrap());
116 writeln!(err, "bless the test to create a line containing `{}`", r[0])
117 .unwrap();
118 // Do not count these lines, they don't exist in the original file and
119 // would thus mess up the line number.
120 }
121 }
122 }
123 }
124 Error::ErrorsWithoutPattern { path, msgs } => {
125 if let Some((path, line)) = path.as_ref() {
126 let path = display(path);
127 let mut err =
128 github_actions::error(path, format!("Unmatched diagnostics{revision}"))
129 .line(*line);
130 for Message {
131 level,
132 message,
133 line: _,
134 span: _,
135 code: _,
136 } in msgs
137 {
138 writeln!(err, "{level:?}: {message}").unwrap();
139 }
140 } else {
141 let mut err = github_actions::error(
142 test_path,
143 format!("Unmatched diagnostics outside the testfile{revision}"),
144 );
145 for Message {
146 level,
147 message,
148 line: _,
149 span: _,
150 code: _,
151 } in msgs
152 {
153 writeln!(err, "{level:?}: {message}").unwrap();
154 }
155 }
156 }
157 Error::InvalidComment { msg, span } => {
158 let mut err = github_actions::error(test_path, format!("Could not parse comment"))
159 .line(line(span));
160 writeln!(err, "{msg}").unwrap();
161 }
162 Error::MultipleRevisionsWithResults { kind, lines } => {
163 github_actions::error(test_path, format!("multiple {kind} found"))
164 .line(line(&lines[0]));
165 }
166 Error::Bug(_) => {}
167 Error::Aux {
168 path: aux_path,
169 errors,
170 } => {
171 github_actions::error(test_path, format!("Aux build failed"))
172 .line(line(&aux_path.span));
173 for error in errors {
174 gha_error(error, &display(aux_path), "")
175 }
176 }
177 Error::Rustfix(error) => {
178 github_actions::error(
179 test_path,
180 format!("failed to apply suggestions with rustfix: {error}"),
181 );
182 }
183 Error::ConfigError(msg) => {
184 github_actions::error(test_path, msg.clone());
185 }
186 }
187}
188
189/// Emits Github Actions Workspace commands to show the failures directly in the github diff view.
190/// If the const generic `GROUP` boolean is `true`, also emit `::group` commands.
191pub struct Gha<const GROUP: bool> {
192 /// Show a specific name for the final summary.
193 pub name: String,
194}
195
196#[derive(Clone)]
197struct PathAndRev<const GROUP: bool> {
198 path: PathBuf,
199 revision: String,
200}
201
202impl<const GROUP: bool> TestStatus for PathAndRev<GROUP> {
203 fn path(&self) -> &Path {
204 &self.path
205 }
206
207 fn for_revision(&self, revision: &str, _style: RevisionStyle) -> Box<dyn TestStatus> {
208 Box::new(Self {
209 path: self.path.clone(),
210 revision: revision.to_owned(),
211 })
212 }
213
214 fn for_path(&self, path: &Path) -> Box<dyn TestStatus> {
215 Box::new(Self {
216 path: path.to_path_buf(),
217 revision: self.revision.clone(),
218 })
219 }
220
221 fn failed_test(&self, _cmd: &str, _stderr: &[u8], _stdout: &[u8]) -> Box<dyn Debug> {
222 if GROUP {
223 Box::new(github_actions::group(format_args!(
224 "{}:{}",
225 display(&self.path),
226 self.revision
227 )))
228 } else {
229 Box::new(())
230 }
231 }
232
233 fn revision(&self) -> &str {
234 &self.revision
235 }
236}
237
238impl<const GROUP: bool> StatusEmitter for Gha<GROUP> {
239 fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus> {
240 Box::new(PathAndRev::<GROUP> {
241 path,
242 revision: String::new(),
243 })
244 }
245
246 fn finalize(
247 &self,
248 _failures: usize,
249 succeeded: usize,
250 ignored: usize,
251 filtered: usize,
252 // Can't aborted on gha
253 _aborted: bool,
254 ) -> Box<dyn Summary> {
255 struct Summarizer<const GROUP: bool> {
256 failures: Vec<String>,
257 succeeded: usize,
258 ignored: usize,
259 filtered: usize,
260 name: String,
261 }
262
263 impl<const GROUP: bool> Summary for Summarizer<GROUP> {
264 fn test_failure(&mut self, status: &dyn TestStatus, errors: &Errors) {
265 let revision = if status.revision().is_empty() {
266 "".to_string()
267 } else {
268 format!(" (revision: {})", status.revision())
269 };
270 for error in errors {
271 gha_error(error, &display(status.path()), &revision);
272 }
273 self.failures
274 .push(format!("{}{revision}", display(status.path())));
275 }
276 }
277 impl<const GROUP: bool> Drop for Summarizer<GROUP> {
278 fn drop(&mut self) {
279 if let Some(mut file) = github_actions::summary() {
280 writeln!(file, "### {}", self.name).unwrap();
281 for line in &self.failures {
282 writeln!(file, "* {line}").unwrap();
283 }
284 writeln!(file).unwrap();
285 writeln!(file, "| failed | passed | ignored | filtered out |").unwrap();
286 writeln!(file, "| --- | --- | --- | --- |").unwrap();
287 writeln!(
288 file,
289 "| {} | {} | {} | {} |",
290 self.failures.len(),
291 self.succeeded,
292 self.ignored,
293 self.filtered,
294 )
295 .unwrap();
296 }
297 }
298 }
299
300 Box::new(Summarizer::<GROUP> {
301 failures: vec![],
302 succeeded,
303 ignored,
304 filtered,
305 name: self.name.clone(),
306 })
307 }
308}
309