blob: a54ff7c4273badba9fa78c265389e977fd41bd99 [file] [log] [blame]
mod env;
use std::{ffi::OsStr, thread, time::Duration, time::Instant};
use xshell::{
cmd, cp, cwd, mkdir_p, mktemp_d, pushd, pushenv, read_dir, read_file, rm_rf, write_file,
};
#[test]
fn smoke() {
setup();
let pwd = "lol";
let cmd = cmd!("echo 'hello '{pwd}");
println!("{}", cmd);
}
#[test]
fn multiline() {
setup();
let output = cmd!(
"
echo hello
"
)
.read()
.unwrap();
assert_eq!(output, "hello");
}
#[test]
fn interpolation() {
setup();
let hello = "hello";
let output = cmd!("echo {hello}").read().unwrap();
assert_eq!(output, "hello");
}
#[test]
fn program_interpolation() {
setup();
let echo = "echo";
let output = cmd!("{echo} hello").read().unwrap();
assert_eq!(output, "hello");
}
#[test]
fn interpolation_concatenation() {
setup();
let hello = "hello";
let world = "world";
let output = cmd!("echo {hello}-{world}").read().unwrap();
assert_eq!(output, "hello-world")
}
#[test]
fn interpolation_move() {
setup();
let hello = "hello".to_string();
let output1 = cmd!("echo {hello}").read().unwrap();
let output2 = cmd!("echo {hello}").read().unwrap();
assert_eq!(output1, output2)
}
#[test]
fn interpolation_spat() {
setup();
let a = &["hello", "world"];
let b: &[&OsStr] = &[];
let c = &["!".to_string()];
let output = cmd!("echo {a...} {b...} {c...}").read().unwrap();
assert_eq!(output, "hello world !")
}
#[test]
fn splat_idiom() {
setup();
let check = if true { &["--", "--check"][..] } else { &[] };
let cmd = cmd!("cargo fmt {check...}");
assert_eq!(cmd.to_string(), "cargo fmt -- --check");
let dry_run = if true { Some("--dry-run") } else { None };
let cmd = cmd!("cargo publish {dry_run...}");
assert_eq!(cmd.to_string(), "cargo publish --dry-run");
}
#[test]
fn exit_status() {
setup();
let err = cmd!("false").read().unwrap_err();
assert_eq!(err.to_string(), "command `false` failed, exit code: 1");
}
#[test]
fn ignore_status() {
setup();
let output = cmd!("false").ignore_status().read().unwrap();
assert_eq!(output, "");
}
#[test]
fn read_stderr() {
setup();
let output = cmd!("git fail").ignore_status().read_stderr().unwrap();
assert!(output.contains("fail"));
}
#[test]
fn unknown_command() {
setup();
let err = cmd!("nope no way").read().unwrap_err();
assert_eq!(err.to_string(), "command not found: `nope`");
}
#[test]
fn args_with_spaces() {
setup();
let hello_world = "hello world";
let cmd = cmd!("echo {hello_world} 'hello world' hello world");
assert_eq!(cmd.to_string(), r#"echo "hello world" "hello world" hello world"#)
}
#[test]
fn escape() {
setup();
let output = cmd!("echo \\hello\\ '\\world\\'").read().unwrap();
assert_eq!(output, r#"\hello\ \world\"#)
}
#[test]
fn stdin_redirection() {
setup();
let lines = "\
foo
baz
bar
";
let output = cmd!("sort").stdin(lines).read().unwrap().replace("\r\n", "\n");
assert_eq!(
output,
"\
bar
baz
foo"
)
}
#[test]
fn test_pushd() {
setup();
let d1 = cwd().unwrap();
{
let _p = pushd("xshell-macros").unwrap();
let d2 = cwd().unwrap();
assert_eq!(d2, d1.join("xshell-macros"));
{
let _p = pushd("src").unwrap();
let d3 = cwd().unwrap();
assert_eq!(d3, d1.join("xshell-macros/src"));
}
let d4 = cwd().unwrap();
assert_eq!(d4, d1.join("xshell-macros"));
}
let d5 = cwd().unwrap();
assert_eq!(d5, d1);
}
#[test]
fn pushd_parent_dir() {
setup();
let current = cwd().unwrap();
let dirname = current.file_name().unwrap();
let _d = pushd("..").unwrap();
let _d = pushd(dirname).unwrap();
assert_eq!(cwd().unwrap(), current);
}
#[test]
fn test_pushd_lock() {
setup();
let t1 = thread::spawn(|| {
let _p = pushd("cbench").unwrap();
sleep_ms(20);
});
sleep_ms(10);
let t2 = thread::spawn(|| {
let _p = pushd("cbench").unwrap();
sleep_ms(30);
});
t1.join().unwrap();
t2.join().unwrap();
}
const VAR: &str = "SPICA";
#[test]
fn test_pushenv() {
setup();
let e1 = std::env::var_os(VAR);
{
let _e = pushenv(VAR, "1");
let e2 = std::env::var_os(VAR);
assert_eq!(e2, Some("1".into()));
{
let _e = pushenv(VAR, "2");
let e3 = std::env::var_os(VAR);
assert_eq!(e3, Some("2".into()));
}
let e4 = std::env::var_os(VAR);
assert_eq!(e4, e2);
}
let e5 = std::env::var_os(VAR);
assert_eq!(e5, e1);
}
#[test]
fn test_pushenv_lock() {
setup();
let t1 = thread::spawn(|| {
let _e = pushenv(VAR, "hello");
sleep_ms(20);
});
sleep_ms(10);
let t2 = thread::spawn(|| {
let _e = pushenv(VAR, "world");
sleep_ms(30);
});
t1.join().unwrap();
t2.join().unwrap();
}
#[test]
fn output_with_ignore() {
setup();
let output = cmd!("echoboth 'hello world!'").ignore_stdout().output().unwrap();
assert_eq!(output.stderr, b"hello world!\n");
assert_eq!(output.stdout, b"");
let output = cmd!("echoboth 'hello world!'").ignore_stderr().output().unwrap();
assert_eq!(output.stdout, b"hello world!\n");
assert_eq!(output.stderr, b"");
let output = cmd!("echoboth 'hello world!'").ignore_stdout().ignore_stderr().output().unwrap();
assert_eq!(output.stdout, b"");
assert_eq!(output.stderr, b"");
}
#[test]
fn test_read_with_ignore() {
setup();
let stdout = cmd!("echo 'hello world'").ignore_stdout().read().unwrap();
assert!(stdout.is_empty());
let stderr = cmd!("echo 'hello world'").ignore_stderr().read_stderr().unwrap();
assert!(stderr.is_empty());
let stdout = cmd!("echoboth 'hello world!'").ignore_stderr().read().unwrap();
assert_eq!(stdout, "hello world!");
let stderr = cmd!("echoboth 'hello world!'").ignore_stdout().read_stderr().unwrap();
assert_eq!(stderr, "hello world!");
}
#[test]
fn test_cp() {
setup();
let path;
{
let tempdir = mktemp_d().unwrap();
path = tempdir.path().to_path_buf();
let foo = tempdir.path().join("foo.txt");
let bar = tempdir.path().join("bar.txt");
let dir = tempdir.path().join("dir");
write_file(&foo, "hello world").unwrap();
mkdir_p(&dir).unwrap();
cp(&foo, &bar).unwrap();
assert_eq!(read_file(&bar).unwrap(), "hello world");
cp(&foo, &dir).unwrap();
assert_eq!(read_file(&dir.join("foo.txt")).unwrap(), "hello world");
assert!(path.exists());
}
assert!(!path.exists());
}
fn check_failure(code: &str, err_msg: &str) {
mkdir_p("./target/cf").unwrap();
let _p = pushd("./target/cf").unwrap();
write_file(
"Cargo.toml",
r#"
[package]
name = "cftest"
version = "0.0.0"
edition = "2018"
[workspace]
[lib]
path = "main.rs"
[dependencies]
xshell = { path = "../../" }
"#,
)
.unwrap();
let snip = format!(
"
use xshell::*;
pub fn f() {{
{};
}}
",
code
);
write_file("main.rs", snip).unwrap();
let stderr = cmd!("cargo build").ignore_status().read_stderr().unwrap();
assert!(
stderr.contains(err_msg),
"\n\nCompile fail fail!\n\nExpected:\n{}\n\nActual:\n{}\n",
err_msg,
stderr
);
}
#[test]
fn write_makes_directory() {
setup();
let tempdir = mktemp_d().unwrap();
let folder = tempdir.path().join("some/nested/folder/structure");
write_file(folder.join(".gitinclude"), "").unwrap();
assert!(folder.exists());
}
#[test]
fn test_compile_failures() {
setup();
check_failure("cmd!(92)", "expected a plain string literal");
check_failure(r#"cmd!(r"raw")"#, "expected a plain string literal");
check_failure(
r#"cmd!("{echo.as_str()}")"#,
"error: can only interpolate simple variables, got this expression instead: `echo.as_str()`",
);
check_failure(
r#"cmd!("echo a{args...}")"#,
"error: can't combine splat with concatenation, add spaces around `{args...}`",
);
check_failure(
r#"cmd!("echo {args...}b")"#,
"error: can't combine splat with concatenation, add spaces around `{args...}`",
);
check_failure(
r#"cmd!("echo a{args...}b")"#,
"error: can't combine splat with concatenation, add spaces around `{args...}`",
);
check_failure(r#"cmd!("")"#, "error: command can't be empty");
check_failure(r#"cmd!("{cmd...}")"#, "error: can't splat program name");
check_failure(r#"cmd!("echo 'hello world")"#, "error: unclosed `'` in command");
check_failure(r#"cmd!("echo {hello world")"#, "error: unclosed `{` in command");
check_failure(
r#"
let x = 92;
cmd!("make -j {x}")"#,
r#"cmd!("make -j {x}")"#,
);
check_failure(
r#"
let dry_run: fn() -> Option<&'static str> = || None;
cmd!("make -j {dry_run...}")"#,
r#"cmd!("make -j {dry_run...}")"#,
);
}
#[test]
fn fixed_cost_compile_times() {
setup();
let _p = pushd("cbench").unwrap();
let baseline = compile_bench("baseline");
let _ducted = compile_bench("ducted");
let xshelled = compile_bench("xshelled");
let ratio = (xshelled.as_millis() as f64) / (baseline.as_millis() as f64);
assert!(1.0 < ratio && ratio < 10.0)
}
fn compile_bench(name: &str) -> Duration {
let _p = pushd(name).unwrap();
cmd!("cargo build -q").read().unwrap();
let n = 5;
let mut times = Vec::new();
for _ in 0..n {
rm_rf("./target").unwrap();
let start = Instant::now();
cmd!("cargo build -q").read().unwrap();
let elapsed = start.elapsed();
times.push(elapsed);
}
times.sort();
times.remove(0);
times.pop();
let total = times.iter().sum::<Duration>();
let average = total / (times.len() as u32);
eprintln!("compiling {}: {:?}", name, average);
total
}
#[test]
fn versions_match() {
setup();
let read_version = |path: &str| {
let text = read_file(path).unwrap();
let vers = text.lines().find(|it| it.starts_with("version =")).unwrap();
let vers = vers.splitn(2, '#').next().unwrap();
vers.trim_start_matches("version =").trim().trim_matches('"').to_string()
};
let v1 = read_version("./Cargo.toml");
let v2 = read_version("./xshell-macros/Cargo.toml");
assert_eq!(v1, v2);
let cargo_toml = read_file("./Cargo.toml").unwrap();
let dep = format!("xshell-macros = {{ version = \"={}\",", v1);
assert!(cargo_toml.contains(&dep));
}
#[test]
fn formatting() {
setup();
cmd!("cargo fmt --all -- --check").run().unwrap()
}
#[test]
fn string_escapes() {
setup();
assert_eq!(cmd!("\"hello\"").to_string(), "\"hello\"");
assert_eq!(cmd!("\"\"\"asdf\"\"\"").to_string(), r##""""asdf""""##);
assert_eq!(cmd!("\\\\").to_string(), r#"\\"#);
}
fn sleep_ms(ms: u64) {
thread::sleep(std::time::Duration::from_millis(ms))
}
fn setup() {
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| {
if let Err(err) = install_mock_binaries() {
panic!("failed to install binaries from mock_bin: {}", err)
}
});
fn install_mock_binaries() -> xshell::Result<()> {
let mock_bin = cwd()?.join("./mock_bin");
let _d = pushd(&mock_bin);
for path in read_dir(".")? {
if path.extension().unwrap_or_default() == "rs" {
cmd!("rustc {path}").run()?
}
}
let old_path = std::env::var("PATH").unwrap_or_default();
let new_path = {
let mut path = std::env::split_paths(&old_path).collect::<Vec<_>>();
path.insert(0, mock_bin);
std::env::join_paths(path).unwrap()
};
std::env::set_var("PATH", new_path);
Ok(())
}
}