Coverage Report

Created: 2026-02-23 07:32

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}