Skip to content

Instantly share code, notes, and snippets.

@vincentdephily
Created December 10, 2019 16:31
Show Gist options
  • Save vincentdephily/592546c41ff9713adff234e5535aa6d4 to your computer and use it in GitHub Desktop.
Save vincentdephily/592546c41ff9713adff234e5535aa6d4 to your computer and use it in GitHub Desktop.
// UPSTREAMING: Though it has seen some refactoring, this code has grown organically and doesn't use
// the latest and greatest assert_cmd APIs (actually it only uses assrt_fs). This file has been
// carelessly stripped from an internal project, don't expect it to compile as-is.
use assert_fs::{prelude::*, TempDir};
use libc::{c_int, kill, SIGCONT, SIGINT, SIGKILL, SIGTERM};
use log::*;
use regex::Regex;
use std::{collections::{HashMap, HashSet},
env,
ffi::OsStr,
fmt::{Debug, Display, Formatter, Result as FmtResult},
fs::File,
io::{Read, Write},
iter::FromIterator,
net::TcpListener,
ops::Range,
os::unix::process::ExitStatusExt,
path::Path,
process::{Child, Command, Stdio},
sync::atomic::{AtomicU16, Ordering},
thread::sleep,
time::{Duration, Instant}};
/// Test file content.
// UPSTREAMING: probably reinventing the wheel here, but it's a small one that fits well.
pub struct FilePredicate {
/// Desciption for assert-logging purpose.
desc: String,
/// Which file to operate on.
file: String,
/// Closure that tests the content of the file.
pred: Box<dyn Fn(&str) -> bool>,
}
impl FilePredicate {
pub fn new(desc: &str, file: &str, p: impl Fn(&str) -> bool + 'static) -> Self {
FilePredicate { desc: desc.to_owned(), file: file.to_owned(), pred: Box::new(p) }
}
}
impl Debug for FilePredicate {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
write!(f, "FilePredicate({}, {})", self.desc, self.file)
}
}
/// Represents a file's name and its data (either from an existing file or directly as Vec<u8>).
#[derive(Debug)]
pub enum FileIn {
From(&'static str, &'static str),
Bin(&'static str, Vec<u8>),
}
/// Create a temp (delete-on-drop) working dir, seeded with some data.
///
/// All files will be deleted when the returned TempDir goes out of scope (unless env contains
/// ASSERTFS_KEEP_TEMP, but that's a user-level feature), and the caller is responsible for making
/// sure that TempDir lives long enough.
// UPSTREAMING: Needs a better persist_if() API. The 'testfailed' and 'testok' files are workarounds
// for the lack of advanced persist strategies.
pub fn workdir_init(files: &[FileIn]) -> TempDir {
// Create temp working dir and decide if it'll delete on Drop.
let wd = TempDir::new().unwrap().persist_if(env::var("ASSERTFS_KEEP_TEMP").is_ok());
if env::var("ASSERTFS_KEEP_TEMP").is_ok() {
warn!("Temp files can be viewed in {}/", wd.path().display());
} else {
warn!("Temp files will be deleted, set ASSERTFS_KEEP_TEMP=1 in your shell to keep them.");
}
wd.child("testfailed").touch().expect("couldn't touch testfailed");
// Setup the input files, if any.
for file in files {
match file {
FileIn::From(n, s) => wd.child(n)
.write_file(&Path::new("test").join(s))
.expect(&format!("Couldn't copy test/{:?}", s)),
FileIn::Bin(n, s) => {
wd.child(n).write_binary(s).expect(&format!("Couldn't write_binary {:?}", s))
},
}
}
// Done.
wd
}
/// Check file contents inside workdir.
// TODO: Provide a nice way to check that no other file was created.
pub fn workdir_check(wd: TempDir, checks: &[FilePredicate]) {
for c in checks {
let mut sb = String::new();
let f = wd.child(&c.file);
let path = f.path();
File::open(path).expect(&format!("Couldn't open {:?}", path))
.read_to_string(&mut sb)
.expect(&format!("Couldn't read {:?} as utf8", path));
assert!((c.pred)(&sb), "Check {:?} failed: file={} content=>>>>{}<<<<", c.desc, c.file, sb);
}
wd.child("testok").touch().expect("couldn't touch testok");
std::fs::remove_file(wd.child("testfailed").path()).expect("couldn't remove testfailed");
}
/// Convenience type to give program arguments as either a `&str` (will get split by whitespace) or
/// a `&[&str]` (Note that this is a slice, not an array. The easyest way to get create that is
/// `["a","b"].as_ref()`.).
pub struct ExecArgs(Vec<String>);
impl From<&str> for ExecArgs {
fn from(s: &str) -> Self {
ExecArgs(s.split_whitespace().map(str::to_string).collect())
}
}
impl From<String> for ExecArgs {
fn from(s: String) -> Self {
ExecArgs(s.split_whitespace().map(str::to_string).collect())
}
}
impl From<&[&str]> for ExecArgs {
fn from(s: &[&str]) -> Self {
ExecArgs(s.iter().map(|s| s.to_string()).collect())
}
}
#[derive(Debug)]
enum CmdState {
Wait(CmdCond),
Started(Child, Instant),
Done,
}
/// This is used by the run loop to decide when to start or signal a `Cmd`.
#[derive(Debug)]
pub enum CmdCond {
/// Immediately true
None,
/// Duration elapsed
Delay(Duration),
/// Other Cmd exited
Cmd(String),
/// File-based predicate
Predicate(FilePredicate),
}
impl CmdCond {
fn check(&self, wd: &TempDir, elapsed: Duration, done: &[String]) -> bool {
match self {
CmdCond::None => true,
CmdCond::Delay(d) => elapsed > *d,
CmdCond::Cmd(c) => done.contains(c),
CmdCond::Predicate(p) => {
let mut sb = String::new();
let f = wd.child(&p.file);
let path = f.path();
File::open(path).map(|mut f| f.read_to_string(&mut sb))
.map(|_| (p.pred)(&sb))
.unwrap_or(false)
},
}
}
}
// UPSTREAMING: These are handy shortcuts (for example `cmd.after(("filename","regex"))`) but hard
// to discover, may need more verbose wrappers just for readability's sake.
impl From<u64> for CmdCond {
fn from(d: u64) -> Self {
CmdCond::Delay(Duration::from_millis(d))
}
}
impl From<&str> for CmdCond {
fn from(c: &str) -> Self {
CmdCond::Cmd(String::from(c))
}
}
impl From<(&str, &str)> for CmdCond {
fn from(file_regex: (&str, &str)) -> Self {
let re = Regex::new(file_regex.1).unwrap();
CmdCond::Predicate(FilePredicate::new("match", file_regex.0, move |s| re.is_match(s)))
}
}
/// This "unittest" actually behaves like the main binary (except some extra output on stdout, and
/// assuming test code doesn't seep into normal code). Use "--" to separate the test binary's
/// arguments from the fakemain binary's arguments:
///
/// `cargo test -- fakemain --ignored --nocapture -- the real args`
// UPSTREAMING: This is a kludge. It's a very useful one because it doesn't compile the normal
// binary at test runtime, saving time but more importantly being able to run tests on a host
// without a compiler. The proper (slower, less crosscompile-friendly) way to to use `lazy_static`
// and `escargot`:
// lazy_static! {
// static ref CARGO_RUN: CargoRun = CargoBuild::new().bin(env!("CARGO_PKG_NAME"))
// .current_release()
// .current_target()
// .run()
// .unwrap();
// }
// But maybe everybody's need is different, and `asset_cmd` should just have doc examples of
// `Cmd.main()` instead of an actual implmentation.
#[test]
#[ignore]
fn fakemain() {
use structopt::StructOpt;
main_(Opt::from_iter(std::env::args_os().skip_while(|a| a != "--")))
}
#[derive(Debug)]
pub struct Cmd {
/// Program to run.
bin: Command,
/// Command name for `after()`, std/err filname prefix, and logs.
name: String,
/// Expected exit status. Some(0)=success, Some(n)=failure, None=timeout.
exit: Option<i32>,
/// Fail if the cmd exits too early.
mintime: Duration,
/// Fail if the cmd run for too long.
maxtime: Duration,
/// Current state.
state: CmdState,
/// List of signals to send to the process after startup.
signals: Vec<(CmdCond, c_int)>,
}
impl Cmd {
/// Constructor using the crate's main binary.
///
/// This actually runs the test binary's "fakemain" test, to save on compiling and simplify
/// cross-compilation.
pub fn main(name: &str, args: impl Into<ExecArgs>) -> Self {
let mut bin = Command::new(std::env::args_os().nth(0).unwrap());
bin.args(&["fakemain", "--ignored", "--nocapture", "--"]);
bin.args(args.into().0);
Cmd { bin,
name: String::from(name),
exit: Some(0),
mintime: Duration::from_millis(0),
maxtime: Duration::from_millis(2000),
state: CmdState::Wait(CmdCond::None),
signals: Vec::new() }
}
/// Constructor using any binary.
pub fn any(name: &str, bin: &str, args: impl Into<ExecArgs>) -> Self {
let mut bin = Command::new(bin);
bin.args(args.into().0);
Cmd { bin,
name: String::from(name),
exit: Some(0),
mintime: Duration::from_millis(0),
maxtime: Duration::from_millis(2000),
state: CmdState::Wait(CmdCond::None),
signals: Vec::new() }
}
// UPSTREAMING: I also have some specialized contructors out of scope for assert_cmd, but we
// should make sure that the the API makes extensions like this easy.
pub fn myconstructor(name: impl Into<String>, foo: &str, after: impl Into<String>) -> Self {
unimplemented!()
}
/// Constructor using a shell (bash) command.
// UPSTREAMING: No attempt was made to make this work on Windows or other non-*nix OSes.
pub fn bash(name: &str, script: &str) -> Self {
let mut bin = Command::new("bash");
bin.args(&["-c", script]);
Cmd { bin,
name: String::from(name),
exit: Some(0),
mintime: Duration::from_millis(0),
maxtime: Duration::from_millis(2000),
state: CmdState::Wait(CmdCond::None),
signals: Vec::new() }
}
/// Append more args to be passed to the binary.
pub fn args(mut self, args: impl Into<ExecArgs>) -> Self {
self.bin.args(args.into().0);
self
}
/// Set env variable.
pub fn env(mut self, key: impl AsRef<OsStr>, val: impl AsRef<OsStr>) -> Self {
self.bin.env(key, val);
self
}
/// Set env variable if not already set.
pub fn env_default(mut self, key: impl AsRef<OsStr>, val: impl AsRef<OsStr>) -> Self {
if env::var(&key).is_err() {
self.bin.env(key, val);
}
self
}
/// Unset env variable.
pub fn env_remove(mut self, key: impl AsRef<OsStr>) -> Self {
self.bin.env_remove(key);
self
}
/// Expected ExitStatus (default `Some(0)`). This will be the program's return value if it
/// finishes voluntarily, the signal number if killed by a signal, and None if it timed out.
// UPSTREAMING: The dual-meaning return_value/signal_number is handy but not very clean.
pub fn exit(mut self, e: impl Into<Option<i32>>) -> Self {
self.exit = e.into();
self
}
/// Fail if programs exits before `t` ms (defaults to 0).
pub fn mintime(mut self, t: u64) -> Self {
self.mintime = Duration::from_millis(t);
self
}
/// Fail if programs runs for longer than `t` ms (defaults to 2000).
pub fn maxtime(mut self, t: u64) -> Self {
self.maxtime = Duration::from_millis(t);
self
}
/// Send signal to the program after CmdCond. Call this multiple times to send multiple signals.
pub fn signal(mut self, sig: c_int, cond: impl Into<CmdCond>) -> Self {
self.signals.push((cond.into(), sig));
self
}
/// Wait for `CmdCond` before starting this one.
pub fn after(mut self, cond: impl Into<CmdCond>) -> Self {
self.state = CmdState::Wait(cond.into());
self
}
// UPSTREAMING: Like myconstructor(), some setters are always the same and the API should
// accomodate that. For example, delaying start of a command until a log file matches a regex.
pub fn mysetter(mut self) -> Self {
unimplemented!()
}
}
impl Drop for Cmd {
/// Make sure the process dies when the struct is droped. Needed when the Cmd timed out, when
/// another Cmd triggered an assert, etc.
fn drop(&mut self) {
if let CmdState::Started(ref mut child, _) = self.state {
if let None = child.try_wait().unwrap() {
warn!("Cmd {} process alive when struct droped, killing {}", self.name, child.id());
unsafe { kill(child.id() as i32, SIGKILL) };
}
}
}
}
/// Run a list of external commands in a controlled environement, and check result.
/// See `Cmd`, `FileIn`, `FilePredicate`, and unittests for docs and examples.
// TODO: Support pipes (unless emulating by calling `sh -c 'cmd1|cmd2'` is enough).
#[derive(Debug)]
pub struct Exec {
cmds: Vec<Cmd>,
timeout: Duration,
files: Vec<FileIn>,
checks: Vec<FilePredicate>,
}
/// Create a new empty `Exec` (defined as a standalone fn to save on typing).
pub fn exec() -> Exec {
log_init();
Exec { cmds: vec![], timeout: Duration::from_millis(10000), files: vec![], checks: vec![] }
}
impl Exec {
/// Add a new `Cmd` to the run list.
pub fn cmd(mut self, c: Cmd) -> Self {
self.cmds.push(c);
self
}
/// Seed the run env with an existing file from the `test/` folder.
pub fn infile(mut self, name: &'static str, src: &'static str) -> Self {
self.files.push(FileIn::From(name, src));
self
}
/// Seed the run env with a file with the given content.
pub fn inbytes(mut self, name: &'static str, src: impl Into<Vec<u8>>) -> Self {
self.files.push(FileIn::Bin(name, src.into()));
self
}
/// Check file contents after programs have run. The files `{N}.out` and `{N}.err` correspond to
/// the stdout and stderr of the command N (see `Cmd::set_name()`).
pub fn check(mut self,
desc: &'static str, // Desciption for assert-logging purpose.
file: &'static str, // Which file to operate on.
pred: impl Fn(&str) -> bool + 'static // Closure that tests the content of the file.
) -> Self {
self.checks.push(FilePredicate::new(desc, file, pred));
self
}
/// Like `check()` but for the common usecase of checking against a regex.
pub fn check_re(mut self,
desc: &'static str, // Desciption for assert-logging purpose.
file: &'static str, // Which file to operate on.
regex: &str // Regex that the file content has to match.
) -> Self {
let re = Regex::new(regex).unwrap();
self.checks.push(FilePredicate::new(desc, file, move |s| re.is_match(s)));
self
}
/// Like `check_re()` but checks that the number of matches is within a specified range.
// UPSTREAMING: Should probably merge with `check_re()`.
pub fn check_re_n(mut self,
desc: &'static str, // Description for assert-logging purpose.
file: &'static str, // Which file to operate on.
regex: &str, // Regex that the file content has to match.
n: Range<usize>)
-> Self {
let re = Regex::new(regex).unwrap();
self.checks
.push(FilePredicate::new(desc, file, move |s| n.contains(&re.find_iter(s).count())));
self
}
/// Set overall run timeout (default 10s).
pub fn timeout(mut self, t: u64) -> Self {
self.timeout = Duration::from_millis(t);
self
}
/// Actually run the test (setup files, launch programs, check outputs...).
pub fn run(mut self) {
// Init
let wd = workdir_init(&self.files);
let startall = Instant::now();
// TODO: how can we get the test name ?
let mut testdesc = File::create(wd.child("testdesc").path()).unwrap();
writeln!(testdesc, "{:?}", self).unwrap();
// Check that we're not waiting for an unknown cmd or self
let names: Vec<String> = self.cmds.iter().map(|c| c.name.clone()).collect();
for c in self.cmds.iter() {
if let CmdState::Wait(CmdCond::Cmd(ref w)) = c.state {
assert!(names.contains(w), "cmd {} start after {}, {:?}", c.name, w, names);
assert!(*w != c.name, "cmd {} start after itself", c.name);
}
for s in c.signals.iter() {
if let (CmdCond::Cmd(ref w), _) = s {
assert!(names.contains(w), "cmd {} signal after {}, {:?}", c.name, w, names);
assert!(*w != c.name, "cmd {} signal after itself", c.name);
}
}
}
// Until all Cmds are `Done`, handle each one in turn depending on their state.
let mut done: Vec<String> = vec![];
while self.cmds.len() > done.len() {
let now = Instant::now();
assert!(now < startall + self.timeout, "overall run timeout {:?}", now - startall);
for c in self.cmds.iter_mut() {
trace!("{:?} {:?}", now, c);
match c.state {
// Cmd is ready to execute, spawn it as a child process.
CmdState::Wait(ref cond) if cond.check(&wd, now - startall, &done) => {
let so = File::create(wd.child(format!("{}.out", c.name)).path()).unwrap();
let se = File::create(wd.child(format!("{}.err", c.name)).path()).unwrap();
let cmd = c.bin
.current_dir(wd.path())
.stdin(Stdio::null()) // UPSTREAMING: should have more options for stdin
.stdout(Stdio::from(so))
.stderr(Stdio::from(se));
info!("Cmd {} start: {:?}", c.name, cmd);
c.state = CmdState::Started(cmd.spawn().expect("Failed to start"), now);
},
// Check if Cmd has exited with expected code, or timeouted
CmdState::Started(ref mut child, started) => {
let elapsed = now - started;
if let Some(exit) = child.try_wait().unwrap() {
info!("Cmd {} exit: {:?} after {:?}", c.name, exit, elapsed);
assert!(elapsed > c.mintime, "{} exit before {:?}", c.name, c.mintime);
assert!(c.exit == exit.signal().or(exit.code()),
"{} expected exit {:?} got {:?}",
c.name,
c.exit,
exit);
c.state = CmdState::Done;
done.push(c.name.clone());
} else if elapsed > c.maxtime {
info!("Cmd {} timeout after {:?}", c.name, elapsed);
assert!(None == c.exit,
"{} expected exit {:?} got timeout",
c.name,
c.exit);
c.state = CmdState::Done;
done.push(c.name.clone());
} else {
// TODO: refactor with https://github.com/rust-lang/rust/issues/43244
let mut i = 0;
while i < c.signals.len() {
if c.signals[i].0.check(&wd, elapsed, &done) {
let s = c.signals[i].1;
info!("Cmd {} sending signal {} to {}", c.name, s, child.id());
c.signals.remove(i);
// std::process::Child::kill() exists, but doesn't allow specifying which signal.
unsafe { assert_eq!(0, kill(child.id() as i32, s), "kill()") };
} else {
i += 1;
}
}
}
},
// Nothing to do: Cmd either Done or pending
_ => (),
}
}
// UPSTREAMING: Yeah, that's lazy coding. Should use futures and async instead.
sleep(Duration::from_millis(10));
}
// Check results
workdir_check(wd, &self.checks);
}
}
// UPSTREAMING: Another pattern that I can't share the code for is elaborate builders for the
// `FilePredicate.pred` closure.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment