cranelift_filetests/
runone.rs

1//! Run the tests in a single test file.
2
3use crate::new_subtest;
4use crate::subtest::SubTest;
5use anyhow::{bail, Context as _, Result};
6use cranelift_codegen::isa::TargetIsa;
7use cranelift_codegen::print_errors::pretty_verifier_error;
8use cranelift_codegen::settings::{Flags, FlagsOrIsa};
9use cranelift_codegen::timing;
10use cranelift_codegen::verify_function;
11use cranelift_reader::{parse_test, IsaSpec, Location, ParseOptions, TestFile};
12use log::info;
13use std::cell::Cell;
14use std::fs;
15use std::path::{Path, PathBuf};
16use std::str::Lines;
17use std::time;
18
19/// Load `path` and run the test in it.
20///
21/// If running this test causes a panic, it will propagate as normal.
22pub fn run(
23    path: &Path,
24    passes: Option<&[String]>,
25    target: Option<&str>,
26) -> anyhow::Result<time::Duration> {
27    let _tt = timing::process_file();
28    info!("---\nFile: {}", path.to_string_lossy());
29    let started = time::Instant::now();
30    let buffer =
31        fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?;
32
33    let options = ParseOptions {
34        target,
35        passes,
36        machine_code_cfg_info: true,
37        ..ParseOptions::default()
38    };
39
40    let testfile = match parse_test(&buffer, options) {
41        Ok(testfile) => testfile,
42        Err(e) => {
43            if e.is_warning {
44                log::warn!(
45                    "skipping test {:?} (line {}): {}",
46                    path,
47                    e.location.line_number,
48                    e.message
49                );
50                return Ok(started.elapsed());
51            }
52            return Err(e)
53                .context(format!("failed to parse {}", path.display()))
54                .into();
55        }
56    };
57
58    if testfile.functions.is_empty() {
59        anyhow::bail!("no functions found");
60    }
61
62    // Parse the test commands.
63    let mut tests = testfile
64        .commands
65        .iter()
66        .map(new_subtest)
67        .collect::<anyhow::Result<Vec<_>>>()?;
68
69    // Flags to use for those tests that don't need an ISA.
70    // This is the cumulative effect of all the `set` commands in the file.
71    let flags = match testfile.isa_spec {
72        IsaSpec::None(ref f) => f,
73        IsaSpec::Some(ref v) => v.last().expect("Empty ISA list").flags(),
74    };
75
76    // Sort the tests so the mutators are at the end, and those that don't need the verifier are at
77    // the front.
78    tests.sort_by_key(|st| (st.is_mutating(), st.needs_verifier()));
79
80    // Expand the tests into (test, flags, isa) tuples.
81    let tuples = test_tuples(&tests, &testfile.isa_spec, flags)?;
82
83    // Bail if the test has no runnable commands
84    if tuples.is_empty() {
85        anyhow::bail!("no test commands found");
86    }
87
88    let mut file_update = FileUpdate::new(&path);
89    let file_path = path.to_string_lossy();
90    for (test, flags, isa) in &tuples {
91        // Should we run the verifier before this test?
92        if test.needs_verifier() {
93            let fisa = FlagsOrIsa { flags, isa: *isa };
94            verify_testfile(&testfile, fisa)?;
95        }
96
97        test.run_target(&testfile, &mut file_update, file_path.as_ref(), flags, *isa)?;
98    }
99
100    Ok(started.elapsed())
101}
102
103// Verifies all functions in a testfile
104fn verify_testfile(testfile: &TestFile, fisa: FlagsOrIsa) -> anyhow::Result<()> {
105    for (func, _) in &testfile.functions {
106        verify_function(func, fisa)
107            .map_err(|errors| anyhow::anyhow!("{}", pretty_verifier_error(&func, None, errors)))?;
108    }
109
110    Ok(())
111}
112
113// Given a slice of tests, generate a vector of (test, flags, isa) tuples.
114fn test_tuples<'a>(
115    tests: &'a [Box<dyn SubTest>],
116    isa_spec: &'a IsaSpec,
117    no_isa_flags: &'a Flags,
118) -> anyhow::Result<Vec<(&'a dyn SubTest, &'a Flags, Option<&'a dyn TargetIsa>)>> {
119    let mut out = Vec::new();
120    for test in tests {
121        if test.needs_isa() {
122            match *isa_spec {
123                IsaSpec::None(_) => {
124                    // TODO: Generate a list of default ISAs.
125                    anyhow::bail!("test {} requires an ISA", test.name());
126                }
127                IsaSpec::Some(ref isas) => {
128                    for isa in isas {
129                        out.push((&**test, isa.flags(), Some(&**isa)));
130                    }
131                }
132            }
133        } else {
134            // This test doesn't require an ISA, and we only want to run one instance of it.
135            // Still, give it an ISA ref if we happen to have a unique one.
136            // For example, `test cat` can use this to print encodings and register names.
137            out.push((&**test, no_isa_flags, isa_spec.unique_isa()));
138        }
139    }
140    Ok(out)
141}
142
143/// A helper struct to update a file in-place as test expectations are
144/// automatically updated.
145///
146/// This structure automatically handles multiple edits to one file. Our edits
147/// are line-based but if editing a previous portion of the file adds lines then
148/// all future edits need to know to skip over those previous lines. Note that
149/// this assumes that edits are done front-to-back.
150pub struct FileUpdate {
151    path: PathBuf,
152    line_diff: Cell<isize>,
153    last_update: Cell<usize>,
154}
155
156impl FileUpdate {
157    fn new(path: &Path) -> FileUpdate {
158        FileUpdate {
159            path: path.to_path_buf(),
160            line_diff: Cell::new(0),
161            last_update: Cell::new(0),
162        }
163    }
164
165    /// Updates the file that this structure references at the `location`
166    /// specified.
167    ///
168    /// The closure `f` is given first a buffer to push the new test into along
169    /// with a lines iterator for the old test.
170    pub fn update_at(
171        &self,
172        location: &Location,
173        f: impl FnOnce(&mut String, &mut Lines<'_>),
174    ) -> Result<()> {
175        // This is required for correctness of this update.
176        assert!(location.line_number > self.last_update.get());
177        self.last_update.set(location.line_number);
178
179        // Read the old test file and calculate the new line number we're
180        // preserving up to based on how many lines prior to this have been
181        // removed or added.
182        let old_test = std::fs::read_to_string(&self.path)?;
183        let mut new_test = String::new();
184        let mut lines = old_test.lines();
185        let lines_to_preserve =
186            (((location.line_number - 1) as isize) + self.line_diff.get()) as usize;
187
188        // Push everything leading up to the start of the function
189        for _ in 0..lines_to_preserve {
190            new_test.push_str(lines.next().unwrap());
191            new_test.push_str("\n");
192        }
193
194        // Push the whole function, leading up to the trailing `}`
195        let mut first = true;
196        while let Some(line) = lines.next() {
197            if first && !line.starts_with("function") {
198                bail!(
199                    "line {} in test file {:?} did not start with `function`, \
200                     cannot automatically update test",
201                    location.line_number,
202                    self.path,
203                );
204            }
205            first = false;
206            new_test.push_str(line);
207            new_test.push_str("\n");
208            if line.starts_with("}") {
209                break;
210            }
211        }
212
213        // Use our custom update function to further update the test.
214        f(&mut new_test, &mut lines);
215
216        // Record the difference in line count so future updates can be adjusted
217        // accordingly, and then write the file back out to the filesystem.
218        let old_line_count = old_test.lines().count();
219        let new_line_count = new_test.lines().count();
220        self.line_diff
221            .set(self.line_diff.get() + (new_line_count as isize - old_line_count as isize));
222
223        std::fs::write(&self.path, new_test)?;
224        Ok(())
225    }
226}