Promote the src/tools crate to the top-level workspace.

The 'src' and 'tests' top-level directories now contain tools sources
and integration tests for any of the library crates.
This commit is contained in:
Jakob Stoklund Olesen
2016-10-17 14:57:42 -07:00
parent 0764df28b5
commit a8a79df620
18 changed files with 25 additions and 23 deletions

150
src/filetest/concurrent.rs Normal file
View File

@@ -0,0 +1,150 @@
//! Run tests concurrently.
//!
//! This module provides the `ConcurrentRunner` struct which uses a pool of threads to run tests
//! concurrently.
use std::panic::catch_unwind;
use std::path::{Path, PathBuf};
use std::sync::mpsc::{channel, Sender, Receiver};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use num_cpus;
use filetest::{TestResult, runone};
// Request sent to worker threads contains jobid and path.
struct Request(usize, PathBuf);
/// Reply from worker thread,
pub enum Reply {
Starting { jobid: usize, thread_num: usize },
Done { jobid: usize, result: TestResult },
Tick,
}
/// Manage threads that run test jobs concurrently.
pub struct ConcurrentRunner {
// Channel for sending requests to the worker threads.
// The workers are sharing the receiver with an `Arc<Mutex<Receiver>>`.
// This is `None` when shutting down.
request_tx: Option<Sender<Request>>,
// Channel for receiving replies from the workers.
// Workers have their own `Sender`.
reply_rx: Receiver<Reply>,
handles: Vec<thread::JoinHandle<()>>,
}
impl ConcurrentRunner {
/// Create a new `ConcurrentRunner` with threads spun up.
pub fn new() -> ConcurrentRunner {
let (request_tx, request_rx) = channel();
let request_mutex = Arc::new(Mutex::new(request_rx));
let (reply_tx, reply_rx) = channel();
heartbeat_thread(reply_tx.clone());
let handles = (0..num_cpus::get())
.map(|num| worker_thread(num, request_mutex.clone(), reply_tx.clone()))
.collect();
ConcurrentRunner {
request_tx: Some(request_tx),
reply_rx: reply_rx,
handles: handles,
}
}
/// Shut down worker threads orderly. They will finish any queued jobs first.
pub fn shutdown(&mut self) {
self.request_tx = None;
}
/// Join all the worker threads.
pub fn join(&mut self) {
assert!(self.request_tx.is_none(), "must shutdown before join");
for h in self.handles.drain(..) {
if let Err(e) = h.join() {
println!("worker panicked: {:?}", e);
}
}
}
/// Add a new job to the queues.
pub fn put(&mut self, jobid: usize, path: &Path) {
self.request_tx
.as_ref()
.expect("cannot push after shutdown")
.send(Request(jobid, path.to_owned()))
.expect("all the worker threads are gone");
}
/// Get a job reply without blocking.
pub fn try_get(&mut self) -> Option<Reply> {
self.reply_rx.try_recv().ok()
}
/// Get a job reply, blocking until one is available.
pub fn get(&mut self) -> Option<Reply> {
self.reply_rx.recv().ok()
}
}
/// Spawn a heartbeat thread which sends ticks down the reply channel every second.
/// This lets us implement timeouts without the not yet stable `recv_timeout`.
fn heartbeat_thread(replies: Sender<Reply>) -> thread::JoinHandle<()> {
thread::Builder::new()
.name("heartbeat".to_string())
.spawn(move || {
while replies.send(Reply::Tick).is_ok() {
thread::sleep(Duration::from_secs(1));
}
})
.unwrap()
}
/// Spawn a worker thread running tests.
fn worker_thread(thread_num: usize,
requests: Arc<Mutex<Receiver<Request>>>,
replies: Sender<Reply>)
-> thread::JoinHandle<()> {
thread::Builder::new()
.name(format!("worker #{}", thread_num))
.spawn(move || {
loop {
// Lock the mutex only long enough to extract a request.
let Request(jobid, path) = match requests.lock().unwrap().recv() {
Err(..) => break, // TX end shuit down. exit thread.
Ok(req) => req,
};
// Tell them we're starting this job.
// The receiver should always be present for this as long as we have jobs.
replies.send(Reply::Starting {
jobid: jobid,
thread_num: thread_num,
})
.unwrap();
let result = catch_unwind(|| runone::run(path.as_path())).unwrap_or_else(|e| {
// The test panicked, leaving us a `Box<Any>`.
// Panics are usually strings.
if let Some(msg) = e.downcast_ref::<String>() {
Err(format!("panicked in worker #{}: {}", thread_num, msg))
} else if let Some(msg) = e.downcast_ref::<&'static str>() {
Err(format!("panicked in worker #{}: {}", thread_num, msg))
} else {
Err(format!("panicked in worker #{}", thread_num))
}
});
replies.send(Reply::Done {
jobid: jobid,
result: result,
})
.unwrap();
}
})
.unwrap()
}

