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