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