103
src/filetest/domtree.rs Normal file
View File

@@ -0,0 +1,103 @@
//! Test command for verifying dominator trees.
//!
//! The `test domtree` test command looks for annotations on instructions like this:
//!
//! jump ebb3 ; dominates: ebb3
//!
//! This annotation means that the jump instruction is expected to be the immediate dominator of
//! `ebb3`.
//!
//! We verify that the dominator tree annotations are complete and correct.
//!
use std::collections::HashMap;
use std::borrow::{Borrow, Cow};
use cretonne::ir::Function;
use cretonne::ir::entities::AnyEntity;
use cretonne::cfg::ControlFlowGraph;
use cretonne::dominator_tree::DominatorTree;
use cton_reader::TestCommand;
use filetest::subtest::{SubTest, Context, Result};
use utils::match_directive;
struct TestDomtree;
pub fn subtest(parsed: &TestCommand) -> Result<Box<SubTest>> {
assert_eq!(parsed.command, "domtree");
if !parsed.options.is_empty() {
Err(format!("No options allowed on {}", parsed))
} else {
Ok(Box::new(TestDomtree))
}
}
impl SubTest for TestDomtree {
fn name(&self) -> Cow<str> {
Cow::from("domtree")
}
// Extract our own dominator tree from
fn run(&self, func: Cow<Function>, context: &Context) -> Result<()> {
let func = func.borrow();
let cfg = ControlFlowGraph::new(func);
let domtree = DominatorTree::new(&cfg);
// Build an expected domtree from the source annotations.
let mut expected = HashMap::new();
for comment in &context.details.comments {
if let Some(tail) = match_directive(comment.text, "dominates:") {
let inst = match comment.entity {
AnyEntity::Inst(inst) => inst,
_ => {
return Err(format!("annotation on non-inst {}: {}",
comment.entity,
comment.text))
}
};
for src_ebb in tail.split_whitespace() {
let ebb = match context.details.map.lookup_str(src_ebb) {
Some(AnyEntity::Ebb(ebb)) => ebb,
_ => return Err(format!("expected EBB: {}", src_ebb)),
};
// Annotations say that `inst` is the idom of `ebb`.
if expected.insert(ebb, inst).is_some() {
return Err(format!("multiple dominators for {}", src_ebb));
}
// Compare to computed domtree.
match domtree.idom(ebb) {
Some((_, got_inst)) if got_inst != inst => {
return Err(format!("mismatching idoms for {}:\n\
want: {}, got: {}",
src_ebb,
inst,
got_inst));
}
None => {
return Err(format!("mismatching idoms for {}:\n\
want: {}, got: unreachable",
src_ebb,
inst));
}
_ => {}
}
}
}
}
// Now we know that everything in `expected` is consistent with `domtree`.
// All other EBB's should be either unreachable or the entry block.
for ebb in func.layout.ebbs().skip(1).filter(|ebb| !expected.contains_key(&ebb)) {
if let Some((_, got_inst)) = domtree.idom(ebb) {
return Err(format!("mismatching idoms for renumbered {}:\n\
want: unrechable, got: {}",
ebb,
got_inst));
}
}
Ok(())
}
}

45
src/filetest/legalizer.rs Normal file
View File

