1 mod env;
2 
3 use std::{ffi::OsStr, thread, time::Duration, time::Instant};
4 
5 use xshell::{
6     cmd, cp, cwd, mkdir_p, mktemp_d, pushd, pushenv, read_dir, read_file, rm_rf, write_file,
7 };
8 
9 #[test]
smoke()10 fn smoke() {
11     setup();
12 
13     let pwd = "lol";
14     let cmd = cmd!("echo 'hello '{pwd}");
15     println!("{}", cmd);
16 }
17 
18 #[test]
multiline()19 fn multiline() {
20     setup();
21 
22     let output = cmd!(
23         "
24         echo hello
25         "
26     )
27     .read()
28     .unwrap();
29     assert_eq!(output, "hello");
30 }
31 
32 #[test]
interpolation()33 fn interpolation() {
34     setup();
35 
36     let hello = "hello";
37     let output = cmd!("echo {hello}").read().unwrap();
38     assert_eq!(output, "hello");
39 }
40 
41 #[test]
program_interpolation()42 fn program_interpolation() {
43     setup();
44 
45     let echo = "echo";
46     let output = cmd!("{echo} hello").read().unwrap();
47     assert_eq!(output, "hello");
48 }
49 
50 #[test]
interpolation_concatenation()51 fn interpolation_concatenation() {
52     setup();
53 
54     let hello = "hello";
55     let world = "world";
56     let output = cmd!("echo {hello}-{world}").read().unwrap();
57     assert_eq!(output, "hello-world")
58 }
59 
60 #[test]
interpolation_move()61 fn interpolation_move() {
62     setup();
63 
64     let hello = "hello".to_string();
65     let output1 = cmd!("echo {hello}").read().unwrap();
66     let output2 = cmd!("echo {hello}").read().unwrap();
67     assert_eq!(output1, output2)
68 }
69 
70 #[test]
interpolation_spat()71 fn interpolation_spat() {
72     setup();
73 
74     let a = &["hello", "world"];
75     let b: &[&OsStr] = &[];
76     let c = &["!".to_string()];
77     let output = cmd!("echo {a...} {b...} {c...}").read().unwrap();
78     assert_eq!(output, "hello world !")
79 }
80 
81 #[test]
splat_idiom()82 fn splat_idiom() {
83     setup();
84 
85     let check = if true { &["--", "--check"][..] } else { &[] };
86     let cmd = cmd!("cargo fmt {check...}");
87     assert_eq!(cmd.to_string(), "cargo fmt -- --check");
88 
89     let dry_run = if true { Some("--dry-run") } else { None };
90     let cmd = cmd!("cargo publish {dry_run...}");
91     assert_eq!(cmd.to_string(), "cargo publish --dry-run");
92 }
93 
94 #[test]
exit_status()95 fn exit_status() {
96     setup();
97 
98     let err = cmd!("false").read().unwrap_err();
99     assert!(err.to_string().starts_with("command `false` failed"));
100 }
101 
102 #[test]
ignore_status()103 fn ignore_status() {
104     setup();
105 
106     let output = cmd!("false").ignore_status().read().unwrap();
107     assert_eq!(output, "");
108 }
109 
110 #[test]
read_stderr()111 fn read_stderr() {
112     setup();
113 
114     let output = cmd!("git fail").ignore_status().read_stderr().unwrap();
115     assert!(output.contains("fail"));
116 }
117 
118 #[test]
unknown_command()119 fn unknown_command() {
120     setup();
121 
122     let err = cmd!("nope no way").read().unwrap_err();
123     assert_eq!(err.to_string(), "command not found: `nope`");
124 }
125 
126 #[test]
args_with_spaces()127 fn args_with_spaces() {
128     setup();
129 
130     let hello_world = "hello world";
131     let cmd = cmd!("echo {hello_world} 'hello world' hello world");
132     assert_eq!(cmd.to_string(), r#"echo "hello world" "hello world" hello world"#)
133 }
134 
135 #[test]
escape()136 fn escape() {
137     setup();
138 
139     let output = cmd!("echo \\hello\\ '\\world\\'").read().unwrap();
140     assert_eq!(output, r#"\hello\ \world\"#)
141 }
142 
143 #[test]
stdin_redirection()144 fn stdin_redirection() {
145     setup();
146 
147     let lines = "\
148 foo
149 baz
150 bar
151 ";
152     let output = cmd!("sort").stdin(lines).read().unwrap().replace("\r\n", "\n");
153     assert_eq!(
154         output,
155         "\
156 bar
157 baz
158 foo"
159     )
160 }
161 
162 #[test]
test_pushd()163 fn test_pushd() {
164     setup();
165 
166     let d1 = cwd().unwrap();
167     {
168         let _p = pushd("xshell-macros").unwrap();
169         let d2 = cwd().unwrap();
170         assert_eq!(d2, d1.join("xshell-macros"));
171         {
172             let _p = pushd("src").unwrap();
173             let d3 = cwd().unwrap();
174             assert_eq!(d3, d1.join("xshell-macros/src"));
175         }
176         let d4 = cwd().unwrap();
177         assert_eq!(d4, d1.join("xshell-macros"));
178     }
179     let d5 = cwd().unwrap();
180     assert_eq!(d5, d1);
181 }
182 
183 #[test]
pushd_parent_dir()184 fn pushd_parent_dir() {
185     setup();
186 
187     let current = cwd().unwrap();
188     let dirname = current.file_name().unwrap();
189     let _d = pushd("..").unwrap();
190     let _d = pushd(dirname).unwrap();
191     assert_eq!(cwd().unwrap(), current);
192 }
193 
194 #[test]
test_pushd_lock()195 fn test_pushd_lock() {
196     setup();
197 
198     let t1 = thread::spawn(|| {
199         let _p = pushd("cbench").unwrap();
200         sleep_ms(20);
201     });
202     sleep_ms(10);
203 
204     let t2 = thread::spawn(|| {
205         let _p = pushd("cbench").unwrap();
206         sleep_ms(30);
207     });
208 
209     t1.join().unwrap();
210     t2.join().unwrap();
211 }
212 
213 const VAR: &str = "SPICA";
214 
215 #[test]
test_pushenv()216 fn test_pushenv() {
217     setup();
218 
219     let e1 = std::env::var_os(VAR);
220     {
221         let _e = pushenv(VAR, "1");
222         let e2 = std::env::var_os(VAR);
223         assert_eq!(e2, Some("1".into()));
224         {
225             let _e = pushenv(VAR, "2");
226             let e3 = std::env::var_os(VAR);
227             assert_eq!(e3, Some("2".into()));
228         }
229         let e4 = std::env::var_os(VAR);
230         assert_eq!(e4, e2);
231     }
232     let e5 = std::env::var_os(VAR);
233     assert_eq!(e5, e1);
234 }
235 
236 #[test]
test_pushenv_lock()237 fn test_pushenv_lock() {
238     setup();
239 
240     let t1 = thread::spawn(|| {
241         let _e = pushenv(VAR, "hello");
242         sleep_ms(20);
243     });
244     sleep_ms(10);
245 
246     let t2 = thread::spawn(|| {
247         let _e = pushenv(VAR, "world");
248         sleep_ms(30);
249     });
250 
251     t1.join().unwrap();
252     t2.join().unwrap();
253 }
254 
255 #[test]
output_with_ignore()256 fn output_with_ignore() {
257     setup();
258 
259     let output = cmd!("echoboth 'hello world!'").ignore_stdout().output().unwrap();
260     assert_eq!(output.stderr, b"hello world!\n");
261     assert_eq!(output.stdout, b"");
262 
263     let output = cmd!("echoboth 'hello world!'").ignore_stderr().output().unwrap();
264     assert_eq!(output.stdout, b"hello world!\n");
265     assert_eq!(output.stderr, b"");
266 
267     let output = cmd!("echoboth 'hello world!'").ignore_stdout().ignore_stderr().output().unwrap();
268     assert_eq!(output.stdout, b"");
269     assert_eq!(output.stderr, b"");
270 }
271 
272 #[test]
test_read_with_ignore()273 fn test_read_with_ignore() {
274     setup();
275 
276     let stdout = cmd!("echo 'hello world'").ignore_stdout().read().unwrap();
277     assert!(stdout.is_empty());
278 
279     let stderr = cmd!("echo 'hello world'").ignore_stderr().read_stderr().unwrap();
280     assert!(stderr.is_empty());
281 
282     let stdout = cmd!("echoboth 'hello world!'").ignore_stderr().read().unwrap();
283     assert_eq!(stdout, "hello world!");
284 
285     let stderr = cmd!("echoboth 'hello world!'").ignore_stdout().read_stderr().unwrap();
286     assert_eq!(stderr, "hello world!");
287 }
288 
289 #[test]
test_cp()290 fn test_cp() {
291     setup();
292 
293     let path;
294     {
295         let tempdir = mktemp_d().unwrap();
296         path = tempdir.path().to_path_buf();
297         let foo = tempdir.path().join("foo.txt");
298         let bar = tempdir.path().join("bar.txt");
299         let dir = tempdir.path().join("dir");
300         write_file(&foo, "hello world").unwrap();
301         mkdir_p(&dir).unwrap();
302 
303         cp(&foo, &bar).unwrap();
304         assert_eq!(read_file(&bar).unwrap(), "hello world");
305 
306         cp(&foo, &dir).unwrap();
307         assert_eq!(read_file(&dir.join("foo.txt")).unwrap(), "hello world");
308         assert!(path.exists());
309     }
310     assert!(!path.exists());
311 }
312 
check_failure(code: &str, err_msg: &str)313 fn check_failure(code: &str, err_msg: &str) {
314     mkdir_p("./target/cf").unwrap();
315     let _p = pushd("./target/cf").unwrap();
316 
317     write_file(
318         "Cargo.toml",
319         r#"
320 [package]
321 name = "cftest"
322 version = "0.0.0"
323 edition = "2018"
324 [workspace]
325 
326 [lib]
327 path = "main.rs"
328 
329 [dependencies]
330 xshell = { path = "../../" }
331 "#,
332     )
333     .unwrap();
334 
335     let snip = format!(
336         "
337 use xshell::*;
338 pub fn f() {{
339     {};
340 }}
341 ",
342         code
343     );
344     write_file("main.rs", snip).unwrap();
345 
346     let stderr = cmd!("cargo build").ignore_status().read_stderr().unwrap();
347     assert!(
348         stderr.contains(err_msg),
349         "\n\nCompile fail fail!\n\nExpected:\n{}\n\nActual:\n{}\n",
350         err_msg,
351         stderr
352     );
353 }
354 
355 #[test]
write_makes_directory()356 fn write_makes_directory() {
357     setup();
358 
359     let tempdir = mktemp_d().unwrap();
360     let folder = tempdir.path().join("some/nested/folder/structure");
361     write_file(folder.join(".gitinclude"), "").unwrap();
362     assert!(folder.exists());
363 }
364 
365 #[test]
recovers_from_panics()366 fn recovers_from_panics() {
367     let tempdir = mktemp_d().unwrap();
368     let tempdir = tempdir.path().canonicalize().unwrap();
369 
370     let orig = cwd().unwrap();
371 
372     std::panic::catch_unwind(|| {
373         let _p = pushd(&tempdir).unwrap();
374         assert_eq!(cwd().unwrap(), tempdir);
375         std::panic::resume_unwind(Box::new(()));
376     })
377     .unwrap_err();
378 
379     assert_eq!(cwd().unwrap(), orig);
380     {
381         let _p = pushd(&tempdir).unwrap();
382         assert_eq!(cwd().unwrap(), tempdir);
383     }
384 }
385 
386 #[test]
test_compile_failures()387 fn test_compile_failures() {
388     setup();
389 
390     check_failure("cmd!(92)", "expected a plain string literal");
391     check_failure(r#"cmd!(r"raw")"#, "expected a plain string literal");
392 
393     check_failure(
394         r#"cmd!("{echo.as_str()}")"#,
395         "error: can only interpolate simple variables, got this expression instead: `echo.as_str()`",
396     );
397 
398     check_failure(
399         r#"cmd!("echo a{args...}")"#,
400         "error: can't combine splat with concatenation, add spaces around `{args...}`",
401     );
402     check_failure(
403         r#"cmd!("echo {args...}b")"#,
404         "error: can't combine splat with concatenation, add spaces around `{args...}`",
405     );
406     check_failure(
407         r#"cmd!("echo a{args...}b")"#,
408         "error: can't combine splat with concatenation, add spaces around `{args...}`",
409     );
410     check_failure(r#"cmd!("")"#, "error: command can't be empty");
411     check_failure(r#"cmd!("{cmd...}")"#, "error: can't splat program name");
412     check_failure(r#"cmd!("echo 'hello world")"#, "error: unclosed `'` in command");
413     check_failure(r#"cmd!("echo {hello world")"#, "error: unclosed `{` in command");
414 
415     check_failure(
416         r#"
417     let x = 92;
418     cmd!("make -j {x}")"#,
419         r#"cmd!("make -j {x}")"#,
420     );
421 
422     check_failure(
423         r#"
424     let dry_run: fn() -> Option<&'static str> = || None;
425     cmd!("make -j {dry_run...}")"#,
426         r#"cmd!("make -j {dry_run...}")"#,
427     );
428 }
429 
430 #[test]
fixed_cost_compile_times()431 fn fixed_cost_compile_times() {
432     setup();
433 
434     let _p = pushd("cbench").unwrap();
435     let baseline = compile_bench("baseline");
436     let _ducted = compile_bench("ducted");
437     let xshelled = compile_bench("xshelled");
438     let ratio = (xshelled.as_millis() as f64) / (baseline.as_millis() as f64);
439     assert!(1.0 < ratio && ratio < 10.0)
440 }
441 
compile_bench(name: &str) -> Duration442 fn compile_bench(name: &str) -> Duration {
443     let _p = pushd(name).unwrap();
444     cmd!("cargo build -q").read().unwrap();
445 
446     let n = 5;
447     let mut times = Vec::new();
448     for _ in 0..n {
449         rm_rf("./target").unwrap();
450         let start = Instant::now();
451         cmd!("cargo build -q").read().unwrap();
452         let elapsed = start.elapsed();
453         times.push(elapsed);
454     }
455 
456     times.sort();
457     times.remove(0);
458     times.pop();
459     let total = times.iter().sum::<Duration>();
460     let average = total / (times.len() as u32);
461 
462     eprintln!("compiling {}: {:?}", name, average);
463 
464     total
465 }
466 
467 #[test]
versions_match()468 fn versions_match() {
469     setup();
470 
471     let read_version = |path: &str| {
472         let text = read_file(path).unwrap();
473         let vers = text.lines().find(|it| it.starts_with("version =")).unwrap();
474         let vers = vers.splitn(2, '#').next().unwrap();
475         vers.trim_start_matches("version =").trim().trim_matches('"').to_string()
476     };
477 
478     let v1 = read_version("./Cargo.toml");
479     let v2 = read_version("./xshell-macros/Cargo.toml");
480     assert_eq!(v1, v2);
481 
482     let cargo_toml = read_file("./Cargo.toml").unwrap();
483     let dep = format!("xshell-macros = {{ version = \"={}\",", v1);
484     assert!(cargo_toml.contains(&dep));
485 }
486 
487 #[test]
formatting()488 fn formatting() {
489     setup();
490 
491     cmd!("cargo fmt --all -- --check").run().unwrap()
492 }
493 
494 #[test]
string_escapes()495 fn string_escapes() {
496     setup();
497 
498     assert_eq!(cmd!("\"hello\"").to_string(), "\"hello\"");
499     assert_eq!(cmd!("\"\"\"asdf\"\"\"").to_string(), r##""""asdf""""##);
500     assert_eq!(cmd!("\\\\").to_string(), r#"\\"#);
501 }
502 
503 #[test]
504 fn cd_tempdir() {
505     println!("cwd: {}", cwd().unwrap().display());
506     let tmp = mktemp_d().unwrap();
507     println!("tmp: {}", tmp.path().display());
508     // Enter directory in child block so pushd guard is dropped before tmp
509     {
510         let _cwd = pushd(tmp.path()).unwrap();
511         println!("cwd: {}", cwd().unwrap().display());
512     }
513 }
514 
515 #[test]
516 fn cd_tempdir_no_block() {
517     let tmp = mktemp_d().unwrap();
518     let _cwd = pushd(tmp.path()).unwrap();
519 }
520 
521 #[test]
522 fn current_version_in_changelog() {
523     let _p = pushd(env!("CARGO_MANIFEST_DIR")).unwrap();
524     let changelog = read_file("CHANGELOG.md").unwrap();
525     let current_version_header = format!("## {}", env!("CARGO_PKG_VERSION"));
526     assert_eq!(changelog.lines().filter(|&line| line == current_version_header).count(), 1);
527 }
528 
529 fn sleep_ms(ms: u64) {
530     thread::sleep(std::time::Duration::from_millis(ms))
531 }
532 
533 fn setup() {
534     static ONCE: std::sync::Once = std::sync::Once::new();
535     ONCE.call_once(|| {
536         if let Err(err) = install_mock_binaries() {
537             panic!("failed to install binaries from mock_bin: {}", err)
538         }
539     });
540 
541     fn install_mock_binaries() -> xshell::Result<()> {
542         let mock_bin = cwd()?.join("./mock_bin");
543         let _d = pushd(&mock_bin);
544         for path in read_dir(".")? {
545             if path.extension().unwrap_or_default() == "rs" {
546                 cmd!("rustc {path}").run()?
547             }
548         }
549         let old_path = std::env::var("PATH").unwrap_or_default();
550         let new_path = {
551             let mut path = std::env::split_paths(&old_path).collect::<Vec<_>>();
552             path.insert(0, mock_bin);
553             std::env::join_paths(path).unwrap()
554         };
555         std::env::set_var("PATH", new_path);
556         Ok(())
557     }
558 }
559