mod cli {
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::{SystemTime, Duration};
use anyhow::Result;
use test_dir::{TestDir, DirBuilder, FileType};
use std::fs::File;
use std::io::Read;
const COUNT_INVOCATIONS: &str = r#"file=${1:?} lines=0; \
printf '%s' '.' >> "$file"; \
read < "$file"; \
printf '%s' "${#REPLY}";"#;
const PRINT_ARGS: &str = r#"args=("$@"); declare -p args;"#;
const EXIT_WITH: &str = r#"exit "${1:?}";"#;
const EXIT_WITH_ENV: &str = r#"exit "${EXIT_WITH:?}";"#;
const AWAIT_AND_TOUCH: &str = r#"echo awaiting; \
until [[ -e "${1:?}" ]]; do sleep .1; done; \
echo > "${2:?}";"#;
fn bkt<P: AsRef<Path>>(cache_dir: P) -> Command {
let test_exe = std::env::current_exe().expect("Could not resolve test location");
let dir = test_exe
.parent().expect("Could not resolve test directory")
.parent().expect("Could not resolve binary directory");
let mut path = dir.join("bkt");
if !path.exists() {
path.set_extension("exe");
}
assert!(path.exists(), "Could not find bkt binary in {:?}", dir);
let mut bkt = Command::new(&path);
bkt.env("BKT_TTL", "5s");
bkt.env("BKT_TMPDIR", cache_dir.as_ref().as_os_str());
bkt
}
fn sudo(cmd: &mut Command) -> Command {
let mut sudo = Command::new("sudo");
sudo.args(&["-n", "-E"]).arg(cmd.get_program()).args(cmd.get_args());
for (key, value) in cmd.get_envs() {
match value {
Some(value) => sudo.env(key, value),
None => sudo.env_remove(key),
};
}
sudo
}
#[derive(Eq, PartialEq, Debug)]
struct CmdResult {
out: String,
err: String,
status: Option<i32>,
}
impl From<std::process::Output> for CmdResult {
fn from(output: std::process::Output) -> Self {
CmdResult {
out: std::str::from_utf8(&output.stdout).unwrap().into(),
err: std::str::from_utf8(&output.stderr).unwrap().into(),
status: output.status.code()
}
}
}
fn run(cmd: &mut Command) -> CmdResult {
cmd.output().unwrap().into()
}
fn succeed(cmd: &mut Command) -> String {
let result = run(cmd);
if cfg!(feature="debug") {
if !result.err.is_empty() { eprintln!("stderr:\n{}", result.err); }
} else {
assert_eq!(result.err, "");
}
assert_eq!(result.status, Some(0));
result.out
}
fn wait_for_contents_to_change<P: AsRef<Path>>(file: P, initial_contents: &str) {
for _ in 1..50 {
if std::fs::read_to_string(&file).unwrap() != initial_contents { return; }
std::thread::sleep(Duration::from_millis(100));
}
panic!("Contents of {} did not change", file.as_ref().to_string_lossy());
}
fn make_dir_stale<P: AsRef<Path>>(dir: P, age: Duration) -> Result<()> {
debug_assert!(dir.as_ref().is_dir());
let desired_time = SystemTime::now() - age;
let stale_time = filetime::FileTime::from_system_time(desired_time);
for entry in std::fs::read_dir(dir)? {
let path = entry?.path();
let last_modified = std::fs::metadata(&path)?.modified()?;
if path.is_file() && last_modified > desired_time {
filetime::set_file_mtime(&path, stale_time)?;
} else if path.is_dir() {
make_dir_stale(&path, age)?;
}
}
Ok(())
}
fn make_file_stale<P: AsRef<Path>>(file: P, age: Duration) -> Result<()> {
debug_assert!(file.as_ref().is_file());
let desired_time = SystemTime::now() - age;
let stale_time = filetime::FileTime::from_system_time(desired_time);
filetime::set_file_mtime(&file, stale_time)?;
Ok(())
}
fn join<A: Clone>(beg: &[A], tail: &[A]) -> Vec<A> {
beg.iter().chain(tail).cloned().collect()
}
#[test]
fn help() {
let dir = TestDir::temp();
let out = succeed(bkt(dir.path("cache")).arg("--help"));
assert!(out.contains("bkt [OPTIONS] --ttl <DURATION> -- <COMMAND>..."), "Was:\n---\n{}\n---", out);
}
#[test]
fn cached() {
let dir = TestDir::temp();
let file = dir.path("file");
let args = ["--", "bash", "-c", COUNT_INVOCATIONS, "arg0", file.to_str().unwrap()];
let first_result = run(bkt(dir.path("cache")).args(args));
for _ in 1..3 {
let subsequent_result = run(bkt(dir.path("cache")).args(args));
if cfg!(feature="debug") {
assert_eq!(first_result.status, subsequent_result.status);
assert_eq!(first_result.out, subsequent_result.out);
} else {
assert_eq!(first_result, subsequent_result);
}
}
}
#[test]
fn cache_expires() {
let dir = TestDir::temp();
let file = dir.path("file");
let args = ["--", "bash", "-c", COUNT_INVOCATIONS, "arg0", file.to_str().unwrap()];
let first_result = succeed(bkt(dir.path("cache")).arg("--ttl=1m").args(args));
assert_eq!(first_result, "1");
make_dir_stale(dir.path("cache"), Duration::from_secs(10)).unwrap();
let subsequent_result = succeed(bkt(dir.path("cache")).arg("--ttl=1m").args(args));
assert_eq!(first_result, subsequent_result);
make_dir_stale(dir.path("cache"), Duration::from_secs(120)).unwrap();
let after_stale_result = succeed(bkt(dir.path("cache")).arg("--ttl=1m").args(args));
assert_eq!(after_stale_result, "2");
make_dir_stale(dir.path("cache"), Duration::from_secs(10)).unwrap();
let env_result = succeed(bkt(dir.path("cache")).env("BKT_TTL", "5s").args(args));
assert_eq!(env_result, "3");
}
#[test]
fn cache_expires_separately() {
let dir = TestDir::temp();
let file1 = dir.path("file1");
let file2 = dir.path("file2");
let args1 = ["--ttl=10s", "--", "bash", "-c", COUNT_INVOCATIONS, "arg0", file1.to_str().unwrap()];
let args2 = ["--ttl=20s", "--", "bash", "-c", COUNT_INVOCATIONS, "arg0", file2.to_str().unwrap()];
assert_eq!(succeed(bkt(dir.path("cache")).args(args1)), "1");
assert_eq!(succeed(bkt(dir.path("cache")).args(args2)), "1");
assert_eq!(succeed(bkt(dir.path("cache")).args(args1)), "1");
assert_eq!(succeed(bkt(dir.path("cache")).args(args2)), "1");
make_dir_stale(dir.path("cache"), Duration::from_secs(15)).unwrap();
assert_eq!(succeed(bkt(dir.path("cache")).args(args1)), "2");
assert_eq!(succeed(bkt(dir.path("cache")).args(args2)), "1");
}
#[test]
fn cache_hits_with_different_settings() {
let dir = TestDir::temp();
let file = dir.path("file");
let args1 = ["--ttl=10s", "--", "bash", "-c", COUNT_INVOCATIONS, "arg0", file.to_str().unwrap()];
let args2 = ["--ttl=20s", "--", "bash", "-c", COUNT_INVOCATIONS, "arg0", file.to_str().unwrap()];
assert_eq!(succeed(bkt(dir.path("cache")).args(args1)), "1");
assert_eq!(succeed(bkt(dir.path("cache")).args(args2)), "1");
make_dir_stale(dir.path("cache"), Duration::from_secs(15)).unwrap();
assert_eq!(succeed(bkt(dir.path("cache")).args(args2)), "1");
make_dir_stale(dir.path("cache"), Duration::from_secs(60)).unwrap(); succeed(bkt(dir.path("cache")).args(["--", "bash", "-c", "sleep 1"])); assert_eq!(succeed(bkt(dir.path("cache")).args(args1)), "2");
}
#[test]
fn cache_refreshes_in_background() {
let dir = TestDir::temp();
let file = dir.path("file");
let args = ["--stale=10s", "--ttl=20s", "--", "bash", "-c", COUNT_INVOCATIONS, "arg0", file.to_str().unwrap()];
assert_eq!(succeed(bkt(dir.path("cache")).args(args)), "1");
make_dir_stale(dir.path("cache"), Duration::from_secs(15)).unwrap();
assert_eq!(succeed(bkt(dir.path("cache")).args(args)), "1");
wait_for_contents_to_change(&file, ".");
assert_eq!(std::fs::read_to_string(&file).unwrap(), "..");
assert_eq!(succeed(bkt(dir.path("cache")).args(args)), "2");
}
#[test]
fn discard_failures() {
let dir = TestDir::temp();
let file = dir.path("file");
let cmd = format!("{} false;", COUNT_INVOCATIONS);
let args = ["--discard-failures", "--", "bash", "-c", &cmd, "arg0", file.to_str().unwrap()];
let result = run(bkt(dir.path("cache")).args(args));
assert_eq!(result.out, "1");
assert_eq!(result.status, Some(1));
let result = run(bkt(dir.path("cache")).args(args));
assert_eq!(result.out, "2");
assert_eq!(result.status, Some(1));
}
#[test]
fn discard_failure_cached_separately() {
let dir = TestDir::temp();
let allow_args = ["--", "bash", "-c", EXIT_WITH_ENV, "arg0"];
let discard_args = join(&["--discard-failures"], &allow_args);
let result1 = run(bkt(dir.path("cache")).args(allow_args).env("EXIT_WITH", "14"));
assert_eq!(result1.status, Some(14));
let result2 = run(bkt(dir.path("cache")).args(discard_args).env("EXIT_WITH", "0"));
assert_eq!(result2.status, Some(0));
}
#[test]
fn discard_failures_in_background() {
let dir = TestDir::temp();
let file = dir.path("file");
let cmd = format!("{} ! \"${{FAIL:-false}}\";", COUNT_INVOCATIONS);
let args = ["--ttl=20s", "--discard-failures", "--", "bash", "-c", &cmd, "arg0", file.to_str().unwrap()];
let stale_args = join(&["--stale=10s"], &args);
assert_eq!(succeed(bkt(dir.path("cache")).args(args)), "1");
std::env::set_var("FAIL", "true");
make_dir_stale(dir.path("cache"), Duration::from_secs(15)).unwrap();
assert_eq!(succeed(bkt(dir.path("cache")).args(&stale_args)), "1");
wait_for_contents_to_change(&file, ".");
assert_eq!(std::fs::read_to_string(&file).unwrap(), "..");
assert_eq!(succeed(bkt(dir.path("cache")).args(args)), "1");
}
#[cfg(unix)]
#[test]
fn cache_dirs_multi_user() {
let dir = TestDir::temp();
let file = dir.path("file");
let args = ["--", "bash", "-c", COUNT_INVOCATIONS, "arg0", file.to_str().unwrap()];
if unsafe { libc::geteuid() } == 0 {
eprint!("Running tests as root already, skipping");
return;
}
let mut sudo_bkt = sudo(bkt(dir.path("cache")).arg("--version"));
if run(&mut sudo_bkt).status.unwrap_or(127) != 0 {
eprint!("Couldn't run `sudo bkt`, skipping");
return;
}
let user_call = succeed(bkt(dir.path("cache")).args(args));
assert_eq!(user_call, "1");
let sudo_call = succeed(&mut sudo(bkt(dir.path("cache")).args(args)));
assert_eq!(sudo_call, "2");
assert_eq!(user_call, succeed(bkt(dir.path("cache")).args(args)));
assert_eq!(sudo_call, succeed(&mut sudo(bkt(dir.path("cache")).args(args))));
}
#[test]
fn respects_cache_dir() {
let dir = TestDir::temp();
let file = dir.path("file");
let args = ["--", "bash", "-c", COUNT_INVOCATIONS, "arg0", file.to_str().unwrap()];
let first_call = succeed(bkt(dir.path("cache")).arg(format!("--cache-dir={}", dir.path("cache").display())).args(args));
assert_eq!(first_call, "1");
assert_eq!(first_call, succeed(bkt(dir.path("cache")).arg(format!("--cache-dir={}", dir.path("cache").display())).args(args)));
let diff_cache = succeed(bkt(dir.path("cache")).arg(format!("--cache-dir={}", dir.path("new-cache").display())).args(args));
assert_eq!(diff_cache, "2");
let env_cache = succeed(bkt(dir.path("cache")).env("BKT_CACHE_DIR", dir.path("env-cache").as_os_str()).args(args));
assert_eq!(env_cache, "3");
}
#[test]
fn respects_relative_cache() {
let dir = TestDir::temp();
let cwd = dir.path("cwd");
std::fs::create_dir(&cwd).unwrap();
let file = dir.path("file");
let args = ["--", "bash", "-c", COUNT_INVOCATIONS, "arg0", file.to_str().unwrap()];
let first_call = succeed(bkt(dir.path("unused")).arg("--cache-dir=cache").args(args).current_dir(&cwd));
assert_eq!(first_call, "1");
assert_eq!(first_call, succeed(bkt(dir.path("unused")).arg("--cache-dir=cache").args(args).current_dir(&cwd)));
}
#[test]
fn respects_cache_scope() {
let dir = TestDir::temp();
let file = dir.path("file");
let args = ["--", "bash", "-c", COUNT_INVOCATIONS, "arg0", file.to_str().unwrap()];
let first_call = succeed(bkt(dir.path("cache")).args(args));
assert_eq!(first_call, "1");
assert_eq!(first_call, succeed(bkt(dir.path("cache")).args(args)));
let diff_scope = succeed(bkt(dir.path("cache"))
.arg("--scope=foo").args(args));
assert_eq!(diff_scope, "2");
assert_eq!(diff_scope, succeed(bkt(dir.path("cache"))
.arg("--scope=foo").args(args)));
assert_eq!(diff_scope, succeed(bkt(dir.path("cache"))
.env("BKT_SCOPE", "foo").args(args)));
}
#[test]
fn respects_args() {
let dir = TestDir::temp();
let file = dir.path("file");
let args = ["--", "bash", "-c", COUNT_INVOCATIONS, "arg0", file.to_str().unwrap()];
let first_call = succeed(bkt(dir.path("cache")).args(args));
assert_eq!(first_call, "1");
assert_eq!(first_call, succeed(bkt(dir.path("cache")).args(args)));
let diff_args = succeed(bkt(dir.path("cache")).args(args).arg("A B"));
assert_eq!(diff_args, "2");
let split_args = succeed(bkt(dir.path("cache")).args(args).args(["A", "B"]));
assert_eq!(split_args, "3");
}
#[test]
fn respects_cwd() {
let dir = TestDir::temp()
.create("dir1", FileType::Dir)
.create("dir2", FileType::Dir);
let args = ["--", "bash", "-c", "pwd"];
let cwd_args = join(&["--cwd"], &args);
let without_cwd_dir1 = succeed(bkt(dir.path("cache")).args(args).current_dir(dir.path("dir1")));
let without_cwd_dir2 = succeed(bkt(dir.path("cache")).args(args).current_dir(dir.path("dir2")));
assert!(without_cwd_dir1.trim().ends_with("/dir1"));
assert!(without_cwd_dir2.trim().ends_with("/dir1"));
let cwd_dir1 = succeed(bkt(dir.path("cache")).args(&cwd_args).current_dir(dir.path("dir1")));
let cwd_dir2 = succeed(bkt(dir.path("cache")).args(&cwd_args).current_dir(dir.path("dir2")));
assert!(cwd_dir1.trim().ends_with("/dir1"));
assert!(cwd_dir2.trim().ends_with("/dir2"));
}
#[test]
#[cfg(not(feature = "debug"))] fn respects_env() {
let dir = TestDir::temp();
let args = ["--", "bash", "-c", r#"printf 'foo:%s bar:%s baz:%s' "$FOO" "$BAR" "$BAZ""#];
let env_args = join(&["--env=FOO", "--env=BAR"], &args);
let without_env = succeed(bkt(dir.path("cache")).args(args)
.env("FOO", "1").env("BAR", "1").env("BAZ", "1"));
assert_eq!(without_env, succeed(bkt(dir.path("cache")).args(args)));
assert_eq!(without_env, succeed(bkt(dir.path("cache")).args(&env_args)));
let env = succeed(bkt(dir.path("cache")).args(&env_args)
.env("FOO", "2").env("BAR", "2").env("BAZ", "2"));
assert_eq!(env, "foo:2 bar:2 baz:2");
let env = succeed(bkt(dir.path("cache")).args(&env_args)
.env("FOO", "3").env("BAR", "2").env("BAZ", "3"));
assert_eq!(env, "foo:3 bar:2 baz:3");
let env = succeed(bkt(dir.path("cache")).args(&env_args)
.env("FOO", "4").env("BAR", "4").env("BAZ", "4"));
assert_eq!(env, "foo:4 bar:4 baz:4");
let env = succeed(bkt(dir.path("cache")).args(&env_args)
.env("FOO", "2").env("BAR", "2").env("BAZ", "5"));
assert_eq!(env, "foo:2 bar:2 baz:2"); }
#[test]
fn respects_modtime() {
let dir = TestDir::temp();
let file = dir.path("file");
let watch_file = dir.path("watch");
let args = ["--modtime", watch_file.to_str().unwrap(), "--", "bash", "-c", COUNT_INVOCATIONS, "arg0", file.to_str().unwrap()];
let no_file_result = succeed(bkt(dir.path("cache")).args(args));
assert_eq!(no_file_result, "1");
assert_eq!(no_file_result, succeed(bkt(dir.path("cache")).args(args)));
File::create(&watch_file).unwrap();
let new_file_result = succeed(bkt(dir.path("cache")).args(args));
assert_eq!(new_file_result, "2");
assert_eq!(new_file_result, succeed(bkt(dir.path("cache")).args(args)));
make_file_stale(&watch_file, Duration::from_secs(10)).unwrap();
let old_file_result = succeed(bkt(dir.path("cache")).args(args));
assert_eq!(old_file_result, "3");
assert_eq!(old_file_result, succeed(bkt(dir.path("cache")).args(args)));
}
#[test]
fn streaming() {
let dir = TestDir::temp();
let file = dir.path("file");
let script = r#"echo BEFORE; for (( i=0; i<50; i++ )); do if [[ -e "$1" ]]; then echo AFTER; exit 0; fi; sleep .1; done; exit 10"#;
let args = ["--", "bash", "-c", script, "arg0", file.to_str().unwrap()];
let mut proc = bkt(dir.path("cache")).args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped()).spawn().unwrap();
let mut buf = [0; 64];
let mut stdout = proc.stdout.take().unwrap();
let len = stdout.read(&mut buf).unwrap();
assert_eq!("BEFORE\n".as_bytes(), &buf[0..len], "len:{} - {:?}", len, buf);
assert_eq!(proc.try_wait().unwrap(), None);
File::create(&file).unwrap(); let len = stdout.read(&mut buf).unwrap();
assert_eq!("AFTER\n".as_bytes(), &buf[0..len], "len:{} - {:?}", len, buf);
if !cfg!(feature="debug") {
let mut buf = String::new();
assert_eq!(proc.stderr.as_mut().unwrap().read_to_string(&mut buf).unwrap(), 0, "{}", buf);
assert_eq!(buf, "");
}
assert_eq!(proc.wait().unwrap().code(), Some(0));
std::fs::remove_file(&file).unwrap();
assert_eq!(succeed(bkt(dir.path("cache")).args(args)), "BEFORE\nAFTER\n");
}
#[test]
fn large_output() {
let dir = TestDir::temp();
let bytes = 1024*100; let script = format!(r#"printf '.%.0s' {{1..{0}}}; printf '.%.0s' {{1..{0}}} >&2"#, bytes);
let args = ["--", "bash", "-c", &script, "arg0"];
let result = run(bkt(dir.path("cache")).args(args));
assert_eq!(result.out.len(), bytes);
if !cfg!(feature="debug") {
assert_eq!(result.err.len(), bytes);
}
assert_eq!(result.status, Some(0));
}
#[test]
fn truncated_output() {
let dir = TestDir::temp();
let bytes = 1024*100; let script = format!(r#"printf '.%.0s' {{1..{0}}}"#, bytes);
let args = ["--", "bash", "-c", &script, "arg0"];
let mut cmd = bkt(dir.path("cache"));
let cmd = cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped());
let mut child = cmd.spawn().unwrap();
let mut buf = [0; 10];
child.stdout.as_mut().unwrap().read_exact(&mut buf).unwrap();
assert_eq!(buf, [b'.'; 10]);
std::mem::drop(child.stdout.take().unwrap());
let result: CmdResult = child.wait_with_output().unwrap().into();
assert_eq!(result.out, "");
if !cfg!(feature="debug") { assert_eq!(result.err, ""); }
assert_eq!(result.status, Some(0));
}
#[test]
#[cfg(not(feature="debug"))]
fn no_debug_output() {
let dir = TestDir::temp();
let args = ["--", "bash", "-c", "true"];
assert_eq!(run(bkt(dir.path("cache")).args(args)),
CmdResult { out: "".into(), err: "".into(), status: Some(0) });
assert_eq!(run(bkt(dir.path("cache")).args(args)),
CmdResult { out: "".into(), err: "".into(), status: Some(0) });
}
#[test]
#[cfg(feature="debug")]
fn debug_output() {
fn starts_with_bkt(s: &str) -> bool { s.lines().all(|l| l.starts_with("bkt: ")) }
let miss_debug_re = regex::Regex::new(
"bkt: state: \nbkt: lookup .* not found\nbkt: cleanup data .*\nbkt: cleanup keys .*\nbkt: store data .*\nbkt: store key .*\n").unwrap();
let hit_debug_re = regex::Regex::new("bkt: lookup .* found\n").unwrap();
let dir = TestDir::temp();
let args = ["--", "bash", "-c", PRINT_ARGS, "arg0"];
let miss = run(bkt(dir.path("cache")).args(args));
assert!(starts_with_bkt(&miss.err), "{}", miss.err);
assert!(miss_debug_re.is_match(&miss.err), "{}", miss.err);
let hit = run(bkt(dir.path("cache")).args(args));
assert!(starts_with_bkt(&hit.err), "{}", hit.err);
assert!(hit_debug_re.is_match(&hit.err), "{}", hit.err);
}
#[test]
fn output_preserved() {
let dir = TestDir::temp();
fn same_output(dir: &TestDir, args: &[&str]) {
let bkt_args = ["--", "bash", "-c", PRINT_ARGS, "arg0"];
assert_eq!(
succeed(bkt(dir.path("cache")).args(bkt_args).args(args)),
succeed(bkt(dir.path("cache")).args(bkt_args).args(args)));
}
same_output(&dir, &[]);
same_output(&dir, &[""]);
same_output(&dir, &["a", "b"]);
same_output(&dir, &["a b"]);
same_output(&dir, &["a b", "c"]);
}
#[test]
#[cfg(not(feature="debug"))]
fn sensitive_output() {
let dir = TestDir::temp();
let args = ["--", "bash", "-c", r"printf 'foo\0bar'; printf 'bar\0baz\n' >&2"];
let output = run(bkt(dir.path("cache")).args(args));
assert_eq!(output,
CmdResult { out: "foo\u{0}bar".into(), err: "bar\u{0}baz\n".into(), status: Some(0) });
assert_eq!(run(bkt(dir.path("cache")).args(args)), output);
}
#[test]
fn exit_code_preserved() {
let dir = TestDir::temp();
let args = ["--", "bash", "-c", EXIT_WITH, "arg0"];
assert_eq!(run(bkt(dir.path("cache")).args(args).arg("14")).status, Some(14));
assert_eq!(run(bkt(dir.path("cache")).args(args).arg("14")).status, Some(14));
}
#[test]
fn warm() {
let dir = TestDir::temp();
let await_file = dir.path("await");
let touch_file = dir.path("touch");
let args = ["--", "bash", "-c", AWAIT_AND_TOUCH, "arg0",
await_file.to_str().unwrap(), touch_file.to_str().unwrap()];
let warm_args = join(&["--warm"], &args);
let output = succeed(bkt(dir.path("cache")).args(warm_args));
assert_eq!(output, "");
assert!(!touch_file.exists());
File::create(&await_file).unwrap(); for _ in 0..10 {
if touch_file.exists() { break; }
std::thread::sleep(Duration::from_millis(200));
}
assert!(touch_file.exists());
std::fs::remove_file(&await_file).unwrap(); let output = succeed(bkt(dir.path("cache")).args(args));
assert_eq!(output, "awaiting\n");
}
#[test]
fn force() {
let dir = TestDir::temp();
let file = dir.path("file");
let args = ["--", "bash", "-c", COUNT_INVOCATIONS, "arg0", file.to_str().unwrap()];
let args_force = join(&["--force"], &args);
let output = succeed(bkt(dir.path("cache")).args(args));
assert_eq!(output, "1");
let output = succeed(bkt(dir.path("cache")).args(args));
assert_eq!(output, "1");
let output = succeed(bkt(dir.path("cache")).args(args_force));
assert_eq!(output, "2");
let output = succeed(bkt(dir.path("cache")).args(args));
assert_eq!(output, "2");
}
#[test]
fn concurrent_call_race() {
let dir = TestDir::temp();
let file = dir.path("file");
let slow_count_invocations = format!(r#"sleep "0.5$RANDOM"; {}"#, COUNT_INVOCATIONS);
let args = ["--", "bash", "-c", &slow_count_invocations, "arg0", file.to_str().unwrap()];
println!("{:?}", args);
let proc1 = bkt(dir.path("cache")).args(args).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap();
let proc2 = bkt(dir.path("cache")).args(args).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap();
let result1: CmdResult = proc1.wait_with_output().unwrap().into();
if !cfg!(feature="debug") { assert_eq!(result1.err, ""); }
assert_eq!(result1.status, Some(0));
let result2: CmdResult = proc2.wait_with_output().unwrap().into();
if !cfg!(feature="debug") { assert_eq!(result2.err, ""); }
assert_eq!(result2.status, Some(0));
assert_eq!(std::fs::read_to_string(&file).unwrap(), "..");
assert!(result1.out == "2" || result2.out == "2"); }
}