@@ -0,0 +1,45 @@
//! Test command for checking the IL legalizer.
//!
//! The `test legalizer` test command runs each function through `legalize_function()` and sends
//! the result to filecheck.
use std::borrow::Cow;
use cretonne::{legalize_function, write_function};
use cretonne::ir::Function;
use cton_reader::TestCommand;
use filetest::subtest::{SubTest, Context, Result, run_filecheck};
struct TestLegalizer;
pub fn subtest(parsed: &TestCommand) -> Result<Box<SubTest>> {
assert_eq!(parsed.command, "legalizer");
if !parsed.options.is_empty() {
Err(format!("No options allowed on {}", parsed))
} else {
Ok(Box::new(TestLegalizer))
}
}
impl SubTest for TestLegalizer {
fn name(&self) -> Cow<str> {
Cow::from("legalizer")
}
fn is_mutating(&self) -> bool {
true
}
fn needs_isa(&self) -> bool {
true
}
fn run(&self, func: Cow<Function>, context: &Context) -> Result<()> {
let mut func = func.into_owned();
let isa = context.isa.expect("legalizer needs an ISA");
legalize_function(&mut func, isa);
let mut text = String::new();
try!(write_function(&mut text, &func, Some(isa)).map_err(|e| e.to_string()));
run_filecheck(&text, context)
}
}

62
src/filetest/mod.rs Normal file
View File

@@ -0,0 +1,62 @@
//! File tests.
//!
//! This module contains the main driver for `cton-util test` as well as implementations of the
//! available test commands.
use std::path::Path;
use std::time;
use cton_reader::TestCommand;
use CommandResult;
use cat;
use print_cfg;
use filetest::runner::TestRunner;
pub mod subtest;
mod runner;
mod runone;
mod concurrent;
mod domtree;
mod verifier;
mod legalizer;
/// The result of running the test in a file.
pub type TestResult = Result<time::Duration, String>;
/// Main entry point for `cton-util test`.
///
/// Take a list of filenames which can be either `.cton` files or directories.
///
/// Files are interpreted as test cases and executed immediately.
///
/// Directories are scanned recursively for test cases ending in `.cton`. These test cases are
/// executed on background threads.
///
pub fn run(verbose: bool, files: Vec<String>) -> CommandResult {
let mut runner = TestRunner::new(verbose);
for path in files.iter().map(Path::new) {
if path.is_file() {
runner.push_test(path);
} else {
runner.push_dir(path);
}
}
runner.start_threads();
runner.run()
}
/// Create a new subcommand trait object to match `parsed.command`.
///
/// This function knows how to create all of the possible `test <foo>` commands that can appear in
/// a .cton test file.
fn new_subtest(parsed: &TestCommand) -> subtest::Result<Box<subtest::SubTest>> {
match parsed.command {
"cat" => cat::subtest(parsed),
"print-cfg" => print_cfg::subtest(parsed),
"domtree" => domtree::subtest(parsed),
"verifier" => verifier::subtest(parsed),
"legalizer" => legalizer::subtest(parsed),
_ => Err(format!("unknown test command '{}'", parsed.command)),
}
}

330
src/filetest/runner.rs Normal file
View File

