cranelift_filetests/
subtest.rs1use 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
16pub struct Context<'a> {
18 pub preamble_comments: &'a [Comment<'a>],
20
21 pub details: &'a Details<'a>,
23
24 pub flags: &'a Flags,
26
27 pub isa: Option<&'a dyn TargetIsa>,
30
31 #[expect(dead_code, reason = "may get used later")]
33 pub file_path: &'a str,
34
35 pub file_update: &'a FileUpdate,
38}
39
40impl<'a> Context<'a> {
41 pub fn flags_or_isa(&self) -> FlagsOrIsa<'a> {
43 FlagsOrIsa {
44 flags: self.flags,
45 isa: self.isa,
46 }
47 }
48}
49
50pub trait SubTest {
55 fn name(&self) -> &'static str;
57
58 fn needs_verifier(&self) -> bool {
60 true
61 }
62
63 fn is_mutating(&self) -> bool {
66 false
67 }
68
69 fn needs_isa(&self) -> bool {
71 false
72 }
73
74 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 fn run(&self, func: Cow<Function>, context: &Context) -> anyhow::Result<()>;
110}
111
112pub 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 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
140pub fn build_filechecker(context: &Context) -> anyhow::Result<Checker> {
142 let mut builder = CheckerBuilder::new();
143 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 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 actual == expected {
174 return Ok(());
175 }
176
177 if env::var("CRANELIFT_TEST_BLESS").unwrap_or(String::new()) == "1" {
179 return update_test(&actual, context);
180 }
181
182 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 new_test.push_str("\n");
205
206 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 new_test.push_str("\n");
218
219 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}