cranelift_filetests/
subtest.rs

1//! `SubTest` trait.
2
3use crate::runone::FileUpdate;
4use anyhow::Context as _;
5use anyhow::{bail, Result};
6use cranelift_codegen::ir::Function;
7use cranelift_codegen::isa::TargetIsa;
8use cranelift_codegen::settings::{Flags, FlagsOrIsa};
9use cranelift_reader::{Comment, Details, TestFile};
10use filecheck::{Checker, CheckerBuilder, NO_VARIABLES};
11use log::info;
12use similar::TextDiff;
13use std::borrow::Cow;
14use std::env;
15
16/// Context for running a test on a single function.
17pub 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
40impl<'a> Context<'a> {
41    /// Get a `FlagsOrIsa` object for passing to the verifier.
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.
54pub 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    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.
65    fn is_mutating(&self) -> bool {
66        false
67    }
68
69    /// Does this test need a `TargetIsa` trait object?
70    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.
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`.
109    fn run(&self, func: Cow<Function>, context: &Context) -> anyhow::Result<()>;
110}
111
112/// Run filecheck on `text`, using directives extracted from `context`.
113pub 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.
141pub 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
157pub 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
199fn 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}