1pub mod exec;
2pub mod pipeline;
3pub use exec::*;
4pub use oxdock_parser::{
5 Arg, ArgType, CommandMeta, CommandSpec, StepKind, all_metadata, all_structural_metadata,
6 lower_command,
7};
8pub use oxdock_process::ProcessManager;
9
10define_pipeline! {
11 StepKind::Run(..) => exec::dispatch_run,
12 StepKind::RunExec { .. } => exec::dispatch_run_exec,
13 StepKind::AsyncBlock { .. } => exec::dispatch_async_block,
14 StepKind::Echo(..) => exec::dispatch_echo,
15 StepKind::Workdir(..) => exec::dispatch_workdir,
16 StepKind::Workspace(..) => exec::dispatch_workspace,
17 StepKind::Env { .. } => exec::dispatch_env,
18 StepKind::Copy { .. } => exec::dispatch_copy,
19 StepKind::CopyGit { .. } => exec::dispatch_copy_git,
20 StepKind::Symlink { .. } => exec::dispatch_symlink,
21 StepKind::Mkdir(..) => exec::dispatch_mkdir,
22 StepKind::Ls(..) => exec::dispatch_ls,
23 StepKind::Cwd => exec::dispatch_cwd,
24 StepKind::Read(..) => exec::dispatch_read,
25 StepKind::ReadLine { .. } => exec::dispatch_read_line,
26 StepKind::Write { .. } => exec::dispatch_write,
27 StepKind::Append { .. } => exec::dispatch_append,
28 StepKind::Expand { .. } => exec::dispatch_expand,
29 StepKind::AssertEq { .. } => exec::dispatch_assert_eq,
30 StepKind::AssertContains { .. } => exec::dispatch_assert_contains,
31 StepKind::HashSha256 { .. } => exec::dispatch_hash_sha256,
32 StepKind::Exit(..) => exec::dispatch_exit,
33 StepKind::AssignAsync { .. } => exec::dispatch_assign_async_step,
34 StepKind::Set { .. } => exec::dispatch_set,
35 StepKind::Await { .. } => exec::dispatch_await_step,
36 StepKind::AssignCapture { .. } => exec::dispatch_assign_capture_step,
37 StepKind::AwaitCapture { .. } => exec::dispatch_await_capture_step,
38 StepKind::Cancel { .. } => exec::dispatch_cancel_step,
39 StepKind::Timeout { .. } => exec::dispatch_timeout_step,
40 StepKind::Sleep { .. } => exec::dispatch_sleep_step,
41 StepKind::FuncDef { .. } => exec::dispatch_func_def,
42 StepKind::Call { .. } => exec::dispatch_call,
43 StepKind::Return { .. } => exec::dispatch_return,
44 StepKind::While { .. } => exec::dispatch_while_loop,
45 StepKind::Break => exec::dispatch_break,
46 StepKind::Continue => exec::dispatch_continue,
47}
48
49pub fn parse_script(input: &str) -> anyhow::Result<Vec<oxdock_parser::Step>> {
53 Ok(oxdock_parser::parse_script(input, lower_command)?)
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59 use indoc::indoc;
60 use oxdock_fs::{GuardedPath, GuardedTempDir, PathResolver};
61 use oxdock_parser::{Step, StepKind};
62 #[cfg(unix)]
63 use std::time::Instant;
64
65 fn guard_root(temp: &GuardedTempDir) -> GuardedPath {
66 temp.as_guarded_path().clone()
67 }
68
69 fn read_trimmed(path: &GuardedPath) -> String {
70 let resolver = PathResolver::new(path.root(), path.root()).unwrap();
71 resolver
72 .read_to_string(path)
73 .unwrap_or_default()
74 .trim()
75 .to_string()
76 }
77
78 fn create_dirs(path: &GuardedPath) {
79 let resolver = PathResolver::new(path.root(), path.root()).unwrap();
80 resolver.create_dir_all(path).unwrap();
81 }
82
83 fn exists(root: &GuardedPath, rel: &str) -> bool {
84 root.join(rel).map(|p| p.exists()).unwrap_or(false)
85 }
86
87 #[test]
88 fn run_isolates_cargo_target_dir_from_workspace() {
89 let temp = GuardedPath::tempdir().unwrap();
90 let root = guard_root(&temp);
91
92 #[allow(clippy::disallowed_macros)]
93 let cmd = if cfg!(windows) {
94 "echo %CARGO_TARGET_DIR% > seen.txt"
95 } else {
96 "printf %s \"$CARGO_TARGET_DIR\" > seen.txt"
97 };
98
99 let steps = vec![Step {
100 guard: None,
101 kind: StepKind::Run(cmd.to_string().into()),
102 scope_enter: 0,
103 scope_exit: 0,
104 }];
105
106 run_steps(&root, &steps).unwrap();
107
108 let seen = read_trimmed(&root.join("seen.txt").unwrap());
109 let legacy = root.join(".cargo-target").unwrap();
110
111 assert_ne!(
112 seen.trim(),
113 legacy.display().to_string(),
114 "CARGO_TARGET_DIR must not target the workspace tree"
115 );
116 assert!(
117 seen.trim().contains("oxdock-cargo-"),
118 "CARGO_TARGET_DIR must point at the isolated scratch location, got {seen:?}"
119 );
120 }
121
122 #[test]
123 fn lazy_snapshot_run_materializes_but_local_run_does_not() {
124 let temp = GuardedPath::tempdir().unwrap();
125 let root = guard_root(&temp);
126
127 let steps = parse_script("RUN echo hi").unwrap();
129 let output = run_steps_with_lazy_snapshot(&root, &steps, ExecIo::new()).unwrap();
130 assert!(
131 output.snapshot.is_materialized(),
132 "snapshot-rooted RUN must materialize the snapshot"
133 );
134
135 let steps = parse_script("WORKSPACE LOCAL\nRUN echo hi").unwrap();
137 let output = run_steps_with_lazy_snapshot(&root, &steps, ExecIo::new()).unwrap();
138 assert!(
139 !output.snapshot.is_materialized(),
140 "LOCAL-rooted RUN must never materialize the snapshot"
141 );
142 }
143
144 #[test]
145 fn lazy_empty_run_creates_nothing() {
146 let temp = GuardedPath::tempdir().unwrap();
147 let root = guard_root(&temp);
148
149 let output = run_steps_with_lazy_snapshot(&root, &[], ExecIo::new()).unwrap();
150 assert!(!output.snapshot.is_materialized());
151 assert!(output.snapshot.get().is_none());
152 }
153
154 #[test]
155 fn lazy_local_echo_assert_failure_never_materializes_snapshot() {
156 let temp = GuardedPath::tempdir().unwrap();
157 let root = guard_root(&temp);
158
159 let script = indoc!(
160 r#"
161 WORKSPACE LOCAL
162 ECHO playground pid is 41067
163 ASSERT_CONTAINS stdout "playground pid i!!"
164 "#
165 );
166 let steps = parse_script(script).unwrap();
167
168 let mut resolver = PathResolver::new_lazy(root.clone()).unwrap();
172 resolver.set_workspace_root(root.clone());
173 let snapshot = resolver.snapshot_handle();
174 let fs: Box<dyn oxdock_fs::WorkspaceFs> = Box::new(resolver);
175 let result = run_steps_with_manager(
176 fs,
177 &steps,
178 oxdock_process::default_process_manager(),
179 ExecIo::new(),
180 );
181 assert!(result.is_err(), "mismatched ASSERT_CONTAINS must fail");
182 let err = result.err().unwrap();
183 assert!(!snapshot.is_materialized());
184 assert!(snapshot.get().is_none());
185
186 let enriched = enrich_lazy_error(&snapshot, &root, err);
187 let msg = enriched.to_string();
188 assert!(
189 msg.contains("did not contain 'playground pid i!!'"),
190 "{msg}"
191 );
192 assert!(msg.contains("never materialized"), "{msg}");
193 assert!(!msg.contains("filesystem snapshot (root"), "{msg}");
194 }
195
196 #[test]
197 fn lazy_local_echo_assert_success_leaves_snapshot_pending() {
198 let temp = GuardedPath::tempdir().unwrap();
199 let root = guard_root(&temp);
200
201 let script = indoc!(
202 r#"
203 WORKSPACE LOCAL
204 ECHO playground pid is 41067
205 ASSERT_CONTAINS stdout "playground pid is 41067"
206 "#
207 );
208 let steps = parse_script(script).unwrap();
209
210 let output = run_steps_with_lazy_snapshot(&root, &steps, ExecIo::new()).unwrap();
211 assert!(!output.snapshot.is_materialized());
212 assert!(output.snapshot.get().is_none());
213 }
214
215 #[test]
216 fn lazy_default_mode_echo_assert_failure_keeps_snapshot_pending() {
217 let temp = GuardedPath::tempdir().unwrap();
218 let root = guard_root(&temp);
219
220 let script = indoc!(
224 r#"
225 ECHO playground pid is 41067
226 ASSERT_CONTAINS stdout "playground pid i!!"
227 "#
228 );
229 let steps = parse_script(script).unwrap();
230
231 let mut resolver = PathResolver::new_lazy(root.clone()).unwrap();
235 resolver.set_workspace_root(root.clone());
236 let snapshot = resolver.snapshot_handle();
237 let fs: Box<dyn oxdock_fs::WorkspaceFs> = Box::new(resolver);
238 let result = run_steps_with_manager(
239 fs,
240 &steps,
241 oxdock_process::default_process_manager(),
242 ExecIo::new(),
243 );
244 assert!(result.is_err(), "mismatched ASSERT_CONTAINS must fail");
245 let err = result.err().unwrap();
246 assert!(!snapshot.is_materialized());
247 assert!(snapshot.get().is_none());
248
249 let enriched = enrich_lazy_error(&snapshot, &root, err);
250 let msg = enriched.to_string();
251 assert!(
252 msg.contains("did not contain 'playground pid i!!'"),
253 "{msg}"
254 );
255 assert!(msg.contains("never materialized"), "{msg}");
256 assert!(!msg.contains("filesystem snapshot (root"), "{msg}");
257 }
258
259 #[test]
260 fn lazy_default_mode_write_materializes_snapshot() {
261 let temp = GuardedPath::tempdir().unwrap();
262 let root = guard_root(&temp);
263
264 let steps = parse_script("WRITE \"snap.txt\" \"snap\"").unwrap();
267
268 let output = run_steps_with_lazy_snapshot(&root, &steps, ExecIo::new()).unwrap();
269 assert!(output.snapshot.is_materialized());
270 let snapshot_root = output.snapshot.get().expect("snapshot must be published");
271 assert!(
272 exists(snapshot_root, "snap.txt"),
273 "WRITE must land inside the materialized snapshot"
274 );
275 }
276
277 #[test]
278 fn guard_skips_when_env_missing() {
279 let temp = GuardedPath::tempdir().unwrap();
280 let root = guard_root(&temp);
281
282 let guard_var = "OXDOCK_GUARD_TEST_TOKEN_UNSET";
283 let script = format!(
284 indoc!(
285 r#"
286 [env:{guard}] WRITE "skipped.txt" "hi"
287 WRITE "kept.txt" "ok"
288 "#
289 ),
290 guard = guard_var
291 );
292 let steps = crate::parse_script(&script).unwrap();
293
294 run_steps(&root, &steps).unwrap();
295
296 assert!(
297 !exists(&root, "skipped.txt"),
298 "guarded WRITE should be skipped"
299 );
300 assert!(exists(&root, "kept.txt"), "unguarded WRITE should run");
301 }
302
303 #[test]
304 fn guard_sees_env_set_by_env_step() {
305 let temp = GuardedPath::tempdir().unwrap();
306 let root = guard_root(&temp);
307
308 let script = indoc!(
309 r#"
310 ENV FOO="1"
311 [env:FOO] WRITE "hit.txt" "yes"
312 WRITE "always.txt" "ok"
313 "#
314 );
315 let steps = crate::parse_script(script).unwrap();
316
317 run_steps(&root, &steps).unwrap();
318
319 assert!(
320 exists(&root, "hit.txt"),
321 "guarded WRITE should run after ENV sets variable"
322 );
323 assert!(exists(&root, "always.txt"), "unguarded WRITE should run");
324 }
325
326 #[test]
327 fn echo_runs_and_allows_subsequent_steps() {
328 let temp = GuardedPath::tempdir().unwrap();
329 let root = guard_root(&temp);
330
331 let script = indoc!(
332 r#"
333 ECHO "Hello, world"
334 WRITE "always.txt" "ok"
335 "#
336 );
337 let steps = crate::parse_script(script).unwrap();
338
339 run_steps(&root, &steps).unwrap();
340
341 assert!(exists(&root, "always.txt"), "WRITE after ECHO should run");
342 }
343
344 #[test]
345 fn guard_on_previous_line_applies_to_next_command() {
346 let temp = GuardedPath::tempdir().unwrap();
347 let root = guard_root(&temp);
348
349 let script = indoc!(
350 r#"
351 ENV FOO="1"
352 [env:FOO]
353 WRITE "hit.txt" "yes"
354 WRITE "always.txt" "ok"
355 "#
356 );
357 let steps = crate::parse_script(script).unwrap();
358
359 run_steps(&root, &steps).unwrap();
360
361 assert!(
362 exists(&root, "hit.txt"),
363 "guarded WRITE on next line should run"
364 );
365 assert!(exists(&root, "always.txt"), "unguarded WRITE should run");
366 }
367
368 #[test]
369 fn guard_respects_platform_negation() {
370 let temp = GuardedPath::tempdir().unwrap();
371 let root = guard_root(&temp);
372
373 let script = indoc!(
374 r#"
375 [not(unix)] WRITE "platform.txt" "hi"
376 WRITE "always.txt" "ok"
377 "#
378 );
379 let steps = crate::parse_script(script).unwrap();
380
381 run_steps(&root, &steps).unwrap();
382
383 #[allow(clippy::disallowed_macros)]
384 let expect_skipped = cfg!(unix);
385 assert_eq!(
386 exists(&root, "platform.txt"),
387 !expect_skipped,
388 "platform guard should skip on unix and run elsewhere"
389 );
390 assert!(exists(&root, "always.txt"), "unguarded WRITE should run");
391 }
392
393 #[test]
394 fn guard_block_env_scope_restores_after_exit() {
395 let temp = GuardedPath::tempdir().unwrap();
396 let root = guard_root(&temp);
397
398 let script = indoc!(
399 r#"
400 ENV RUN="1"
401 [env:RUN] {
402 ENV INNER="1"
403 WRITE "scoped.txt" "hit"
404 }
405 [env:INNER] WRITE "leak.txt" "nope"
406 "#
407 );
408 let steps = crate::parse_script(script).unwrap();
409
410 run_steps(&root, &steps).unwrap();
411
412 assert!(exists(&root, "scoped.txt"), "block should run");
413 assert!(
414 !exists(&root, "leak.txt"),
415 "env set inside block must not leak outward"
416 );
417 }
418
419 #[test]
420 fn guard_block_workdir_scope_restores_after_exit() {
421 let temp = GuardedPath::tempdir().unwrap();
422 let root = guard_root(&temp);
423
424 let script = indoc!(
425 r#"
426 MKDIR "nested"
427 ENV RUN="1"
428 [env:RUN] {
429 WORKDIR "nested"
430 WRITE "inside.txt" "ok"
431 }
432 WRITE "outside.txt" "root"
433 "#
434 );
435 let steps = crate::parse_script(script).unwrap();
436
437 run_steps(&root, &steps).unwrap();
438
439 assert!(
440 exists(&root, "nested/inside.txt"),
441 "inside write should land in nested dir"
442 );
443 assert!(
444 exists(&root, "outside.txt"),
445 "workdir should reset after block exits"
446 );
447 assert!(
448 !exists(&root, "nested/outside.txt"),
449 "writes after block should not stay scoped"
450 );
451 }
452
453 #[test]
454 fn workspace_scope_restores_after_guard_block() {
455 let snapshot = GuardedPath::tempdir().unwrap();
456 let local = GuardedPath::tempdir().unwrap();
457 let snapshot_root = guard_root(&snapshot);
458 let local_root = guard_root(&local);
459
460 let script = indoc!(
461 r#"
462 ENV RUN="1"
463 [env:RUN] {
464 WORKSPACE LOCAL
465 WRITE "local_only.txt" "inside"
466 }
467 WRITE "snapshot_only.txt" "outside"
468 "#
469 );
470 let steps = crate::parse_script(script).unwrap();
471
472 run_steps_with_context(&snapshot_root, &local_root, &steps).unwrap();
473
474 assert!(
475 local_root.join("local_only.txt").unwrap().exists(),
476 "workspace switch inside block should affect local root"
477 );
478 assert!(
479 snapshot_root.join("snapshot_only.txt").unwrap().exists(),
480 "writes after block must target snapshot again"
481 );
482 assert!(
483 !local_root.join("snapshot_only.txt").unwrap().exists(),
484 "workspace should reset after guard block exits"
485 );
486 }
487
488 #[test]
489 fn guard_matches_profile_env() {
490 let temp = GuardedPath::tempdir().unwrap();
492 let root = guard_root(&temp);
493
494 let profile = std::env::var("PROFILE").unwrap_or_else(|_| "debug".to_string());
495 let script = format!(
496 indoc!(
497 r#"
498 ENV PROFILE={0}
499 [eq(env:PROFILE, {0})] WRITE "hit.txt" "yes"
500 [ne(env:PROFILE, {0})] WRITE "miss.txt" "no"
501 "#
502 ),
503 profile
504 );
505
506 let steps = crate::parse_script(&script).unwrap();
507 run_steps(&root, &steps).unwrap();
508
509 assert!(
510 exists(&root, "hit.txt"),
511 "PROFILE-matching guard should run"
512 );
513 assert!(
514 !exists(&root, "miss.txt"),
515 "PROFILE inequality guard should skip for current profile"
516 );
517 }
518
519 #[test]
520 fn multiple_guards_all_must_pass() {
521 let temp = GuardedPath::tempdir().unwrap();
522 let root = guard_root(&temp);
523
524 let key = "OXDOCK_MULTI_GUARD_TEST_PASS";
525
526 let script = format!(
527 indoc!(
528 r#"
529 ENV {k}=ok
530 [env:{k},eq(env:{k}, ok)] WRITE "hit.txt" "yes"
531 WRITE "always.txt" "ok"
532 "#
533 ),
534 k = key
535 );
536 let steps = crate::parse_script(&script).unwrap();
537 run_steps(&root, &steps).unwrap();
538
539 assert!(
540 exists(&root, "hit.txt"),
541 "guarded step should run when all guards pass"
542 );
543 assert!(exists(&root, "always.txt"), "unguarded step should run");
544 }
545
546 #[test]
547 fn multiple_guards_skip_when_one_fails() {
548 let temp = GuardedPath::tempdir().unwrap();
549 let root = guard_root(&temp);
550
551 let key = "OXDOCK_MULTI_GUARD_TEST_FAIL";
552
553 let script = format!(
554 indoc!(
555 r#"
556 ENV {k}=ok
557 [env:{k},ne(env:{k}, ok)] WRITE "miss.txt" "yes"
558 WRITE "always.txt" "ok"
559 "#
560 ),
561 k = key
562 );
563 let steps = crate::parse_script(&script).unwrap();
564 run_steps(&root, &steps).unwrap();
565
566 assert!(
567 !exists(&root, "miss.txt"),
568 "guarded step should skip when any guard fails"
569 );
570 assert!(exists(&root, "always.txt"), "unguarded step should run");
571 }
572
573 #[cfg(unix)]
574 #[cfg_attr(
575 miri,
576 ignore = "stdout streaming not supported for background command under miri"
577 )]
578 #[test]
579 fn async_exits_success_and_stops_pipeline() {
580 let temp = GuardedPath::tempdir().unwrap();
581 let root = guard_root(&temp);
582
583 let script = "ASYNC RUN \"sh -c 'sleep 0.05'\"";
585 let steps = crate::parse_script(script).unwrap();
586 let res = run_steps(&root, &steps);
587 assert!(res.is_ok(), "ASYNC success should allow clean exit");
588 }
589
590 #[cfg(unix)]
591 #[cfg_attr(
592 miri,
593 ignore = "stdout streaming not supported for background command under miri"
594 )]
595 #[test]
596 fn async_failure_bubbles_status() {
597 let temp = GuardedPath::tempdir().unwrap();
598 let root = guard_root(&temp);
599
600 let script = "ASYNC RUN \"sh -c 'sleep 0.05; exit 7'\"";
601 let steps = crate::parse_script(script).unwrap();
602 let err = run_steps(&root, &steps).unwrap_err();
603 let msg = err.to_string();
604 assert!(
605 msg.contains("ASYNC process exited with status") || msg.contains("exit status: 7"),
606 "should surface failing ASYNC exit code"
607 );
608 }
609
610 #[cfg(unix)]
611 #[cfg_attr(
612 miri,
613 ignore = "timing-sensitive background process test is unreliable under Miri"
614 )]
615 #[test]
616 fn async_multiple_stops_on_first_exit_and_does_not_block_steps() {
617 let temp = GuardedPath::tempdir().unwrap();
618 let root = guard_root(&temp);
619
620 let script = indoc! {
621 r#"
622 ASYNC RUN "sh -c 'sleep 0.2; echo one > one.txt'"
623 ASYNC RUN "sh -c 'sleep 0.5; echo two > two.txt'"
624 WRITE "done.txt" "ok"
625 "#
626 };
627
628 let steps = crate::parse_script(script).unwrap();
629 let start = Instant::now();
630 let res = run_steps(&root, &steps);
631 let elapsed = start.elapsed();
632
633 assert!(res.is_ok(), "ASYNC success should allow clean exit");
634 assert!(
635 exists(&root, "done.txt"),
636 "foreground step should run after spawning backgrounds"
637 );
638 assert!(
639 exists(&root, "one.txt"),
640 "first background should finish and emit output"
641 );
642 assert!(
645 exists(&root, "two.txt"),
646 "second background should finish (poll-all waits for all)"
647 );
648
649 let upper = 0.8;
650 assert!(
651 elapsed.as_secs_f32() < upper && elapsed.as_secs_f32() > 0.15,
652 "should wait for both backgrounds (~0.5s); got {elapsed:?}"
653 );
654 }
655
656 #[cfg(unix)]
657 #[cfg_attr(
658 miri,
659 ignore = "timing-sensitive background process test is unreliable under Miri"
660 )]
661 #[test]
662 fn background_killed_when_unrelated_step_fails() {
663 let temp = GuardedPath::tempdir().unwrap();
664 let root = guard_root(&temp);
665
666 let script = indoc! {
667 r#"
668 ASYNC RUN "sh -c 'sleep 1; echo late > late.txt'"
669 RUN "__oxdock_missing_command_xyz__"
670 "#
671 };
672
673 let steps = crate::parse_script(script).unwrap();
674 assert!(
675 run_steps(&root, &steps).is_err(),
676 "pipeline should fail on the missing command"
677 );
678 assert!(
682 !exists(&root, "late.txt"),
683 "abandoned background writer must be killed by Drop teardown"
684 );
685 }
686
687 #[cfg(unix)]
688 #[cfg_attr(
689 miri,
690 ignore = "stdout streaming not supported for background command under miri"
691 )]
692 #[test]
693 fn exit_terminates_backgrounds_and_returns_code() {
694 let temp = GuardedPath::tempdir().unwrap();
695 let root = guard_root(&temp);
696
697 let script = indoc! {
698 r#"
699 ASYNC RUN "sh -c 'sleep 1; echo late > late.txt'"
700 EXIT 5
701 "#
702 };
703
704 let steps = crate::parse_script(script).unwrap();
705 let err = run_steps(&root, &steps).unwrap_err();
706 let msg = err.to_string();
707 assert!(msg.contains("EXIT requested with code 5"));
708 assert!(
709 !exists(&root, "late.txt"),
710 "background process should be killed when EXIT is hit"
711 );
712 }
713
714 #[test]
715 #[cfg_attr(
716 miri,
717 ignore = "EXIT joins real background threads; timing-sensitive under Miri"
718 )]
719 fn exit_inside_nested_block_reports_code_and_kills_backgrounds() {
720 let temp = GuardedPath::tempdir().unwrap();
721 let root = guard_root(&temp);
722
723 let script = indoc! {
724 r#"
725 ASYNC {
726 SLEEP 30s
727 }
728 [bool:true] {
729 EXIT 5
730 }
731 "#
732 };
733
734 let steps = crate::parse_script(script).unwrap();
735 let start = std::time::Instant::now();
736 let err = run_steps(&root, &steps).unwrap_err();
737 assert!(
738 err.to_string().contains("EXIT requested with code 5"),
739 "nested EXIT must report its code: {err}"
740 );
741 assert!(
742 start.elapsed() < std::time::Duration::from_secs(10),
743 "nested EXIT must kill the background SLEEP promptly"
744 );
745 }
746
747 #[cfg_attr(
748 miri,
749 ignore = "stdout streaming not supported for background command under miri"
750 )]
751 #[test]
752 fn env_applies_to_run_and_background() {
753 let temp = GuardedPath::tempdir().unwrap();
754 let root = guard_root(&temp);
755
756 #[allow(clippy::disallowed_macros)]
757 let script = if cfg!(windows) {
758 indoc! {
759 r#"
760 ENV FOO="bar"
761 RUN "echo %FOO% > run.txt"
762 ASYNC RUN "echo %FOO% > bg.txt"
763 "#
764 }
765 } else {
766 indoc! {
767 r#"
768 ENV FOO="bar"
769 RUN "sh -c 'printf %s \"$FOO\" > run.txt'"
770 ASYNC RUN "sh -c 'printf %s \"$FOO\" > bg.txt'"
771 "#
772 }
773 };
774
775 let steps = crate::parse_script(script).unwrap();
776 run_steps(&root, &steps).unwrap();
777
778 assert_eq!(read_trimmed(&root.join("run.txt").unwrap()), "bar");
779 assert_eq!(read_trimmed(&root.join("bg.txt").unwrap()), "bar");
780 }
781
782 #[test]
783 fn workspace_switches_between_snapshot_and_local() {
784 let snapshot = GuardedPath::tempdir().unwrap();
785 let local = GuardedPath::tempdir().unwrap();
786 let snapshot_root = guard_root(&snapshot);
787 let local_root = guard_root(&local);
788
789 let script = indoc! {
790 r#"
791 WRITE "snap.txt" "snap"
792 WORKSPACE LOCAL
793 WRITE "local.txt" "local"
794 WORKSPACE SNAPSHOT
795 WRITE "snap2.txt" "again"
796 "#
797 };
798
799 let steps = crate::parse_script(script).unwrap();
800 run_steps_with_context(&snapshot_root, &local_root, &steps).unwrap();
801
802 assert!(snapshot_root.join("snap.txt").unwrap().exists());
803 assert!(snapshot_root.join("snap2.txt").unwrap().exists());
804 assert!(local_root.join("local.txt").unwrap().exists());
805 }
806
807 #[test]
808 fn workspace_root_changes_where_slash_points() {
809 let snapshot = GuardedPath::tempdir().unwrap();
810 let local = GuardedPath::tempdir().unwrap();
811 let snapshot_root = guard_root(&snapshot);
812 let local_root = guard_root(&local);
813 let local_client = local_root.join("client").unwrap();
814 create_dirs(&local_client);
815
816 let script = indoc! {
817 r#"
818 WORKSPACE LOCAL
819 WORKDIR "/"
820 WRITE "localroot.txt" "one"
821 WORKDIR "client"
822 WRITE "client.txt" "two"
823 WORKSPACE SNAPSHOT
824 WORKDIR "/"
825 WRITE "snaproot.txt" "three"
826 "#
827 };
828
829 let steps = crate::parse_script(script).unwrap();
830 run_steps_with_context(&snapshot_root, &local_root, &steps).unwrap();
831
832 assert!(local_root.join("localroot.txt").unwrap().exists());
833 assert!(local_client.join("client.txt").unwrap().exists());
834 assert!(snapshot_root.join("snaproot.txt").unwrap().exists());
835 }
836}