Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bytecodealliance
GitHub Repository: bytecodealliance/wasmtime
Path: blob/main/cranelift/filetests/src/subtest.rs
1691 views
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
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.
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`.
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.
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
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
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
236