@@ -0,0 +1,330 @@
//! Test runner.
//!
//! This module implements the `TestRunner` struct which manages executing tests as well as
//! scanning directories for tests.
use std::error::Error;
use std::fmt::{self, Display};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use filetest::{TestResult, runone};
use filetest::concurrent::{ConcurrentRunner, Reply};
use CommandResult;
// Timeout in seconds when we're not making progress.
const TIMEOUT_PANIC: usize = 10;
// Timeout for reporting slow tests without panicking.
const TIMEOUT_SLOW: usize = 3;
struct QueueEntry {
path: PathBuf,
state: State,
}
#[derive(PartialEq, Eq, Debug)]
enum State {
New,
Queued,
Running,
Done(TestResult),
}
impl QueueEntry {
pub fn path(&self) -> &Path {
self.path.as_path()
}
}
impl Display for QueueEntry {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let p = self.path.to_string_lossy();
match self.state {
State::Done(Ok(dur)) => {
write!(f,
"{}.{:03} {}",
dur.as_secs(),
dur.subsec_nanos() / 1000000,
p)
}
State::Done(Err(ref e)) => write!(f, "FAIL {}: {}", p, e),
_ => write!(f, "{}", p),
}
}
}
pub struct TestRunner {
verbose: bool,
// Directories that have not yet been scanned.
dir_stack: Vec<PathBuf>,
// Filenames of tests to run.
tests: Vec<QueueEntry>,
// Pointer into `tests` where the `New` entries begin.
new_tests: usize,
// Number of contiguous reported tests at the front of `tests`.
reported_tests: usize,
// Number of errors seen so far.
errors: usize,
// Number of ticks received since we saw any progress.
ticks_since_progress: usize,
threads: Option<ConcurrentRunner>,
}
impl TestRunner {
/// Create a new blank TrstRunner.
pub fn new(verbose: bool) -> TestRunner {
TestRunner {
verbose: verbose,
dir_stack: Vec::new(),
tests: Vec::new(),
new_tests: 0,
reported_tests: 0,
errors: 0,
ticks_since_progress: 0,
threads: None,
}
}
/// Add a directory path to be scanned later.
///
/// If `dir` turns out to be a regular file, it is silently ignored.
/// Otherwise, any problems reading the directory are reported.
pub fn push_dir<P: Into<PathBuf>>(&mut self, dir: P) {
self.dir_stack.push(dir.into());
}
/// Add a test to be executed later.
///
/// Any problems reading `file` as a test case file will be reported as a test failure.
pub fn push_test<P: Into<PathBuf>>(&mut self, file: P) {
self.tests.push(QueueEntry {
path: file.into(),
state: State::New,
});
}
/// Begin running tests concurrently.
pub fn start_threads(&mut self) {
assert!(self.threads.is_none());
self.threads = Some(ConcurrentRunner::new());
}
/// Scan any directories pushed so far.
/// Push any potential test cases found.
pub fn scan_dirs(&mut self) {
// This recursive search tries to minimize statting in a directory hierarchy containing
// mostly test cases.
//
// - Directory entries with a "cton" extension are presumed to be test case files.
// - Directory entries with no extension are presumed to be subdirectories.
// - Anything else is ignored.
//
while let Some(dir) = self.dir_stack.pop() {
match dir.read_dir() {
Err(err) => {
// Fail silently if `dir` was actually a regular file.
// This lets us skip spurious extensionless files without statting everything
// needlessly.
if !dir.is_file() {
self.path_error(dir, err);
}
}
Ok(entries) => {
// Read all directory entries. Avoid statting.
for entry_result in entries {
match entry_result {
Err(err) => {
// Not sure why this would happen. `read_dir` succeeds, but there's
// a problem with an entry. I/O error during a getdirentries
// syscall seems to be the reason. The implementation in
// libstd/sys/unix/fs.rs seems to suggest that breaking now would
// be a good idea, or the iterator could keep returning the same
// error forever.
self.path_error(dir, err);
break;
}
Ok(entry) => {
let path = entry.path();
// Recognize directories and tests by extension.
// Yes, this means we ignore directories with '.' in their name.
match path.extension().and_then(OsStr::to_str) {
Some("cton") => self.push_test(path),
Some(_) => {}
None => self.push_dir(path),
}
}
}
}
}
}
// Get the new jobs running before moving on to the next directory.
self.schedule_jobs();
}
}
/// Report an error related to a path.
fn path_error<E: Error>(&mut self, path: PathBuf, err: E) {
self.errors += 1;
println!("{}: {}", path.to_string_lossy(), err);
}
/// Report on the next in-order job, if it's done.
fn report_job(&self) -> bool {
let jobid = self.reported_tests;
if let Some(&QueueEntry { state: State::Done(ref result), .. }) = self.tests.get(jobid) {
if self.verbose || result.is_err() {
println!("{}", self.tests[jobid]);
}
true
} else {
false
}
}
/// Schedule any new jobs to run.
fn schedule_jobs(&mut self) {
for jobid in self.new_tests..self.tests.len() {
assert_eq!(self.tests[jobid].state, State::New);
if let Some(ref mut conc) = self.threads {
// Queue test for concurrent execution.
self.tests[jobid].state = State::Queued;
conc.put(jobid, self.tests[jobid].path());
} else {
// Run test synchronously.
self.tests[jobid].state = State::Running;
let result = runone::run(self.tests[jobid].path());
self.finish_job(jobid, result);
}
self.new_tests = jobid + 1;
}
// Check for any asynchronous replies without blocking.
while let Some(reply) = self.threads.as_mut().and_then(ConcurrentRunner::try_get) {
self.handle_reply(reply);
}
}
/// Report the end of a job.
fn finish_job(&mut self, jobid: usize, result: TestResult) {
assert_eq!(self.tests[jobid].state, State::Running);
if result.is_err() {
self.errors += 1;
}
self.tests[jobid].state = State::Done(result);
// Rports jobs in order.
while self.report_job() {
self.reported_tests += 1;
}
}
/// Handle a reply from the async threads.
fn handle_reply(&mut self, reply: Reply) {
match reply {
Reply::Starting { jobid, .. } => {
assert_eq!(self.tests[jobid].state, State::Queued);
self.tests[jobid].state = State::Running;
}
Reply::Done { jobid, result } => {
self.ticks_since_progress = 0;
self.finish_job(jobid, result)
}
Reply::Tick => {
self.ticks_since_progress += 1;
if self.ticks_since_progress == TIMEOUT_SLOW {
println!("STALLED for {} seconds with {}/{} tests finished",
self.ticks_since_progress,
self.reported_tests,
self.tests.len());
for jobid in self.reported_tests..self.tests.len() {
if self.tests[jobid].state == State::Running {
println!("slow: {}", self.tests[jobid]);
}
}
}
if self.ticks_since_progress >= TIMEOUT_PANIC {
panic!("worker threads stalled for {} seconds.",
self.ticks_since_progress);
}
}
}
}
/// Drain the async jobs and shut down the threads.
fn drain_threads(&mut self) {
if let Some(mut conc) = self.threads.take() {
conc.shutdown();
while self.reported_tests < self.tests.len() {
match conc.get() {
Some(reply) => self.handle_reply(reply),
None => break,
}
}
conc.join();
}
}
/// Print out a report of slow tests.
fn report_slow_tests(&self) {
// Collect runtimes of succeeded tests.
let mut times = self.tests
.iter()
.filter_map(|entry| match *entry {
QueueEntry { state: State::Done(Ok(dur)), .. } => Some(dur),
_ => None,
})
.collect::<Vec<_>>();
// Get me some real data, kid.
let len = times.len();
if len < 4 {
return;
}
// Compute quartiles.
times.sort();
let qlen = len / 4;
let q1 = times[qlen];
let q3 = times[len - 1 - qlen];
// Inter-quartile range.
let iqr = q3 - q1;
// Cut-off for what we consider a 'slow' test: 1.5 IQR from the 75% quartile.
// These are the data points that would be plotted as outliers outside a box plot.
let cut = q3 + iqr * 2 / 3;
if cut > *times.last().unwrap() {
return;
}
for t in self.tests
.iter()
.filter(|entry| match **entry {
QueueEntry { state: State::Done(Ok(dur)), .. } => dur > cut,
_ => false,
}) {
println!("slow: {}", t)
}
}
/// Scan pushed directories for tests and run them.
pub fn run(&mut self) -> CommandResult {
self.scan_dirs();
self.schedule_jobs();
self.drain_threads();
self.report_slow_tests();
println!("{} tests", self.tests.len());
match self.errors {
0 => Ok(()),
1 => Err("1 failure".to_string()),
n => Err(format!("{} failures", n)),
}
}
}

