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