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.
flags_or_isa(&self) -> FlagsOrIsa<'a>42 pub fn flags_or_isa(&self) -> FlagsOrIsa<'a> {
43 FlagsOrIsa {
44 flags: self.flags,
45 isa: self.isa,
46 }
47 }
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.
name(&self) -> &'static str56 fn name(&self) -> &'static str;
57
58 /// Should the verifier be run on the function before running the test?
needs_verifier(&self) -> bool59 fn needs_verifier(&self) -> bool {
60 true
61 }
62
63 /// Does this test mutate the function when it runs?
64 /// This is used as a hint to avoid cloning the function needlessly.
is_mutating(&self) -> bool65 fn is_mutating(&self) -> bool {
66 false
67 }
68
69 /// Does this test need a `TargetIsa` trait object?
needs_isa(&self) -> bool70 fn needs_isa(&self) -> bool {
71 false
72 }
73
74 /// Runs the entire subtest for a given target, invokes [Self::run] for running
75 /// individual tests.
run_target<'a>( &self, testfile: &TestFile, file_update: &mut FileUpdate, file_path: &'a str, flags: &'a Flags, isa: Option<&'a dyn TargetIsa>, ) -> anyhow::Result<()>76 fn run_target<'a>(
77 &self,
78 testfile: &TestFile,
79 file_update: &mut FileUpdate,
80 file_path: &'a str,
81 flags: &'a Flags,
82 isa: Option<&'a dyn TargetIsa>,
83 ) -> anyhow::Result<()> {
84 for (func, details) in &testfile.functions {
85 info!(
86 "Test: {}({}) {}",
87 self.name(),
88 func.name,
89 isa.map_or("-", TargetIsa::name)
90 );
91
92 let context = Context {
93 preamble_comments: &testfile.preamble_comments,
94 details,
95 flags,
96 isa,
97 file_path: file_path.as_ref(),
98 file_update,
99 };
100
101 self.run(Cow::Borrowed(&func), &context)
102 .context(self.name())?;
103 }
104
105 Ok(())
106 }
107
108 /// Run this test on `func`.
run(&self, func: Cow<Function>, context: &Context) -> anyhow::Result<()>109 fn run(&self, func: Cow<Function>, context: &Context) -> anyhow::Result<()>;
110 }
111
112 /// Run filecheck on `text`, using directives extracted from `context`.
run_filecheck(text: &str, context: &Context) -> anyhow::Result<()>113 pub fn run_filecheck(text: &str, context: &Context) -> anyhow::Result<()> {
114 log::debug!(
115 "Filecheck Input:\n\
116 =======================\n\
117 {text}\n\
118 ======================="
119 );
120 let checker = build_filechecker(context)?;
121 if checker
122 .check(text, NO_VARIABLES)
123 .context("filecheck failed")?
124 {
125 Ok(())
126 } else {
127 // Filecheck mismatch. Emit an explanation as output.
128 let (_, explain) = checker
129 .explain(text, NO_VARIABLES)
130 .context("filecheck explain failed")?;
131 anyhow::bail!(
132 "filecheck failed for function on line {}:\n{}{}",
133 context.details.location.line_number,
134 checker,
135 explain
136 );
137 }
138 }
139
140 /// Build a filechecker using the directives in the file preamble and the function's comments.
build_filechecker(context: &Context) -> anyhow::Result<Checker>141 pub fn build_filechecker(context: &Context) -> anyhow::Result<Checker> {
142 let mut builder = CheckerBuilder::new();
143 // Preamble comments apply to all functions.
144 for comment in context.preamble_comments {
145 builder
146 .directive(comment.text)
147 .context("filecheck directive failed")?;
148 }
149 for comment in &context.details.comments {
150 builder
151 .directive(comment.text)
152 .context("filecheck directive failed")?;
153 }
154 Ok(builder.finish())
155 }
156
check_precise_output(actual: &[&str], context: &Context) -> Result<()>157 pub fn check_precise_output(actual: &[&str], context: &Context) -> Result<()> {
158 // Use the comments after the function to build the test expectation.
159 let expected = context
160 .details
161 .comments
162 .iter()
163 .filter(|c| !c.text.starts_with(";;"))
164 .map(|c| {
165 c.text
166 .strip_prefix("; ")
167 .or_else(|| c.text.strip_prefix(";"))
168 .unwrap_or(c.text)
169 })
170 .collect::<Vec<_>>();
171
172 // If the expectation matches what we got, then there's nothing to do.
173 if actual == expected {
174 return Ok(());
175 }
176
177 // If we're supposed to automatically update the test, then do so here.
178 if env::var("CRANELIFT_TEST_BLESS").unwrap_or(String::new()) == "1" {
179 return update_test(&actual, context);
180 }
181
182 // Otherwise this test has failed, and we can print out as such.
183 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 TextDiff::from_slices(&expected, &actual)
194 .unified_diff()
195 .header("expected", "actual")
196 )
197 }
198
update_test(output: &[&str], context: &Context) -> Result<()>199 fn update_test(output: &[&str], context: &Context) -> Result<()> {
200 context
201 .file_update
202 .update_at(&context.details.location, |new_test, old_test| {
203 // blank newline after the function
204 new_test.push_str("\n");
205
206 // Splice in the test output
207 for output in output {
208 new_test.push(';');
209 if !output.is_empty() {
210 new_test.push(' ');
211 new_test.push_str(output);
212 }
213 new_test.push_str("\n");
214 }
215
216 // blank newline after test assertion
217 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 let mut in_next_function = false;
222 for line in old_test {
223 if !in_next_function
224 && (line.trim().is_empty()
225 || (line.starts_with(";") && !line.starts_with(";;")))
226 {
227 continue;
228 }
229 in_next_function = true;
230 new_test.push_str(line);
231 new_test.push_str("\n");
232 }
233 })
234 }
235