115
src/filetest/runone.rs Normal file
View File

@@ -0,0 +1,115 @@
//! Run the tests in a single test file.
use std::borrow::Cow;
use std::path::Path;
use std::time;
use cretonne::ir::Function;
use cretonne::isa::TargetIsa;
use cretonne::settings::Flags;
use cretonne::verify_function;
use cton_reader::parse_test;
use cton_reader::IsaSpec;
use utils::read_to_string;
use filetest::{TestResult, new_subtest};
use filetest::subtest::{SubTest, Context, Result};
/// Load `path` and run the test in it.
///
/// If running this test causes a panic, it will propagate as normal.
pub fn run(path: &Path) -> TestResult {
let started = time::Instant::now();
let buffer = try!(read_to_string(path).map_err(|e| e.to_string()));
let testfile = try!(parse_test(&buffer).map_err(|e| e.to_string()));
if testfile.functions.is_empty() {
return Err("no functions found".to_string());
}
// Parse the test commands.
let mut tests = try!(testfile.commands.iter().map(new_subtest).collect::<Result<Vec<_>>>());
// Flags to use for those tests that don't need an ISA.
// This is the cumulative effect of all the `set` commands in the file.
let flags = match testfile.isa_spec {
IsaSpec::None(ref f) => f,
IsaSpec::Some(ref v) => v.last().expect("Empty ISA list").flags(),
};
// Sort the tests so the mutators are at the end, and those that don't need the verifier are at
// the front.
tests.sort_by_key(|st| (st.is_mutating(), st.needs_verifier()));
// Expand the tests into (test, flags, isa) tuples.
let mut tuples = try!(test_tuples(&tests, &testfile.isa_spec, flags));
// Isolate the last test in the hope that this is the only mutating test.
// If so, we can completely avoid cloning functions.
let last_tuple = match tuples.pop() {
None => return Err("no test commands found".to_string()),
Some(t) => t,
};
for (func, details) in testfile.functions {
let mut context = Context {
details: details,
verified: false,
flags: flags,
isa: None,
};
for tuple in &tuples {
try!(run_one_test(*tuple, Cow::Borrowed(&func), &mut context));
}
// Run the last test with an owned function which means it won't need to clone it before
// mutating.
try!(run_one_test(last_tuple, Cow::Owned(func), &mut context));
}
// TODO: Actually run the tests.
Ok(started.elapsed())
}
// Given a slice of tests, generate a vector of (test, flags, isa) tuples.
fn test_tuples<'a>(tests: &'a [Box<SubTest>],
isa_spec: &'a IsaSpec,
no_isa_flags: &'a Flags)
-> Result<Vec<(&'a SubTest, &'a Flags, Option<&'a TargetIsa>)>> {
let mut out = Vec::new();
for test in tests {
if test.needs_isa() {
match *isa_spec {
IsaSpec::None(_) => {
// TODO: Generate a list of default ISAs.
return Err(format!("test {} requires an ISA", test.name()));
}
IsaSpec::Some(ref isas) => {
for isa in isas {
out.push((&**test, isa.flags(), Some(&**isa)));
}
}
}
} else {
out.push((&**test, no_isa_flags, None));
}
}
Ok(out)
}
fn run_one_test<'a>(tuple: (&'a SubTest, &'a Flags, Option<&'a TargetIsa>),
func: Cow<Function>,
context: &mut Context<'a>)
-> Result<()> {
let (test, flags, isa) = tuple;
let name = format!("{}({})", test.name(), func.name);
context.flags = flags;
context.isa = isa;
// Should we run the verifier before this test?
if !context.verified && test.needs_verifier() {
try!(verify_function(&func).map_err(|e| e.to_string()));
context.verified = true;
}
test.run(func, context).map_err(|e| format!("{}: {}", name, e))
}

