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