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:
150
src/filetest/concurrent.rs
Normal file
150
src/filetest/concurrent.rs
Normal 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
103
src/filetest/domtree.rs
Normal 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
45
src/filetest/legalizer.rs
Normal 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
62
src/filetest/mod.rs
Normal 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
330
src/filetest/runner.rs
Normal 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
115
src/filetest/runone.rs
Normal 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
95
src/filetest/subtest.rs
Normal 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
78
src/filetest/verifier.rs
Normal 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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user