95
src/filetest/subtest.rs Normal file
View File

@@ -0,0 +1,95 @@
//! SubTest trait.
use std::result;
use std::borrow::Cow;
use cretonne::ir::Function;
use cretonne::isa::TargetIsa;
use cretonne::settings::Flags;
use cton_reader::Details;
use filecheck::{self, CheckerBuilder, Checker, Value as FCValue};
pub type Result<T> = result::Result<T, String>;
/// Context for running a a test on a single function.
pub struct Context<'a> {
/// Additional details about the function from the parser.
pub details: Details<'a>,
/// Was the function verified before running this test?
pub verified: bool,
/// ISA-independent flags for this test.
pub flags: &'a Flags,
/// Target ISA to test against. Only present for sub-tests whose `needs_isa` method returned
/// true.
pub isa: Option<&'a TargetIsa>,
}
/// Common interface for implementations of test commands.
///
/// Each `.cton` test file may contain multiple test commands, each represented by a `SubTest`
/// trait object.
pub trait SubTest {
/// Name identifying this subtest. Typically the same as the test command.
fn name(&self) -> Cow<str>;
/// Should the verifier be run on the function before running the test?
fn needs_verifier(&self) -> bool {
true
}
/// Does this test mutate the function when it runs?
/// This is used as a hint to avoid cloning the function needlessly.
fn is_mutating(&self) -> bool {
false
}
/// Does this test need a `TargetIsa` trait object?
fn needs_isa(&self) -> bool {
false
}
/// Run this test on `func`.
fn run(&self, func: Cow<Function>, context: &Context) -> Result<()>;
}
/// Make the parser's source map available as filecheck variables.
///
/// This means that the filecheck directives can refer to entities like `jump $ebb3`, where `$ebb3`
/// will expand to the EBB number that was assigned to `ebb3` in the input source.
///
/// The expanded entity names are wrapped in word boundary regex guards so that 'inst1' doesn't
/// match 'inst10'.
impl<'a> filecheck::VariableMap for Context<'a> {
fn lookup(&self, varname: &str) -> Option<FCValue> {
self.details.map.lookup_str(varname).map(|e| FCValue::Regex(format!(r"\b{}\b", e).into()))
}
}
/// Run filecheck on `text`, using directives extracted from `context`.
pub fn run_filecheck(text: &str, context: &Context) -> Result<()> {
let checker = try!(build_filechecker(&context.details));
if try!(checker.check(&text, context).map_err(|e| format!("filecheck: {}", e))) {
Ok(())
} else {
// Filecheck mismatch. Emit an explanation as output.
let (_, explain) = try!(checker.explain(&text, context)
.map_err(|e| format!("explain: {}", e)));
Err(format!("filecheck failed:\n{}{}", checker, explain))
}
}
/// Build a filechecker using the directives in the function's comments.
pub fn build_filechecker(details: &Details) -> Result<Checker> {
let mut builder = CheckerBuilder::new();
for comment in &details.comments {
try!(builder.directive(comment.text).map_err(|e| format!("filecheck: {}", e)));
}
let checker = builder.finish();
if checker.is_empty() {
Err("no filecheck directives in function".to_string())
} else {
Ok(checker)
}
}

