/src/wasmtime/cranelift/filetests/src/subtest.rs
Line | Count | Source |
1 | | //! `SubTest` trait. |
2 | | |
3 | | use crate::runone::FileUpdate; |
4 | | use anyhow::Context as _; |
5 | | use anyhow::{Result, bail}; |
6 | | use cranelift_codegen::ir::Function; |
7 | | use cranelift_codegen::isa::TargetIsa; |
8 | | use cranelift_codegen::settings::{Flags, FlagsOrIsa}; |
9 | | use cranelift_reader::{Comment, Details, TestFile}; |
10 | | use filecheck::{Checker, CheckerBuilder, NO_VARIABLES}; |
11 | | use log::info; |
12 | | use similar::TextDiff; |
13 | | use std::borrow::Cow; |
14 | | use std::env; |
15 | | |
16 | | /// Context for running a test on a single function. |
17 | | pub struct Context<'a> { |
18 | | /// Comments from the preamble f the test file. These apply to all functions. |
19 | | pub preamble_comments: &'a [Comment<'a>], |
20 | | |
21 | | /// Additional details about the function from the parser. |
22 | | pub details: &'a Details<'a>, |
23 | | |
24 | | /// ISA-independent flags for this test. |
25 | | pub flags: &'a Flags, |
26 | | |
27 | | /// Target ISA to test against. Only guaranteed to be present for sub-tests whose `needs_isa` |
28 | | /// method returned `true`. For other sub-tests, this is set if the test file has a unique ISA. |
29 | | pub isa: Option<&'a dyn TargetIsa>, |
30 | | |
31 | | /// Full path to the file containing the test. |
32 | | #[expect(dead_code, reason = "may get used later")] |
33 | | pub file_path: &'a str, |
34 | | |
35 | | /// Context used to update the original `file_path` in-place with its test |
36 | | /// expectations if so configured in the environment. |
37 | | pub file_update: &'a FileUpdate, |
38 | | } |
39 | | |
40 | | impl<'a> Context<'a> { |
41 | | /// Get a `FlagsOrIsa` object for passing to the verifier. |
42 | 0 | pub fn flags_or_isa(&self) -> FlagsOrIsa<'a> { |
43 | 0 | FlagsOrIsa { |
44 | 0 | flags: self.flags, |
45 | 0 | isa: self.isa, |
46 | 0 | } |
47 | 0 | } |
48 | | } |
49 | | |
50 | | /// Common interface for implementations of test commands. |
51 | | /// |
52 | | /// Each `.clif` test file may contain multiple test commands, each represented by a `SubTest` |
53 | | /// trait object. |
54 | | pub trait SubTest { |
55 | | /// Name identifying this subtest. Typically the same as the test command. |
56 | | fn name(&self) -> &'static str; |
57 | | |
58 | | /// Should the verifier be run on the function before running the test? |
59 | 0 | fn needs_verifier(&self) -> bool { |
60 | 0 | true |
61 | 0 | } Unexecuted instantiation: <cranelift_filetests::test_inline::TestInline as cranelift_filetests::subtest::SubTest>::needs_verifier Unexecuted instantiation: <cranelift_filetests::test_unwind::TestUnwind as cranelift_filetests::subtest::SubTest>::needs_verifier Unexecuted instantiation: <cranelift_filetests::test_compile::TestCompile as cranelift_filetests::subtest::SubTest>::needs_verifier Unexecuted instantiation: <cranelift_filetests::test_domtree::TestDomtree as cranelift_filetests::subtest::SubTest>::needs_verifier Unexecuted instantiation: <cranelift_filetests::test_optimize::TestOptimize as cranelift_filetests::subtest::SubTest>::needs_verifier Unexecuted instantiation: <cranelift_filetests::test_run::TestRun as cranelift_filetests::subtest::SubTest>::needs_verifier Unexecuted instantiation: <cranelift_filetests::test_interpret::TestInterpret as cranelift_filetests::subtest::SubTest>::needs_verifier Unexecuted instantiation: <cranelift_filetests::test_safepoint::TestSafepoint as cranelift_filetests::subtest::SubTest>::needs_verifier Unexecuted instantiation: <cranelift_filetests::test_alias_analysis::TestAliasAnalysis as cranelift_filetests::subtest::SubTest>::needs_verifier Unexecuted instantiation: <cranelift_filetests::test_legalizer::TestLegalizer as cranelift_filetests::subtest::SubTest>::needs_verifier |
62 | | |
63 | | /// Does this test mutate the function when it runs? |
64 | | /// This is used as a hint to avoid cloning the function needlessly. |
65 | 0 | fn is_mutating(&self) -> bool { |
66 | 0 | false |
67 | 0 | } Unexecuted instantiation: <cranelift_filetests::test_domtree::TestDomtree as cranelift_filetests::subtest::SubTest>::is_mutating Unexecuted instantiation: <cranelift_filetests::test_cat::TestCat as cranelift_filetests::subtest::SubTest>::is_mutating Unexecuted instantiation: <cranelift_filetests::test_verifier::TestVerifier as cranelift_filetests::subtest::SubTest>::is_mutating Unexecuted instantiation: <cranelift_filetests::test_print_cfg::TestPrintCfg as cranelift_filetests::subtest::SubTest>::is_mutating Unexecuted instantiation: <cranelift_filetests::test_safepoint::TestSafepoint as cranelift_filetests::subtest::SubTest>::is_mutating |
68 | | |
69 | | /// Does this test need a `TargetIsa` trait object? |
70 | 0 | fn needs_isa(&self) -> bool { |
71 | 0 | false |
72 | 0 | } Unexecuted instantiation: <cranelift_filetests::test_domtree::TestDomtree as cranelift_filetests::subtest::SubTest>::needs_isa Unexecuted instantiation: <cranelift_filetests::test_cat::TestCat as cranelift_filetests::subtest::SubTest>::needs_isa Unexecuted instantiation: <cranelift_filetests::test_verifier::TestVerifier as cranelift_filetests::subtest::SubTest>::needs_isa Unexecuted instantiation: <cranelift_filetests::test_print_cfg::TestPrintCfg as cranelift_filetests::subtest::SubTest>::needs_isa Unexecuted instantiation: <cranelift_filetests::test_safepoint::TestSafepoint as cranelift_filetests::subtest::SubTest>::needs_isa Unexecuted instantiation: <cranelift_filetests::test_alias_analysis::TestAliasAnalysis as cranelift_filetests::subtest::SubTest>::needs_isa |
73 | | |
74 | | /// Runs the entire subtest for a given target, invokes [Self::run] for running |
75 | | /// individual tests. |
76 | 0 | fn run_target<'a>( |
77 | 0 | &self, |
78 | 0 | testfile: &TestFile, |
79 | 0 | file_update: &mut FileUpdate, |
80 | 0 | file_path: &'a str, |
81 | 0 | flags: &'a Flags, |
82 | 0 | isa: Option<&'a dyn TargetIsa>, |
83 | 0 | ) -> anyhow::Result<()> { |
84 | 0 | for (func, details) in &testfile.functions { |
85 | 0 | info!( |
86 | | "Test: {}({}) {}", |
87 | 0 | self.name(), |
88 | | func.name, |
89 | 0 | isa.map_or("-", TargetIsa::name) |
90 | | ); |
91 | | |
92 | 0 | let context = Context { |
93 | 0 | preamble_comments: &testfile.preamble_comments, |
94 | 0 | details, |
95 | 0 | flags, |
96 | 0 | isa, |
97 | 0 | file_path: file_path.as_ref(), |
98 | 0 | file_update, |
99 | 0 | }; |
100 | | |
101 | 0 | self.run(Cow::Borrowed(&func), &context) |
102 | 0 | .context(self.name())?; |
103 | | } |
104 | | |
105 | 0 | Ok(()) |
106 | 0 | } Unexecuted instantiation: <cranelift_filetests::test_inline::TestInline as cranelift_filetests::subtest::SubTest>::run_target Unexecuted instantiation: <cranelift_filetests::test_unwind::TestUnwind as cranelift_filetests::subtest::SubTest>::run_target Unexecuted instantiation: <cranelift_filetests::test_compile::TestCompile as cranelift_filetests::subtest::SubTest>::run_target Unexecuted instantiation: <cranelift_filetests::test_domtree::TestDomtree as cranelift_filetests::subtest::SubTest>::run_target Unexecuted instantiation: <cranelift_filetests::test_optimize::TestOptimize as cranelift_filetests::subtest::SubTest>::run_target Unexecuted instantiation: <cranelift_filetests::test_cat::TestCat as cranelift_filetests::subtest::SubTest>::run_target Unexecuted instantiation: <cranelift_filetests::test_verifier::TestVerifier as cranelift_filetests::subtest::SubTest>::run_target Unexecuted instantiation: <cranelift_filetests::test_print_cfg::TestPrintCfg as cranelift_filetests::subtest::SubTest>::run_target Unexecuted instantiation: <cranelift_filetests::test_safepoint::TestSafepoint as cranelift_filetests::subtest::SubTest>::run_target Unexecuted instantiation: <cranelift_filetests::test_alias_analysis::TestAliasAnalysis as cranelift_filetests::subtest::SubTest>::run_target Unexecuted instantiation: <cranelift_filetests::test_legalizer::TestLegalizer as cranelift_filetests::subtest::SubTest>::run_target |
107 | | |
108 | | /// Run this test on `func`. |
109 | | fn run(&self, func: Cow<Function>, context: &Context) -> anyhow::Result<()>; |
110 | | } |
111 | | |
112 | | /// Run filecheck on `text`, using directives extracted from `context`. |
113 | 0 | pub fn run_filecheck(text: &str, context: &Context) -> anyhow::Result<()> { |
114 | 0 | log::debug!( |
115 | | "Filecheck Input:\n\ |
116 | | =======================\n\ |
117 | | {text}\n\ |
118 | | =======================" |
119 | | ); |
120 | 0 | let checker = build_filechecker(context)?; |
121 | 0 | if checker |
122 | 0 | .check(text, NO_VARIABLES) |
123 | 0 | .context("filecheck failed")? |
124 | | { |
125 | 0 | Ok(()) |
126 | | } else { |
127 | | // Filecheck mismatch. Emit an explanation as output. |
128 | 0 | let (_, explain) = checker |
129 | 0 | .explain(text, NO_VARIABLES) |
130 | 0 | .context("filecheck explain failed")?; |
131 | 0 | anyhow::bail!( |
132 | | "filecheck failed for function on line {}:\n{}{}", |
133 | | context.details.location.line_number, |
134 | | checker, |
135 | | explain |
136 | | ); |
137 | | } |
138 | 0 | } |
139 | | |
140 | | /// Build a filechecker using the directives in the file preamble and the function's comments. |
141 | 0 | pub fn build_filechecker(context: &Context) -> anyhow::Result<Checker> { |
142 | 0 | let mut builder = CheckerBuilder::new(); |
143 | | // Preamble comments apply to all functions. |
144 | 0 | for comment in context.preamble_comments { |
145 | 0 | builder |
146 | 0 | .directive(comment.text) |
147 | 0 | .context("filecheck directive failed")?; |
148 | | } |
149 | 0 | for comment in &context.details.comments { |
150 | 0 | builder |
151 | 0 | .directive(comment.text) |
152 | 0 | .context("filecheck directive failed")?; |
153 | | } |
154 | 0 | Ok(builder.finish()) |
155 | 0 | } |
156 | | |
157 | 0 | pub fn check_precise_output(actual: &[&str], context: &Context) -> Result<()> { |
158 | | // Use the comments after the function to build the test expectation. |
159 | 0 | let expected = context |
160 | 0 | .details |
161 | 0 | .comments |
162 | 0 | .iter() |
163 | 0 | .filter(|c| !c.text.starts_with(";;")) |
164 | 0 | .map(|c| { |
165 | 0 | c.text |
166 | 0 | .strip_prefix("; ") |
167 | 0 | .or_else(|| c.text.strip_prefix(";")) |
168 | 0 | .unwrap_or(c.text) |
169 | 0 | }) |
170 | 0 | .collect::<Vec<_>>(); |
171 | | |
172 | | // If the expectation matches what we got, then there's nothing to do. |
173 | 0 | if actual == expected { |
174 | 0 | return Ok(()); |
175 | 0 | } |
176 | | |
177 | | // If we're supposed to automatically update the test, then do so here. |
178 | 0 | if env::var("CRANELIFT_TEST_BLESS").unwrap_or(String::new()) == "1" { |
179 | 0 | return update_test(&actual, context); |
180 | 0 | } |
181 | | |
182 | | // Otherwise this test has failed, and we can print out as such. |
183 | 0 | bail!( |
184 | | "compilation of function on line {} does not match\n\ |
185 | | the text expectation\n\ |
186 | | \n\ |
187 | | {}\n\ |
188 | | \n\ |
189 | | This test assertion can be automatically updated by setting the\n\ |
190 | | CRANELIFT_TEST_BLESS=1 environment variable when running this test. |
191 | | ", |
192 | | context.details.location.line_number, |
193 | 0 | TextDiff::from_slices(&expected, &actual) |
194 | 0 | .unified_diff() |
195 | 0 | .header("expected", "actual") |
196 | | ) |
197 | 0 | } |
198 | | |
199 | 0 | fn update_test(output: &[&str], context: &Context) -> Result<()> { |
200 | 0 | context |
201 | 0 | .file_update |
202 | 0 | .update_at(&context.details.location, |new_test, old_test| { |
203 | | // blank newline after the function |
204 | 0 | new_test.push_str("\n"); |
205 | | |
206 | | // Splice in the test output |
207 | 0 | for output in output { |
208 | 0 | new_test.push(';'); |
209 | 0 | if !output.is_empty() { |
210 | 0 | new_test.push(' '); |
211 | 0 | new_test.push_str(output); |
212 | 0 | } |
213 | 0 | new_test.push_str("\n"); |
214 | | } |
215 | | |
216 | | // blank newline after test assertion |
217 | 0 | new_test.push_str("\n"); |
218 | | |
219 | | // Drop all remaining commented lines (presumably the old test expectation), |
220 | | // but after we hit a real line then we push all remaining lines. |
221 | 0 | let mut in_next_function = false; |
222 | 0 | for line in old_test { |
223 | 0 | if !in_next_function |
224 | 0 | && (line.trim().is_empty() |
225 | 0 | || (line.starts_with(";") && !line.starts_with(";;"))) |
226 | | { |
227 | 0 | continue; |
228 | 0 | } |
229 | 0 | in_next_function = true; |
230 | 0 | new_test.push_str(line); |
231 | 0 | new_test.push_str("\n"); |
232 | | } |
233 | 0 | }) |
234 | 0 | } |