Skip to main content

compiletest/runtest/
ui.rs

1use std::collections::HashSet;
2use std::fs::OpenOptions;
3use std::io::Write;
4
5use rustfix::{Filter, apply_suggestions, get_suggestions_from_json};
6use tracing::debug;
7
8use crate::common::PassFailMode;
9use crate::json;
10use crate::runtest::{
11    AllowUnused, Emit, LinkToAux, ProcRes, RunResult, TargetLocation, TestCx, TestOutput,
12    TestSuite, Truncated, UI_FIXED, WillExecute,
13};
14
15impl TestCx<'_> {
16    pub(super) fn run_ui_test(&self) {
17        if self.config.suite == TestSuite::RustdocUi && self.props.should_fail {
18            writeln!(
19                self.stderr,
20                "`should-fail` should not be used in `rustdoc-ui` testsuite, use `failure-status` instead",
21            );
22            // Since it's expecting the test to fail/panic, we return without running anything,
23            // preventing the test to be marked as passed.
24            return;
25        }
26        let pass_fail =
27            self.effective_pass_fail_mode().expect("UI tests always have a pass/fail mode");
28
29        if pass_fail == PassFailMode::BuildFail {
30            // Make sure a build-fail test cannot fail due to failing analysis (e.g. typeck).
31            let proc_res = self.compile_test(WillExecute::No, Emit::Metadata);
32            self.check_if_test_should_compile(PassFailMode::CheckPass, &proc_res);
33        }
34
35        let will_execute = if pass_fail.is_run() { self.run_if_enabled() } else { WillExecute::No };
36        let emit_metadata = if pass_fail.is_check() { Emit::Metadata } else { Emit::None };
37        let proc_res = self.compile_test(will_execute, emit_metadata);
38        self.check_if_test_should_compile(pass_fail, &proc_res);
39
40        if matches!(proc_res.truncated, Truncated::Yes)
41            && !self.props.dont_check_compiler_stdout
42            && !self.props.dont_check_compiler_stderr
43        {
44            self.fatal_proc_rec(
45                "compiler output got truncated, cannot compare with reference file",
46                &proc_res,
47            );
48        }
49
50        // if the user specified a format in the ui test
51        // print the output to the stderr file, otherwise extract
52        // the rendered error messages from json and print them
53        let explicit = self.props.compile_flags.iter().any(|s| s.contains("--error-format"));
54
55        let expected_fixed = self.load_expected_output(UI_FIXED);
56
57        self.check_and_prune_duplicate_outputs(&proc_res, &[], &[]);
58
59        let mut errors = self.load_compare_outputs(&proc_res, TestOutput::Compile, explicit);
60        let rustfix_input = json::rustfix_diagnostics_only(&proc_res.stderr);
61
62        if self.config.compare_mode.is_some() {
63            // don't test rustfix with nll right now
64        } else if self.config.rustfix_coverage {
65            // Find out which tests have `MachineApplicable` suggestions but are missing
66            // `run-rustfix` or `run-rustfix-only-machine-applicable` directives.
67            //
68            // This will return an empty `Vec` in case the executed test file has a
69            // `compile-flags: --error-format=xxxx` directive with a value other than `json`.
70            let suggestions = get_suggestions_from_json(
71                &rustfix_input,
72                &HashSet::new(),
73                Filter::MachineApplicableOnly,
74            )
75            .unwrap_or_default();
76            if !suggestions.is_empty()
77                && !self.props.run_rustfix
78                && !self.props.rustfix_only_machine_applicable
79            {
80                let mut coverage_file_path = self.config.build_test_suite_root.clone();
81                coverage_file_path.push("rustfix_missing_coverage.txt");
82                debug!("coverage_file_path: {}", coverage_file_path);
83
84                let mut file = OpenOptions::new()
85                    .create(true)
86                    .append(true)
87                    .open(coverage_file_path.as_path())
88                    .expect("could not create or open file");
89
90                if let Err(e) = writeln!(file, "{}", self.testpaths.file) {
91                    panic!("couldn't write to {}: {e:?}", coverage_file_path);
92                }
93            }
94        } else if self.props.run_rustfix {
95            // Apply suggestions from rustc to the code itself
96            let unfixed_code = self.load_expected_output_from_path(&self.testpaths.file).unwrap();
97            let suggestions = get_suggestions_from_json(
98                &rustfix_input,
99                &HashSet::new(),
100                if self.props.rustfix_only_machine_applicable {
101                    Filter::MachineApplicableOnly
102                } else {
103                    Filter::Everything
104                },
105            )
106            .unwrap();
107            let fixed_code = apply_suggestions(&unfixed_code, &suggestions).unwrap_or_else(|e| {
108                panic!(
109                    "failed to apply suggestions for {:?} with rustfix: {}",
110                    self.testpaths.file, e
111                )
112            });
113
114            if self
115                .compare_output("fixed", &fixed_code, &fixed_code, &expected_fixed)
116                .should_error()
117            {
118                errors += 1;
119            }
120        } else if !expected_fixed.is_empty() {
121            panic!(
122                "the `//@ run-rustfix` directive wasn't found but a `*.fixed` \
123                 file was found"
124            );
125        }
126
127        if errors > 0 {
128            writeln!(
129                self.stdout,
130                "To update references, rerun the tests and pass the `--bless` flag"
131            );
132            let relative_path_to_file =
133                self.testpaths.relative_dir.join(self.testpaths.file.file_name().unwrap());
134            writeln!(
135                self.stdout,
136                "To only update this specific test, also pass `--test-args {}`",
137                relative_path_to_file,
138            );
139            self.fatal_proc_rec(
140                &format!("{} errors occurred comparing output.", errors),
141                &proc_res,
142            );
143        }
144
145        // If the test is executed, capture its ProcRes separately so that
146        // pattern/forbid checks can report the *runtime* stdout/stderr when they fail.
147        let mut run_proc_res: Option<ProcRes> = None;
148        let output_to_check = if will_execute == WillExecute::Yes {
149            let proc_res = self.exec_compiled_test();
150            let run_output_errors = if self.props.check_run_results {
151                self.load_compare_outputs(&proc_res, TestOutput::Run, explicit)
152            } else {
153                0
154            };
155            if run_output_errors > 0 {
156                self.fatal_proc_rec(
157                    &format!("{} errors occurred comparing run output.", run_output_errors),
158                    &proc_res,
159                );
160            }
161            let code = proc_res.status.code();
162            let run_result = if proc_res.status.success() {
163                RunResult::Pass
164            } else if code.is_some_and(|c| c >= 1 && c <= 127) {
165                RunResult::Fail
166            } else {
167                RunResult::Crash
168            };
169
170            // Help users understand why the test failed by including the actual
171            // exit code and actual run result in the failure message.
172            let pass_hint = format!("code={code:?} so test would pass with `{run_result}`");
173            match pass_fail {
174                PassFailMode::CheckFail
175                | PassFailMode::CheckPass
176                | PassFailMode::BuildFail
177                | PassFailMode::BuildPass => {
178                    unreachable!("test program should not have run in mode {pass_fail:?}")
179                }
180
181                PassFailMode::RunPass => {
182                    if run_result != RunResult::Pass {
183                        self.fatal_proc_rec(
184                            &format!("test did not exit with success! {pass_hint}"),
185                            &proc_res,
186                        );
187                    }
188                }
189
190                PassFailMode::RunFail => {
191                    // If the test is marked as `run-fail` but do not support
192                    // unwinding we allow it to crash, since a panic will trigger an
193                    // abort (crash) instead of unwind (exit with code 101).
194                    let crash_ok = !self.config.can_unwind();
195                    if run_result != RunResult::Fail
196                        && !(crash_ok && run_result == RunResult::Crash)
197                    {
198                        let err = if crash_ok {
199                            format!(
200                                "test did not exit with failure or crash (`{}` can't unwind)! {pass_hint}",
201                                self.config.target
202                            )
203                        } else {
204                            format!("test did not exit with failure! {pass_hint}")
205                        };
206                        self.fatal_proc_rec(&err, &proc_res);
207                    }
208                }
209
210                PassFailMode::RunCrash => {
211                    if run_result != RunResult::Crash {
212                        self.fatal_proc_rec(&format!("test did not crash! {pass_hint}"), &proc_res);
213                    }
214                }
215
216                PassFailMode::RunFailOrCrash => {
217                    if run_result != RunResult::Fail && run_result != RunResult::Crash {
218                        self.fatal_proc_rec(
219                            &format!("test did not exit with failure or crash! {pass_hint}"),
220                            &proc_res,
221                        );
222                    }
223                }
224            }
225
226            let output = self.get_output(&proc_res);
227            // Move the proc_res into our option after we've extracted output.
228            run_proc_res = Some(proc_res);
229            output
230        } else {
231            self.get_output(&proc_res)
232        };
233
234        debug!(
235            "run_ui_test: explicit={:?} config.compare_mode={:?} \
236               proc_res.status={:?} props.error_patterns={:?} output_to_check={:?}",
237            explicit,
238            self.config.compare_mode,
239            proc_res.status,
240            self.props.error_patterns,
241            output_to_check,
242        );
243
244        // Compiler diagnostics (expected errors) are always tied to the compile-time ProcRes.
245        self.check_expected_errors(&proc_res);
246
247        // For runtime pattern/forbid checks prefer the executed program's ProcRes if available
248        // so that missing pattern failures include the program's stdout/stderr.
249        let pattern_proc_res = run_proc_res.as_ref().unwrap_or(&proc_res);
250        self.check_all_error_patterns(&output_to_check, pattern_proc_res);
251        self.check_forbid_output(&output_to_check, pattern_proc_res);
252
253        if self.props.run_rustfix && self.config.compare_mode.is_none() {
254            // And finally, compile the fixed code and make sure it both
255            // succeeds and has no diagnostics.
256            let mut rustc = self.make_compile_args(
257                self.compiler_kind_for_non_aux(),
258                &self.expected_output_path(UI_FIXED),
259                TargetLocation::ThisFile(self.make_exe_name()),
260                emit_metadata,
261                AllowUnused::No,
262                LinkToAux::Yes,
263                Vec::new(),
264            );
265
266            // If a test is revisioned, it's fixed source file can be named "a.foo.fixed", which,
267            // well, "a.foo" isn't a valid crate name. So we explicitly mangle the test name
268            // (including the revision) here to avoid the test writer having to manually specify a
269            // `#![crate_name = "..."]` as a workaround. This is okay since we're only checking if
270            // the fixed code is compilable.
271            if self.variant.revision.is_some() {
272                let crate_name =
273                    self.testpaths.file.file_stem().expect("test must have a file stem");
274                // crate name must be alphanumeric or `_`.
275                // replace `a.foo` -> `a__foo` for crate name purposes.
276                // replace `revision-name-with-dashes` -> `revision_name_with_underscore`
277                let crate_name = crate_name.replace('.', "__");
278                let crate_name = crate_name.replace('-', "_");
279                rustc.arg("--crate-name");
280                rustc.arg(crate_name);
281            }
282
283            let res = self.compose_and_run_compiler(rustc, None);
284            if !res.status.success() {
285                self.fatal_proc_rec("failed to compile fixed code", &res);
286            }
287            if !res.stderr.is_empty()
288                && !self.props.rustfix_only_machine_applicable
289                && !json::rustfix_diagnostics_only(&res.stderr).is_empty()
290            {
291                self.fatal_proc_rec("fixed code is still producing diagnostics", &res);
292            }
293        }
294    }
295}