Path: blob/main/cranelift/filetests/src/subtest.rs
1691 views
//! `SubTest` trait.12use crate::runone::FileUpdate;3use anyhow::Context as _;4use anyhow::{Result, bail};5use cranelift_codegen::ir::Function;6use cranelift_codegen::isa::TargetIsa;7use cranelift_codegen::settings::{Flags, FlagsOrIsa};8use cranelift_reader::{Comment, Details, TestFile};9use filecheck::{Checker, CheckerBuilder, NO_VARIABLES};10use log::info;11use similar::TextDiff;12use std::borrow::Cow;13use std::env;1415/// Context for running a test on a single function.16pub struct Context<'a> {17/// Comments from the preamble f the test file. These apply to all functions.18pub preamble_comments: &'a [Comment<'a>],1920/// Additional details about the function from the parser.21pub details: &'a Details<'a>,2223/// ISA-independent flags for this test.24pub flags: &'a Flags,2526/// Target ISA to test against. Only guaranteed to be present for sub-tests whose `needs_isa`27/// method returned `true`. For other sub-tests, this is set if the test file has a unique ISA.28pub isa: Option<&'a dyn TargetIsa>,2930/// Full path to the file containing the test.31#[expect(dead_code, reason = "may get used later")]32pub file_path: &'a str,3334/// Context used to update the original `file_path` in-place with its test35/// expectations if so configured in the environment.36pub file_update: &'a FileUpdate,37}3839impl<'a> Context<'a> {40/// Get a `FlagsOrIsa` object for passing to the verifier.41pub fn flags_or_isa(&self) -> FlagsOrIsa<'a> {42FlagsOrIsa {43flags: self.flags,44isa: self.isa,45}46}47}4849/// Common interface for implementations of test commands.50///51/// Each `.clif` test file may contain multiple test commands, each represented by a `SubTest`52/// trait object.53pub trait SubTest {54/// Name identifying this subtest. Typically the same as the test command.55fn name(&self) -> &'static str;5657/// Should the verifier be run on the function before running the test?58fn needs_verifier(&self) -> bool {59true60}6162/// Does this test mutate the function when it runs?63/// This is used as a hint to avoid cloning the function needlessly.64fn is_mutating(&self) -> bool {65false66}6768/// Does this test need a `TargetIsa` trait object?69fn needs_isa(&self) -> bool {70false71}7273/// Runs the entire subtest for a given target, invokes [Self::run] for running74/// individual tests.75fn run_target<'a>(76&self,77testfile: &TestFile,78file_update: &mut FileUpdate,79file_path: &'a str,80flags: &'a Flags,81isa: Option<&'a dyn TargetIsa>,82) -> anyhow::Result<()> {83for (func, details) in &testfile.functions {84info!(85"Test: {}({}) {}",86self.name(),87func.name,88isa.map_or("-", TargetIsa::name)89);9091let context = Context {92preamble_comments: &testfile.preamble_comments,93details,94flags,95isa,96file_path: file_path.as_ref(),97file_update,98};99100self.run(Cow::Borrowed(&func), &context)101.context(self.name())?;102}103104Ok(())105}106107/// Run this test on `func`.108fn run(&self, func: Cow<Function>, context: &Context) -> anyhow::Result<()>;109}110111/// Run filecheck on `text`, using directives extracted from `context`.112pub fn run_filecheck(text: &str, context: &Context) -> anyhow::Result<()> {113log::debug!(114"Filecheck Input:\n\115=======================\n\116{text}\n\117======================="118);119let checker = build_filechecker(context)?;120if checker121.check(text, NO_VARIABLES)122.context("filecheck failed")?123{124Ok(())125} else {126// Filecheck mismatch. Emit an explanation as output.127let (_, explain) = checker128.explain(text, NO_VARIABLES)129.context("filecheck explain failed")?;130anyhow::bail!(131"filecheck failed for function on line {}:\n{}{}",132context.details.location.line_number,133checker,134explain135);136}137}138139/// Build a filechecker using the directives in the file preamble and the function's comments.140pub fn build_filechecker(context: &Context) -> anyhow::Result<Checker> {141let mut builder = CheckerBuilder::new();142// Preamble comments apply to all functions.143for comment in context.preamble_comments {144builder145.directive(comment.text)146.context("filecheck directive failed")?;147}148for comment in &context.details.comments {149builder150.directive(comment.text)151.context("filecheck directive failed")?;152}153Ok(builder.finish())154}155156pub fn check_precise_output(actual: &[&str], context: &Context) -> Result<()> {157// Use the comments after the function to build the test expectation.158let expected = context159.details160.comments161.iter()162.filter(|c| !c.text.starts_with(";;"))163.map(|c| {164c.text165.strip_prefix("; ")166.or_else(|| c.text.strip_prefix(";"))167.unwrap_or(c.text)168})169.collect::<Vec<_>>();170171// If the expectation matches what we got, then there's nothing to do.172if actual == expected {173return Ok(());174}175176// If we're supposed to automatically update the test, then do so here.177if env::var("CRANELIFT_TEST_BLESS").unwrap_or(String::new()) == "1" {178return update_test(&actual, context);179}180181// Otherwise this test has failed, and we can print out as such.182bail!(183"compilation of function on line {} does not match\n\184the text expectation\n\185\n\186{}\n\187\n\188This test assertion can be automatically updated by setting the\n\189CRANELIFT_TEST_BLESS=1 environment variable when running this test.190",191context.details.location.line_number,192TextDiff::from_slices(&expected, &actual)193.unified_diff()194.header("expected", "actual")195)196}197198fn update_test(output: &[&str], context: &Context) -> Result<()> {199context200.file_update201.update_at(&context.details.location, |new_test, old_test| {202// blank newline after the function203new_test.push_str("\n");204205// Splice in the test output206for output in output {207new_test.push(';');208if !output.is_empty() {209new_test.push(' ');210new_test.push_str(output);211}212new_test.push_str("\n");213}214215// blank newline after test assertion216new_test.push_str("\n");217218// Drop all remaining commented lines (presumably the old test expectation),219// but after we hit a real line then we push all remaining lines.220let mut in_next_function = false;221for line in old_test {222if !in_next_function223&& (line.trim().is_empty()224|| (line.starts_with(";") && !line.starts_with(";;")))225{226continue;227}228in_next_function = true;229new_test.push_str(line);230new_test.push_str("\n");231}232})233}234235236