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