78
src/filetest/verifier.rs Normal file
View File

@@ -0,0 +1,78 @@
//! Test command for checking the IL verifier.
//!
//! The `test verifier` test command looks for annotations on instructions like this:
//!
//! jump ebb3 ; error: jump to non-existent EBB
//!
//! This annotation means that the verifier is expected to given an error for the jump instruction
//! containing the substring "jump to non-existent EBB".
use std::borrow::{Borrow, Cow};
use cretonne::verify_function;
use cretonne::ir::Function;
use cton_reader::TestCommand;
use filetest::subtest::{SubTest, Context, Result};
use utils::match_directive;
struct TestVerifier;
pub fn subtest(parsed: &TestCommand) -> Result<Box<SubTest>> {
assert_eq!(parsed.command, "verifier");
if !parsed.options.is_empty() {
Err(format!("No options allowed on {}", parsed))
} else {
Ok(Box::new(TestVerifier))
}
}
impl SubTest for TestVerifier {
fn name(&self) -> Cow<str> {
Cow::from("verifier")
}
fn needs_verifier(&self) -> bool {
// Running the verifier before this test would defeat its purpose.
false
}
fn run(&self, func: Cow<Function>, context: &Context) -> Result<()> {
let func = func.borrow();
// Scan source annotations for "error:" directives.
let mut expected = None;
for comment in &context.details.comments {
if let Some(tail) = match_directive(comment.text, "error:") {
// Currently, the verifier can only report one problem at a time.
// Reject more than one `error:` directives.
if expected.is_some() {
return Err("cannot handle multiple error: directives".to_string());
}
expected = Some((comment.entity, tail));
}
}
match verify_function(func) {
Ok(_) => {
match expected {
None => Ok(()),
Some((_, msg)) => Err(format!("passed, expected error: {}", msg)),
}
}
Err(got) => {
match expected {
None => Err(format!("verifier pass, got {}", got)),
Some((want_loc, want_msg)) if got.message.contains(want_msg) => {
if want_loc == got.location {
Ok(())
} else {
Err(format!("correct error reported on {}, but wanted {}",
got.location,
want_loc))
}
}
Some(_) => Err(format!("mismatching error: {}", got)),
}
}
}
}
}