1use std::collections::BTreeMap;
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16
17use async_trait::async_trait;
18use tokio::sync::Mutex;
19use tokio_util::sync::CancellationToken;
20
21use crate::env::{
22 check_cancel_exec, check_cancel_file, ExecutionEnv, FileContent, FileInfo, FileKind,
23 FileSystem, Shell, ShellExecOptions, ShellOutput,
24};
25#[cfg(test)]
26use crate::error::ExecutionErrorCode;
27use crate::error::{io_to_file_code, ExecutionError, FileError, FileErrorCode};
28use crate::file_mutation_queue::{MutatingEnv, MutationQueueRegistry};
29
30#[derive(Clone)]
32struct InMemoryFile {
33 bytes: Vec<u8>,
34 mtime_ms: i64,
35 kind: FileKind,
36}
37
38#[derive(Default)]
39struct Inner {
40 cwd: PathBuf,
41 files: BTreeMap<String, InMemoryFile>,
42 clock: i64,
45 shell_scripts: Vec<(String, ShellScript)>,
48 active_pids: Vec<i64>,
51}
52
53#[derive(Clone)]
55pub struct ShellScript {
56 pub stdout: String,
57 pub stderr: String,
58 pub exit_code: i32,
59 pub delay_ms: Option<u64>,
62}
63
64impl ShellScript {
65 pub fn success(stdout: impl Into<String>) -> Self {
66 Self {
67 stdout: stdout.into(),
68 stderr: String::new(),
69 exit_code: 0,
70 delay_ms: None,
71 }
72 }
73 pub fn failure(exit_code: i32, stderr: impl Into<String>) -> Self {
74 Self {
75 stdout: String::new(),
76 stderr: stderr.into(),
77 exit_code,
78 delay_ms: None,
79 }
80 }
81}
82
83#[derive(Clone)]
86pub struct InMemoryExecutionEnv {
87 inner: Arc<Mutex<Inner>>,
88 registry: Arc<MutationQueueRegistry>,
89 cwd_snapshot: Arc<PathBuf>,
92}
93
94impl InMemoryExecutionEnv {
95 pub fn new() -> Self {
96 Self::with_cwd(PathBuf::from("/"))
97 }
98
99 pub fn with_cwd(cwd: PathBuf) -> Self {
100 let snap = Arc::new(cwd.clone());
101 let mut files: BTreeMap<String, InMemoryFile> = BTreeMap::new();
102 let cwd_norm = Self::norm(&cwd.to_string_lossy());
108 let mut acc = String::new();
109 for seg in cwd_norm.trim_matches('/').split('/') {
110 acc = if acc.is_empty() {
111 format!("/{seg}")
112 } else {
113 format!("{acc}/{seg}")
114 };
115 files.insert(
116 acc.clone(),
117 InMemoryFile {
118 bytes: Vec::new(),
119 mtime_ms: 0,
120 kind: FileKind::Directory,
121 },
122 );
123 }
124 Self {
125 inner: Arc::new(Mutex::new(Inner {
126 cwd,
127 files,
128 clock: 0,
129 shell_scripts: Vec::new(),
130 active_pids: Vec::new(),
131 })),
132 registry: Arc::new(MutationQueueRegistry::new()),
133 cwd_snapshot: snap,
134 }
135 }
136
137 pub async fn register_shell(&self, prefix: impl Into<String>, script: ShellScript) {
140 let mut g = self.inner.lock().await;
141 g.shell_scripts.push((prefix.into(), script));
142 g.shell_scripts.sort_by(|a, b| b.0.len().cmp(&a.0.len()));
144 }
145
146 pub async fn seed_file(&self, abs_path: &str, bytes: Vec<u8>) {
148 let mut g = self.inner.lock().await;
149 g.clock += 1;
150 let mtime = g.clock;
151 g.files.insert(
152 abs_path.to_string(),
153 InMemoryFile {
154 bytes,
155 mtime_ms: mtime,
156 kind: FileKind::File,
157 },
158 );
159 }
160
161 async fn bump_clock(&self, inner: &mut Inner) -> i64 {
162 inner.clock += 1;
163 inner.clock
164 }
165
166 fn resolve(cwd: &Path, path: &str) -> PathBuf {
177 let pb = PathBuf::from(path);
178 let joined = if pb.is_absolute() { pb } else { cwd.join(pb) };
179 let mut out = PathBuf::new();
180 for comp in joined.components() {
181 match comp {
182 std::path::Component::ParentDir => {
183 out.pop();
184 }
185 std::path::Component::CurDir => {}
186 other => out.push(other),
187 }
188 }
189 out
190 }
191
192 fn norm(s: &str) -> String {
203 s.replace('\\', "/")
204 }
205
206 fn resolve_key(cwd: &Path, path: &str) -> String {
209 Self::norm(&Self::resolve(cwd, path).to_string_lossy())
210 }
211}
212
213impl Default for InMemoryExecutionEnv {
214 fn default() -> Self {
215 Self::new()
216 }
217}
218
219#[async_trait]
220impl FileSystem for InMemoryExecutionEnv {
221 fn cwd(&self) -> &Path {
222 &self.cwd_snapshot
223 }
224
225 async fn absolute_path(
226 &self,
227 path: &str,
228 cancel: Option<&CancellationToken>,
229 ) -> Result<PathBuf, FileError> {
230 check_cancel_file(cancel, Some(path))?;
231 let g = self.inner.lock().await;
232 let resolved = Self::resolve(&g.cwd, path).to_string_lossy().into_owned();
233 Ok(PathBuf::from(Self::norm(&resolved)))
237 }
238
239 async fn join_path(
240 &self,
241 parts: &[&str],
242 cancel: Option<&CancellationToken>,
243 ) -> Result<PathBuf, FileError> {
244 check_cancel_file(cancel, None)?;
245 let mut out = String::new();
246 for p in parts {
247 if out.is_empty() {
248 out = p.to_string();
249 } else if out.ends_with('/') {
250 out.push_str(p);
251 } else {
252 out.push('/');
253 out.push_str(p);
254 }
255 }
256 Ok(PathBuf::from(Self::norm(&out)))
258 }
259
260 async fn read_text_file(
261 &self,
262 path: &str,
263 cancel: Option<&CancellationToken>,
264 ) -> Result<String, FileError> {
265 let bytes = self.read_binary_file(path, cancel).await?;
266 String::from_utf8(bytes).map_err(|e| {
267 FileError::new(FileErrorCode::Invalid, format!("invalid utf-8: {e}")).with_path(path)
268 })
269 }
270
271 async fn read_text_lines(
272 &self,
273 path: &str,
274 max_lines: Option<usize>,
275 cancel: Option<&CancellationToken>,
276 ) -> Result<Vec<String>, FileError> {
277 check_cancel_file(cancel, Some(path))?;
278 if max_lines.map(|m| m == 0).unwrap_or(false) {
279 return Ok(Vec::new());
280 }
281 let text = self.read_text_file(path, cancel).await?;
282 let mut lines: Vec<String> = text.split('\n').map(|s| s.to_string()).collect();
283 if text.ends_with('\n') {
284 if lines.last().map(|s| s.is_empty()).unwrap_or(false) {
285 lines.pop();
286 }
287 }
288 if let Some(m) = max_lines {
289 lines.truncate(m);
290 }
291 Ok(lines)
292 }
293
294 async fn read_binary_file(
295 &self,
296 path: &str,
297 cancel: Option<&CancellationToken>,
298 ) -> Result<Vec<u8>, FileError> {
299 check_cancel_file(cancel, Some(path))?;
300 let g = self.inner.lock().await;
301 let abs = Self::resolve_key(&g.cwd, path);
302 match g.files.get(&abs) {
303 Some(f) if f.kind == FileKind::File => Ok(f.bytes.clone()),
304 Some(f) if f.kind == FileKind::Directory => Err(FileError::new(
305 FileErrorCode::IsDirectory,
306 "path is a directory",
307 )
308 .with_path(&abs)),
309 Some(f) if f.kind == FileKind::Symlink => {
310 Err(
312 FileError::new(FileErrorCode::NotSupported, "symlink read unsupported")
313 .with_path(&abs),
314 )
315 }
316 _ => Err(FileError::not_found(&abs)),
317 }
318 }
319
320 async fn write_file(
321 &self,
322 path: &str,
323 content: FileContent,
324 cancel: Option<&CancellationToken>,
325 ) -> Result<(), FileError> {
326 check_cancel_file(cancel, Some(path))?;
327 let mut g = self.inner.lock().await;
328 let abs = Self::resolve_key(&g.cwd, path);
329 let parent = Path::new(&abs)
332 .parent()
333 .map(|p| Self::norm(&p.to_string_lossy()));
334 if let Some(p) = parent {
335 if !p.is_empty() && !g.files.contains_key(&p) {
336 let mtime = self.bump_clock(&mut g).await;
337 g.files.insert(
338 p,
339 InMemoryFile {
340 bytes: Vec::new(),
341 mtime_ms: mtime,
342 kind: FileKind::Directory,
343 },
344 );
345 }
346 }
347 let mtime = self.bump_clock(&mut g).await;
348 g.files.insert(
349 abs.clone(),
350 InMemoryFile {
351 bytes: content.as_bytes().to_vec(),
352 mtime_ms: mtime,
353 kind: FileKind::File,
354 },
355 );
356 Ok(())
357 }
358
359 async fn append_file(
360 &self,
361 path: &str,
362 content: FileContent,
363 cancel: Option<&CancellationToken>,
364 ) -> Result<(), FileError> {
365 check_cancel_file(cancel, Some(path))?;
366 let mut g = self.inner.lock().await;
367 let abs = Self::resolve_key(&g.cwd, path);
368 let mtime = self.bump_clock(&mut g).await;
369 match g.files.get_mut(&abs) {
370 Some(f) if f.kind == FileKind::File => {
371 f.bytes.extend_from_slice(content.as_bytes());
372 f.mtime_ms = mtime;
373 Ok(())
374 }
375 Some(f) if f.kind == FileKind::Directory => Err(FileError::new(
376 FileErrorCode::IsDirectory,
377 "path is a directory",
378 )
379 .with_path(&abs)),
380 _ => {
381 g.files.insert(
382 abs.clone(),
383 InMemoryFile {
384 bytes: content.as_bytes().to_vec(),
385 mtime_ms: mtime,
386 kind: FileKind::File,
387 },
388 );
389 Ok(())
390 }
391 }
392 }
393
394 async fn rename_file(
395 &self,
396 source: &str,
397 dest: &str,
398 cancel: Option<&CancellationToken>,
399 ) -> Result<(), FileError> {
400 check_cancel_file(cancel, Some(source))?;
401 let mut g = self.inner.lock().await;
402 let src_abs = Self::resolve_key(&g.cwd, source);
403 let dst_abs = Self::resolve_key(&g.cwd, dest);
404 match g.files.remove(&src_abs) {
405 Some(f) => {
406 let mtime = self.bump_clock(&mut g).await;
407 g.files.insert(
408 dst_abs.clone(),
409 InMemoryFile {
410 bytes: f.bytes,
411 mtime_ms: mtime,
412 kind: f.kind,
413 },
414 );
415 Ok(())
416 }
417 None => Err(FileError::not_found(&src_abs)),
418 }
419 }
420
421 async fn file_info(
422 &self,
423 path: &str,
424 cancel: Option<&CancellationToken>,
425 ) -> Result<FileInfo, FileError> {
426 check_cancel_file(cancel, Some(path))?;
427 let g = self.inner.lock().await;
428 let abs = Self::resolve_key(&g.cwd, path);
429 match g.files.get(&abs) {
430 Some(f) => Ok(FileInfo {
431 name: Path::new(&abs)
432 .file_name()
433 .map(|n| n.to_string_lossy().into_owned())
434 .unwrap_or_else(|| abs.clone()),
435 path: PathBuf::from(&abs),
436 kind: f.kind,
437 size: f.bytes.len() as u64,
438 mtime_ms: f.mtime_ms,
439 }),
440 None => Err(FileError::not_found(&abs)),
441 }
442 }
443
444 async fn list_dir(
445 &self,
446 path: &str,
447 cancel: Option<&CancellationToken>,
448 ) -> Result<Vec<FileInfo>, FileError> {
449 check_cancel_file(cancel, Some(path))?;
450 let g = self.inner.lock().await;
451 let abs = Self::resolve_key(&g.cwd, path);
452 let prefix = if abs.ends_with('/') {
454 abs.clone()
455 } else {
456 format!("{abs}/")
457 };
458 let dir_exists = g
460 .files
461 .get(&abs)
462 .map(|f| f.kind == FileKind::Directory)
463 .unwrap_or(abs == "/");
464 if !dir_exists {
465 return Err(if g.files.contains_key(&abs) {
466 FileError::new(FileErrorCode::NotDirectory, "path is not a directory")
467 .with_path(&abs)
468 } else {
469 FileError::not_found(&abs)
470 });
471 }
472 let mut out = Vec::new();
473 for (k, f) in g.files.iter() {
474 if let Some(rest) = k.strip_prefix(&prefix) {
475 if !rest.is_empty() && !rest.contains('/') {
477 out.push(FileInfo {
478 name: rest.to_string(),
479 path: PathBuf::from(k),
480 kind: f.kind,
481 size: f.bytes.len() as u64,
482 mtime_ms: f.mtime_ms,
483 });
484 }
485 }
486 }
487 Ok(out)
488 }
489
490 async fn canonical_path(
491 &self,
492 path: &str,
493 cancel: Option<&CancellationToken>,
494 ) -> Result<PathBuf, FileError> {
495 check_cancel_file(cancel, Some(path))?;
496 let g = self.inner.lock().await;
497 let abs = Self::resolve_key(&g.cwd, path);
498 if g.files.contains_key(&abs) || abs == "/" {
499 Ok(PathBuf::from(&abs))
500 } else {
501 Err(FileError::not_found(&abs))
502 }
503 }
504
505 async fn exists(
506 &self,
507 path: &str,
508 cancel: Option<&CancellationToken>,
509 ) -> Result<bool, FileError> {
510 check_cancel_file(cancel, Some(path))?;
511 let g = self.inner.lock().await;
512 let abs = Self::resolve_key(&g.cwd, path);
513 Ok(g.files.contains_key(&abs) || abs == "/")
514 }
515
516 async fn create_dir(
517 &self,
518 path: &str,
519 recursive: bool,
520 cancel: Option<&CancellationToken>,
521 ) -> Result<(), FileError> {
522 check_cancel_file(cancel, Some(path))?;
523 let mut g = self.inner.lock().await;
524 let abs = Self::resolve_key(&g.cwd, path);
525 if recursive {
526 let mut acc = String::new();
528 for seg in abs.trim_matches('/').split('/') {
529 if acc.is_empty() {
530 acc = format!("/{seg}");
531 } else {
532 acc = format!("{acc}/{seg}");
533 }
534 if !g.files.contains_key(&acc) {
535 let mtime = self.bump_clock(&mut g).await;
536 g.files.insert(
537 acc.clone(),
538 InMemoryFile {
539 bytes: Vec::new(),
540 mtime_ms: mtime,
541 kind: FileKind::Directory,
542 },
543 );
544 }
545 }
546 } else {
547 if !g.files.contains_key(&abs) {
548 let mtime = self.bump_clock(&mut g).await;
549 g.files.insert(
550 abs.clone(),
551 InMemoryFile {
552 bytes: Vec::new(),
553 mtime_ms: mtime,
554 kind: FileKind::Directory,
555 },
556 );
557 }
558 }
559 Ok(())
560 }
561
562 async fn remove(
563 &self,
564 path: &str,
565 recursive: bool,
566 force: bool,
567 cancel: Option<&CancellationToken>,
568 ) -> Result<(), FileError> {
569 check_cancel_file(cancel, Some(path))?;
570 let mut g = self.inner.lock().await;
571 let abs = Self::resolve_key(&g.cwd, path);
572 if !g.files.contains_key(&abs) {
573 if force {
574 return Ok(());
575 }
576 return Err(FileError::not_found(&abs));
577 }
578 if recursive {
579 let prefix = if abs.ends_with('/') {
581 abs.clone()
582 } else {
583 format!("{abs}/")
584 };
585 let to_remove: Vec<String> = g
586 .files
587 .keys()
588 .filter(|k| *k == &abs || k.starts_with(&prefix))
589 .cloned()
590 .collect();
591 for k in to_remove {
592 g.files.remove(&k);
593 }
594 } else {
595 let prefix = if abs.ends_with('/') {
597 abs.clone()
598 } else {
599 format!("{abs}/")
600 };
601 let has_children = g.files.keys().any(|k| k.starts_with(&prefix));
602 if has_children {
603 return Err(FileError::new(
604 FileErrorCode::NotSupported,
605 "directory not empty (use recursive)",
606 )
607 .with_path(&abs));
608 }
609 g.files.remove(&abs);
610 }
611 Ok(())
612 }
613
614 async fn create_temp_dir(
615 &self,
616 prefix: Option<&str>,
617 cancel: Option<&CancellationToken>,
618 ) -> Result<PathBuf, FileError> {
619 check_cancel_file(cancel, None)?;
620 let mut g = self.inner.lock().await;
621 let prefix = prefix.unwrap_or("tmp-");
622 let n = g.clock + 1;
624 let name = format!("{}{}", prefix, unique_suffix(n));
625 let abs = format!("/tmp/{name}");
626 let mtime = self.bump_clock(&mut g).await;
627 g.files.insert(
628 abs.clone(),
629 InMemoryFile {
630 bytes: Vec::new(),
631 mtime_ms: mtime,
632 kind: FileKind::Directory,
633 },
634 );
635 Ok(PathBuf::from(abs))
636 }
637
638 async fn create_temp_file(
639 &self,
640 prefix: &str,
641 suffix: &str,
642 cancel: Option<&CancellationToken>,
643 ) -> Result<PathBuf, FileError> {
644 check_cancel_file(cancel, None)?;
645 let mut g = self.inner.lock().await;
646 let n = g.clock + 1;
647 let name = format!("{}{}{}", prefix, unique_suffix(n), suffix);
648 let abs = format!("/tmp/{name}");
649 let mtime = self.bump_clock(&mut g).await;
650 g.files.insert(
651 abs.clone(),
652 InMemoryFile {
653 bytes: Vec::new(),
654 mtime_ms: mtime,
655 kind: FileKind::File,
656 },
657 );
658 Ok(PathBuf::from(abs))
659 }
660
661 async fn cleanup(&self) {
662 let mut g = self.inner.lock().await;
663 g.active_pids.clear();
664 }
665}
666
667#[async_trait]
668impl Shell for InMemoryExecutionEnv {
669 async fn exec<'a>(
670 &'a self,
671 command: &str,
672 mut options: ShellExecOptions<'a>,
673 ) -> Result<ShellOutput, ExecutionError> {
674 check_cancel_exec(options.cancel)?;
675 let scripts = {
676 let g = self.inner.lock().await;
677 g.shell_scripts.clone()
678 };
679 let matched = scripts.iter().find(|(p, _)| command.starts_with(p));
681 let script = match matched {
682 Some((_, s)) => s.clone(),
683 None => ShellScript {
684 stdout: String::new(),
685 stderr: format!("no shell script registered for: {command}"),
686 exit_code: 127,
687 delay_ms: None,
688 },
689 };
690
691 {
693 let mut g = self.inner.lock().await;
694 let pid = g.clock;
695 g.active_pids.push(pid);
696 }
697
698 if let Some(ms) = script.delay_ms {
700 let dur = std::time::Duration::from_millis(ms);
701 if let Some(token) = options.cancel {
702 tokio::select! {
703 _ = tokio::time::sleep(dur) => {}
704 _ = token.cancelled() => {
705 return Err(ExecutionError::aborted());
706 }
707 }
708 } else if let Some(secs) = options.timeout {
709 let to = std::time::Duration::from_secs_f64(secs);
710 let sleep = tokio::time::sleep(dur.min(to));
711 tokio::pin!(sleep);
712 let deadline = tokio::time::sleep(to);
713 tokio::pin!(deadline);
714 tokio::select! {
715 _ = &mut sleep => {}
716 _ = &mut deadline => {
717 return Err(ExecutionError::timeout(secs as u64));
718 }
719 }
720 } else {
721 tokio::time::sleep(dur).await;
722 }
723 } else if let Some(secs) = options.timeout {
724 let _ = secs;
729 }
730
731 if let Some(cb) = options.on_stdout.as_mut() {
733 if !script.stdout.is_empty() {
734 cb(&script.stdout);
735 }
736 }
737 if let Some(cb) = options.on_stderr.as_mut() {
738 if !script.stderr.is_empty() {
739 cb(&script.stderr);
740 }
741 }
742
743 Ok(ShellOutput {
744 stdout: script.stdout,
745 stderr: script.stderr,
746 exit_code: script.exit_code,
747 })
748 }
749
750 async fn cleanup(&self) {
751 let mut g = self.inner.lock().await;
752 g.active_pids.clear();
753 }
754}
755
756#[async_trait]
757impl ExecutionEnv for InMemoryExecutionEnv {}
758
759#[async_trait]
760impl MutatingEnv for InMemoryExecutionEnv {
761 fn as_env(&self) -> &dyn ExecutionEnv {
762 self
763 }
764 fn mutation_registry(&self) -> &MutationQueueRegistry {
765 &self.registry
766 }
767}
768
769fn unique_suffix(n: i64) -> String {
772 let mut s = String::new();
774 let mut v = n.max(0) as u64;
775 if v == 0 {
776 return "000000".to_string();
777 }
778 while v > 0 {
779 let d = (v % 36) as u8;
780 let c = if d < 10 { b'0' + d } else { b'a' + (d - 10) };
781 s.insert(0, c as char);
782 v /= 36;
783 }
784 while s.len() < 6 {
785 s.insert(0, '0');
786 }
787 s
788}
789
790#[allow(dead_code)]
794fn unused_io_reexport(_: std::io::Error) -> FileErrorCode {
795 io_to_file_code(&std::io::Error::from(std::io::ErrorKind::Other))
796}
797
798#[cfg(test)]
799mod tests {
800 use super::*;
801
802 #[tokio::test]
803 async fn write_read_roundtrip() {
804 let env = InMemoryExecutionEnv::new();
805 env.write_file("/a/b.txt", "hello".into(), None)
806 .await
807 .unwrap();
808 let text = env.read_text_file("/a/b.txt", None).await.unwrap();
809 assert_eq!(text, "hello");
810 assert!(env.exists("/a", None).await.unwrap());
812 }
813
814 #[tokio::test]
815 async fn rename_moves_bytes() {
816 let env = InMemoryExecutionEnv::new();
817 env.write_file("/x.txt", "data".into(), None).await.unwrap();
818 env.rename_file("/x.txt", "/y.txt", None).await.unwrap();
819 assert!(!env.exists("/x.txt", None).await.unwrap());
820 assert_eq!(env.read_text_file("/y.txt", None).await.unwrap(), "data");
821 }
822
823 #[tokio::test]
824 async fn list_dir_children() {
825 let env = InMemoryExecutionEnv::new();
826 env.create_dir("/d", true, None).await.unwrap();
827 env.write_file("/d/a.txt", "1".into(), None).await.unwrap();
828 env.write_file("/d/b.txt", "2".into(), None).await.unwrap();
829 let entries = env.list_dir("/d", None).await.unwrap();
830 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
831 assert!(names.contains(&"a.txt"));
832 assert!(names.contains(&"b.txt"));
833 }
834
835 #[tokio::test]
836 async fn remove_recursive() {
837 let env = InMemoryExecutionEnv::new();
838 env.create_dir("/r", true, None).await.unwrap();
839 env.write_file("/r/a.txt", "1".into(), None).await.unwrap();
840 env.remove("/r", true, true, None).await.unwrap();
841 assert!(!env.exists("/r", None).await.unwrap());
842 assert!(!env.exists("/r/a.txt", None).await.unwrap());
843 }
844
845 #[tokio::test]
846 async fn temp_dir_and_file_are_unique() {
847 let env = InMemoryExecutionEnv::new();
848 let d1 = env.create_temp_dir(Some("prefix-"), None).await.unwrap();
849 let d2 = env.create_temp_dir(Some("prefix-"), None).await.unwrap();
850 assert_ne!(d1, d2);
851 let f1 = env.create_temp_file("pre-", "-suf", None).await.unwrap();
852 let f2 = env.create_temp_file("pre-", "-suf", None).await.unwrap();
853 assert_ne!(f1, f2);
854 }
855
856 #[tokio::test]
857 async fn shell_script_longest_prefix_match() {
858 let env = InMemoryExecutionEnv::new();
859 env.register_shell("git", ShellScript::success("git-out"))
860 .await;
861 env.register_shell("git status", ShellScript::success("clean"))
862 .await;
863 let out = env
864 .exec("git status", ShellExecOptions::default())
865 .await
866 .unwrap();
867 assert_eq!(out.stdout, "clean");
868 let out2 = env
869 .exec("git log", ShellExecOptions::default())
870 .await
871 .unwrap();
872 assert_eq!(out2.stdout, "git-out");
873 }
874
875 #[tokio::test]
876 async fn cancel_returns_aborted() {
877 let env = InMemoryExecutionEnv::new();
878 let token = CancellationToken::new();
879 token.cancel();
880 let r = env
881 .exec(
882 "anything",
883 ShellExecOptions {
884 cancel: Some(&token),
885 ..Default::default()
886 },
887 )
888 .await;
889 assert!(matches!(
890 r,
891 Err(ExecutionError {
892 code: ExecutionErrorCode::Aborted,
893 ..
894 })
895 ));
896 }
897}