trycmd/
runner.rs

1use std::io::prelude::*;
2
3#[cfg(feature = "color")]
4use anstream::eprintln;
5#[cfg(feature = "color")]
6use anstream::panic;
7#[cfg(feature = "color")]
8use anstream::stderr;
9#[cfg(not(feature = "color"))]
10use std::eprintln;
11#[cfg(not(feature = "color"))]
12use std::io::stderr;
13
14use rayon::prelude::*;
15use snapbox::data::DataFormat;
16use snapbox::dir::FileType;
17use snapbox::filter::{Filter as _, FilterNewlines, FilterPaths, NormalizeToExpected};
18use snapbox::IntoData;
19
20#[derive(Debug)]
21pub(crate) struct Runner {
22    cases: Vec<Case>,
23}
24
25impl Runner {
26    pub(crate) fn new() -> Self {
27        Self {
28            cases: Default::default(),
29        }
30    }
31
32    pub(crate) fn case(&mut self, case: Case) {
33        self.cases.push(case);
34    }
35
36    pub(crate) fn run(
37        &self,
38        mode: &Mode,
39        bins: &crate::BinRegistry,
40        substitutions: &snapbox::Redactions,
41    ) {
42        #![allow(unexpected_cfgs)] // HACK: until we upgrade the minimum anstream
43        let palette = snapbox::report::Palette::color();
44
45        if self.cases.is_empty() {
46            eprintln!("{}", palette.warn("There are no trycmd tests enabled yet"));
47        } else {
48            let failures: Vec<_> = self
49                .cases
50                .par_iter()
51                .flat_map(|c| {
52                    let results = c.run(mode, bins, substitutions);
53
54                    let stderr = stderr();
55                    let mut stderr = stderr.lock();
56
57                    results
58                        .into_iter()
59                        .filter_map(|s| {
60                            snapbox::debug!("Case: {:#?}", s);
61                            match s {
62                                Ok(status) => {
63                                    let _ = write!(
64                                        stderr,
65                                        "{} {} ... {}",
66                                        palette.hint("Testing"),
67                                        status.name(),
68                                        status.spawn.status.summary(),
69                                    );
70                                    if let Some(duration) = status.duration {
71                                        let _ = write!(
72                                            stderr,
73                                            " {}",
74                                            palette.hint(humantime::format_duration(duration)),
75                                        );
76                                    }
77                                    let _ = writeln!(stderr);
78                                    if !status.is_ok() {
79                                        // Assuming `status` will print the newline
80                                        let _ = write!(stderr, "{}", &status);
81                                    }
82                                    None
83                                }
84                                Err(status) => {
85                                    let _ = write!(
86                                        stderr,
87                                        "{} {} ... {}",
88                                        palette.hint("Testing"),
89                                        status.name(),
90                                        palette.error("failed"),
91                                    );
92                                    if let Some(duration) = status.duration {
93                                        let _ = write!(
94                                            stderr,
95                                            " {}",
96                                            palette.hint(humantime::format_duration(duration)),
97                                        );
98                                    }
99                                    let _ = writeln!(stderr);
100                                    // Assuming `status` will print the newline
101                                    let _ = write!(stderr, "{}", &status);
102                                    Some(status)
103                                }
104                            }
105                        })
106                        .collect::<Vec<_>>()
107                })
108                .collect();
109
110            if !failures.is_empty() {
111                let stderr = stderr();
112                let mut stderr = stderr.lock();
113                let _ = writeln!(
114                    stderr,
115                    "{}",
116                    palette.hint("Update snapshots with `TRYCMD=overwrite`"),
117                );
118                let _ = writeln!(
119                    stderr,
120                    "{}",
121                    palette.hint("Debug output with `TRYCMD=dump`"),
122                );
123                panic!("{} of {} tests failed", failures.len(), self.cases.len());
124            }
125        }
126    }
127}
128
129impl Default for Runner {
130    fn default() -> Self {
131        Self::new()
132    }
133}
134
135#[derive(Debug)]
136pub(crate) struct Case {
137    pub(crate) path: std::path::PathBuf,
138    pub(crate) expected: Option<crate::schema::CommandStatus>,
139    pub(crate) timeout: Option<std::time::Duration>,
140    pub(crate) default_bin: Option<crate::schema::Bin>,
141    pub(crate) env: crate::schema::Env,
142    pub(crate) error: Option<SpawnStatus>,
143}
144
145impl Case {
146    pub(crate) fn with_error(path: std::path::PathBuf, error: crate::Error) -> Self {
147        Self {
148            path,
149            expected: None,
150            timeout: None,
151            default_bin: None,
152            env: Default::default(),
153            error: Some(SpawnStatus::Failure(error)),
154        }
155    }
156
157    pub(crate) fn run(
158        &self,
159        mode: &Mode,
160        bins: &crate::BinRegistry,
161        substitutions: &snapbox::Redactions,
162    ) -> Vec<Result<Output, Output>> {
163        if self.expected == Some(crate::schema::CommandStatus::Skipped) {
164            let output = Output::sequence(self.path.clone());
165            assert_eq!(output.spawn.status, SpawnStatus::Skipped);
166            return vec![Ok(output)];
167        }
168
169        if let Some(err) = self.error.clone() {
170            let mut output = Output::step(self.path.clone(), "setup".into());
171            output.spawn.status = err;
172            return vec![Err(output)];
173        }
174
175        let mut sequence = match crate::schema::TryCmd::load(&self.path) {
176            Ok(sequence) => sequence,
177            Err(e) => {
178                let output = Output::step(self.path.clone(), "setup".into());
179                return vec![Err(output.error(e))];
180            }
181        };
182
183        if sequence.steps.is_empty() {
184            let output = Output::sequence(self.path.clone());
185            assert_eq!(output.spawn.status, SpawnStatus::Skipped);
186            return vec![Ok(output)];
187        }
188
189        let fs_context = match fs_context(
190            &self.path,
191            sequence.fs.base.as_deref(),
192            sequence.fs.sandbox(),
193            mode,
194        ) {
195            Ok(fs_context) => fs_context,
196            Err(e) => {
197                let output = Output::step(self.path.clone(), "setup".into());
198                return vec![Err(
199                    output.error(format!("Failed to initialize sandbox: {e}").into())
200                )];
201            }
202        };
203        let cwd = match fs_context
204            .path()
205            .map(|p| {
206                sequence.fs.rel_cwd().map(|rel| {
207                    let p = p.join(rel);
208                    snapbox::dir::strip_trailing_slash(&p).to_owned()
209                })
210            })
211            .transpose()
212        {
213            Ok(cwd) => cwd.or_else(|| std::env::current_dir().ok()),
214            Err(e) => {
215                let output = Output::step(self.path.clone(), "setup".into());
216                return vec![Err(output.error(e))];
217            }
218        };
219        let mut substitutions = substitutions.clone();
220        if let Some(root) = fs_context.path() {
221            substitutions.insert("[ROOT]", root.to_owned()).unwrap();
222        }
223        if let Some(cwd) = cwd.clone().or_else(|| std::env::current_dir().ok()) {
224            substitutions.insert("[CWD]", cwd).unwrap();
225        }
226        substitutions
227            .insert("[EXE]", std::env::consts::EXE_SUFFIX)
228            .unwrap();
229        snapbox::debug!("{:?}", substitutions);
230
231        let mut outputs = Vec::with_capacity(sequence.steps.len());
232        let mut prior_step_failed = false;
233        for step in &mut sequence.steps {
234            if prior_step_failed {
235                step.expected_status = Some(crate::schema::CommandStatus::Skipped);
236            }
237
238            let step_status = self.run_step(step, cwd.as_deref(), bins, &substitutions);
239            if fs_context.is_mutable() && step_status.is_err() && *mode == Mode::Fail {
240                prior_step_failed = true;
241            }
242            outputs.push(step_status);
243        }
244        match mode {
245            Mode::Dump(root) => {
246                for output in &mut outputs {
247                    let output = match output {
248                        Ok(output) => output,
249                        Err(output) => output,
250                    };
251                    output.stdout =
252                        match self.dump_stream(root, output.id.as_deref(), output.stdout.take()) {
253                            Ok(stream) => stream,
254                            Err(stream) => stream,
255                        };
256                    output.stderr =
257                        match self.dump_stream(root, output.id.as_deref(), output.stderr.take()) {
258                            Ok(stream) => stream,
259                            Err(stream) => stream,
260                        };
261                }
262            }
263            Mode::Overwrite => {
264                // `rev()` to ensure we don't mess up our line number info
265                for step_status in outputs.iter_mut().rev() {
266                    if let Err(output) = step_status {
267                        let res = sequence.overwrite(
268                            &self.path,
269                            output.id.as_deref(),
270                            output.stdout.as_ref().map(|s| &s.content),
271                            output.stderr.as_ref().map(|s| &s.content),
272                            output.spawn.exit,
273                        );
274
275                        if res.is_ok() {
276                            *step_status = Ok(output.clone());
277                        }
278                    }
279                }
280            }
281            Mode::Fail => {}
282        }
283
284        if sequence.fs.sandbox() {
285            let mut ok = true;
286            let mut output = Output::step(self.path.clone(), "teardown".into());
287
288            output.fs = match self.validate_fs(
289                fs_context.path().expect("sandbox must be filled"),
290                output.fs,
291                mode,
292                &substitutions,
293            ) {
294                Ok(fs) => fs,
295                Err(fs) => {
296                    ok = false;
297                    fs
298                }
299            };
300            if let Err(err) = fs_context.close() {
301                ok = false;
302                output.fs.context.push(FileStatus::Failure(
303                    format!("Failed to cleanup sandbox: {err}").into(),
304                ));
305            }
306
307            let output = if ok {
308                output.spawn.status = SpawnStatus::Ok;
309                Ok(output)
310            } else {
311                output.spawn.status = SpawnStatus::Failure("Files left in unexpected state".into());
312                Err(output)
313            };
314            outputs.push(output);
315        }
316
317        outputs
318    }
319
320    #[allow(clippy::result_large_err)]
321    pub(crate) fn run_step(
322        &self,
323        step: &mut crate::schema::Step,
324        cwd: Option<&std::path::Path>,
325        bins: &crate::BinRegistry,
326        substitutions: &snapbox::Redactions,
327    ) -> Result<Output, Output> {
328        let output = if let Some(id) = step.id.clone() {
329            Output::step(self.path.clone(), id)
330        } else {
331            Output::sequence(self.path.clone())
332        };
333
334        let mut bin = step.bin.take();
335        if bin.is_none() {
336            bin.clone_from(&self.default_bin);
337        }
338        bin = bin
339            .map(|name| bins.resolve_bin(name))
340            .transpose()
341            .map_err(|e| output.clone().error(e))?;
342        step.bin = bin;
343        if step.timeout.is_none() {
344            step.timeout = self.timeout;
345        }
346        if self.expected.is_some() {
347            step.expected_status = self.expected;
348        }
349        step.env.update(&self.env);
350
351        if step.expected_status() == crate::schema::CommandStatus::Skipped {
352            assert_eq!(output.spawn.status, SpawnStatus::Skipped);
353            return Ok(output);
354        }
355
356        match &step.bin {
357            Some(crate::schema::Bin::Path(_)) => {}
358            Some(crate::schema::Bin::Name(_name)) => {
359                // Unhandled by resolve
360                snapbox::debug!("bin={:?} not found", _name);
361                assert_eq!(output.spawn.status, SpawnStatus::Skipped);
362                return Ok(output);
363            }
364            Some(crate::schema::Bin::Error(_)) => {}
365            // Unlike `Name`, this always represents a bug
366            None => {}
367            Some(crate::schema::Bin::Ignore) => {
368                // Unhandled by resolve
369                assert_eq!(output.spawn.status, SpawnStatus::Skipped);
370                return Ok(output);
371            }
372        }
373
374        let cmd = step.to_command(cwd).map_err(|e| output.clone().error(e))?;
375        let timer = std::time::Instant::now();
376        let cmd_output = cmd
377            .output()
378            .map_err(|e| output.clone().error(e.to_string().into()))?;
379
380        let output = output.output(cmd_output);
381        let output = output.duration(timer.elapsed());
382
383        // For Mode::Dump's sake, allow running all
384        let output = self.validate_spawn(output, step.expected_status());
385        let output = self.validate_streams(output, step, substitutions);
386
387        if output.is_ok() {
388            Ok(output)
389        } else {
390            Err(output)
391        }
392    }
393
394    fn validate_spawn(&self, mut output: Output, expected: crate::schema::CommandStatus) -> Output {
395        let status = output.spawn.exit.expect("bale out before now");
396        match expected {
397            crate::schema::CommandStatus::Success => {
398                if !status.success() {
399                    output.spawn.status = SpawnStatus::Expected("success".into());
400                }
401            }
402            crate::schema::CommandStatus::Failed => {
403                if status.success() || status.code().is_none() {
404                    output.spawn.status = SpawnStatus::Expected("failure".into());
405                }
406            }
407            crate::schema::CommandStatus::Interrupted => {
408                if status.code().is_some() {
409                    output.spawn.status = SpawnStatus::Expected("interrupted".into());
410                }
411            }
412            crate::schema::CommandStatus::Skipped => unreachable!("handled earlier"),
413            crate::schema::CommandStatus::Code(expected_code) => {
414                if Some(expected_code) != status.code() {
415                    output.spawn.status = SpawnStatus::Expected(expected_code.to_string());
416                }
417            }
418        }
419
420        output
421    }
422
423    fn validate_streams(
424        &self,
425        mut output: Output,
426        step: &crate::schema::Step,
427        substitutions: &snapbox::Redactions,
428    ) -> Output {
429        output.stdout = self.validate_stream(
430            output.stdout,
431            step.expected_stdout.as_ref(),
432            step.binary,
433            substitutions,
434        );
435        output.stderr = self.validate_stream(
436            output.stderr,
437            step.expected_stderr.as_ref(),
438            step.binary,
439            substitutions,
440        );
441
442        output
443    }
444
445    fn validate_stream(
446        &self,
447        stream: Option<Stream>,
448        expected_content: Option<&crate::Data>,
449        binary: bool,
450        substitutions: &snapbox::Redactions,
451    ) -> Option<Stream> {
452        let mut stream = stream?;
453
454        if !binary {
455            stream = stream.make_text();
456            if !stream.is_ok() {
457                return Some(stream);
458            }
459        }
460
461        if let Some(expected_content) = expected_content {
462            stream.content = NormalizeToExpected::new()
463                .redact_with(substitutions)
464                .normalize(stream.content, expected_content);
465
466            if stream.content != *expected_content {
467                stream.status = StreamStatus::Expected(expected_content.clone());
468                return Some(stream);
469            }
470        }
471
472        Some(stream)
473    }
474
475    fn dump_stream(
476        &self,
477        root: &std::path::Path,
478        id: Option<&str>,
479        stream: Option<Stream>,
480    ) -> Result<Option<Stream>, Option<Stream>> {
481        if let Some(stream) = stream {
482            let file_name = match id {
483                Some(id) => {
484                    format!(
485                        "{}-{}.{}",
486                        self.path.file_stem().unwrap().to_string_lossy(),
487                        id,
488                        stream.stream.as_str(),
489                    )
490                }
491                None => {
492                    format!(
493                        "{}.{}",
494                        self.path.file_stem().unwrap().to_string_lossy(),
495                        stream.stream.as_str(),
496                    )
497                }
498            };
499            let stream_path = root.join(file_name);
500            stream.content.write_to_path(&stream_path).map_err(|e| {
501                let mut stream = stream.clone();
502                if stream.is_ok() {
503                    stream.status = StreamStatus::Failure(e);
504                }
505                stream
506            })?;
507            Ok(Some(stream))
508        } else {
509            Ok(None)
510        }
511    }
512
513    fn validate_fs(
514        &self,
515        actual_root: &std::path::Path,
516        mut fs: Filesystem,
517        mode: &Mode,
518        substitutions: &snapbox::Redactions,
519    ) -> Result<Filesystem, Filesystem> {
520        let mut ok = true;
521
522        #[cfg(feature = "filesystem")]
523        if let Mode::Dump(_) = mode {
524            // Handled as part of DirRoot
525        } else {
526            let fixture_root = self.path.with_extension("out");
527            if fixture_root.exists() {
528                for status in snapbox::dir::PathDiff::subset_matches_iter(
529                    fixture_root,
530                    actual_root,
531                    substitutions,
532                ) {
533                    match status {
534                        Ok((expected_path, actual_path)) => {
535                            fs.context.push(FileStatus::Ok {
536                                actual_path,
537                                expected_path,
538                            });
539                        }
540                        Err(diff) => {
541                            let mut is_current_ok = false;
542                            if *mode == Mode::Overwrite && diff.overwrite().is_ok() {
543                                is_current_ok = true;
544                            }
545                            fs.context.push(diff.into());
546                            if !is_current_ok {
547                                ok = false;
548                            }
549                        }
550                    }
551                }
552            }
553        }
554
555        if ok {
556            Ok(fs)
557        } else {
558            Err(fs)
559        }
560    }
561}
562
563#[derive(Clone, Debug, PartialEq, Eq)]
564pub(crate) struct Output {
565    path: std::path::PathBuf,
566    id: Option<String>,
567    spawn: Spawn,
568    stdout: Option<Stream>,
569    stderr: Option<Stream>,
570    fs: Filesystem,
571    duration: Option<std::time::Duration>,
572}
573
574impl Output {
575    fn sequence(path: std::path::PathBuf) -> Self {
576        Self {
577            path,
578            id: None,
579            spawn: Spawn {
580                exit: None,
581                status: SpawnStatus::Skipped,
582            },
583            stdout: None,
584            stderr: None,
585            fs: Default::default(),
586            duration: Default::default(),
587        }
588    }
589
590    fn step(path: std::path::PathBuf, step: String) -> Self {
591        Self {
592            path,
593            id: Some(step),
594            spawn: Default::default(),
595            stdout: None,
596            stderr: None,
597            fs: Default::default(),
598            duration: Default::default(),
599        }
600    }
601
602    fn output(mut self, output: std::process::Output) -> Self {
603        self.spawn.exit = Some(output.status);
604        assert_eq!(self.spawn.status, SpawnStatus::Skipped);
605        self.spawn.status = SpawnStatus::Ok;
606        self.stdout = Some(Stream {
607            stream: Stdio::Stdout,
608            content: output.stdout.into_data(),
609            status: StreamStatus::Ok,
610        });
611        self.stderr = Some(Stream {
612            stream: Stdio::Stderr,
613            content: output.stderr.into_data(),
614            status: StreamStatus::Ok,
615        });
616        self
617    }
618
619    fn error(mut self, msg: crate::Error) -> Self {
620        self.spawn.status = SpawnStatus::Failure(msg);
621        self
622    }
623
624    fn duration(mut self, duration: std::time::Duration) -> Self {
625        self.duration = Some(duration);
626        self
627    }
628
629    fn is_ok(&self) -> bool {
630        self.spawn.is_ok()
631            && self.stdout.as_ref().map(|s| s.is_ok()).unwrap_or(true)
632            && self.stderr.as_ref().map(|s| s.is_ok()).unwrap_or(true)
633            && self.fs.is_ok()
634    }
635
636    fn name(&self) -> String {
637        self.id
638            .as_deref()
639            .map(|id| format!("{}:{}", self.path.display(), id))
640            .unwrap_or_else(|| self.path.display().to_string())
641    }
642}
643
644impl std::fmt::Display for Output {
645    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
646        self.spawn.fmt(f)?;
647        if let Some(stdout) = &self.stdout {
648            stdout.fmt(f)?;
649        }
650        if let Some(stderr) = &self.stderr {
651            stderr.fmt(f)?;
652        }
653        self.fs.fmt(f)?;
654
655        Ok(())
656    }
657}
658
659#[derive(Clone, Debug, PartialEq, Eq)]
660struct Spawn {
661    exit: Option<std::process::ExitStatus>,
662    status: SpawnStatus,
663}
664
665impl Spawn {
666    fn is_ok(&self) -> bool {
667        self.status.is_ok()
668    }
669}
670
671impl Default for Spawn {
672    fn default() -> Self {
673        Self {
674            exit: None,
675            status: SpawnStatus::Skipped,
676        }
677    }
678}
679
680impl std::fmt::Display for Spawn {
681    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
682        let palette = snapbox::report::Palette::color();
683
684        match &self.status {
685            SpawnStatus::Ok => {
686                if let Some(exit) = self.exit {
687                    if exit.success() {
688                        writeln!(f, "Exit: {}", palette.info("success"))?;
689                    } else if let Some(code) = exit.code() {
690                        writeln!(f, "Exit: {}", palette.error(code))?;
691                    } else {
692                        writeln!(f, "Exit: {}", palette.error("interrupted"))?;
693                    }
694                }
695            }
696            SpawnStatus::Skipped => {
697                writeln!(f, "{}", palette.warn("Skipped"))?;
698            }
699            SpawnStatus::Failure(msg) => {
700                writeln!(f, "Failed: {}", palette.error(msg))?;
701            }
702            SpawnStatus::Expected(expected) => {
703                if let Some(exit) = self.exit {
704                    if exit.success() {
705                        writeln!(
706                            f,
707                            "Expected {}, was {}",
708                            palette.info(expected),
709                            palette.error("success")
710                        )?;
711                    } else {
712                        writeln!(
713                            f,
714                            "Expected {}, was {}",
715                            palette.info(expected),
716                            palette.error(snapbox::cmd::display_exit_status(exit))
717                        )?;
718                    }
719                }
720            }
721        }
722
723        Ok(())
724    }
725}
726
727#[derive(Clone, Debug, PartialEq, Eq)]
728pub(crate) enum SpawnStatus {
729    Ok,
730    Skipped,
731    Failure(crate::Error),
732    Expected(String),
733}
734
735impl SpawnStatus {
736    fn is_ok(&self) -> bool {
737        match self {
738            Self::Ok | Self::Skipped => true,
739            Self::Failure(_) | Self::Expected(_) => false,
740        }
741    }
742
743    fn summary(&self) -> impl std::fmt::Display {
744        let palette = snapbox::report::Palette::color();
745        match self {
746            Self::Ok => palette.info("ok"),
747            Self::Skipped => palette.warn("ignored"),
748            Self::Failure(_) | Self::Expected(_) => palette.error("failed"),
749        }
750    }
751}
752
753#[derive(Clone, Debug, PartialEq, Eq)]
754struct Stream {
755    stream: Stdio,
756    content: crate::Data,
757    status: StreamStatus,
758}
759
760impl Stream {
761    fn make_text(mut self) -> Self {
762        let content = self.content.coerce_to(DataFormat::Text);
763        if content.format() != DataFormat::Text {
764            self.status = StreamStatus::Failure("Unable to convert underlying Data to Text".into());
765        }
766        self.content = FilterNewlines.filter(FilterPaths.filter(content));
767        self
768    }
769
770    fn is_ok(&self) -> bool {
771        self.status.is_ok()
772    }
773}
774
775impl std::fmt::Display for Stream {
776    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
777        let palette = snapbox::report::Palette::color();
778
779        match &self.status {
780            StreamStatus::Ok => {
781                writeln!(f, "{}:", self.stream)?;
782                writeln!(f, "{}", palette.info(&self.content))?;
783            }
784            StreamStatus::Failure(msg) => {
785                writeln!(
786                    f,
787                    "{} {}:",
788                    self.stream,
789                    palette.error(format_args!("({msg})"))
790                )?;
791                writeln!(f, "{}", palette.info(&self.content))?;
792            }
793            StreamStatus::Expected(expected) => {
794                snapbox::report::write_diff(
795                    f,
796                    expected,
797                    &self.content,
798                    Some(&self.stream),
799                    Some(&self.stream),
800                    palette,
801                )?;
802            }
803        }
804
805        Ok(())
806    }
807}
808
809#[derive(Clone, Debug, PartialEq, Eq)]
810enum StreamStatus {
811    Ok,
812    Failure(crate::Error),
813    Expected(crate::Data),
814}
815
816impl StreamStatus {
817    fn is_ok(&self) -> bool {
818        match self {
819            Self::Ok => true,
820            Self::Failure(_) | Self::Expected(_) => false,
821        }
822    }
823}
824
825#[derive(Copy, Clone, Debug, PartialEq, Eq)]
826enum Stdio {
827    Stdout,
828    Stderr,
829}
830
831impl Stdio {
832    fn as_str(&self) -> &str {
833        match self {
834            Self::Stdout => "stdout",
835            Self::Stderr => "stderr",
836        }
837    }
838}
839
840impl std::fmt::Display for Stdio {
841    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
842        self.as_str().fmt(f)
843    }
844}
845
846#[derive(Clone, Default, Debug, PartialEq, Eq)]
847struct Filesystem {
848    context: Vec<FileStatus>,
849}
850
851impl Filesystem {
852    fn is_ok(&self) -> bool {
853        if self.context.is_empty() {
854            true
855        } else {
856            self.context.iter().all(FileStatus::is_ok)
857        }
858    }
859}
860
861impl std::fmt::Display for Filesystem {
862    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
863        for status in &self.context {
864            status.fmt(f)?;
865        }
866
867        Ok(())
868    }
869}
870
871#[derive(Clone, Debug, PartialEq, Eq)]
872enum FileStatus {
873    Ok {
874        expected_path: std::path::PathBuf,
875        actual_path: std::path::PathBuf,
876    },
877    Failure(crate::Error),
878    TypeMismatch {
879        expected_path: std::path::PathBuf,
880        actual_path: std::path::PathBuf,
881        expected_type: FileType,
882        actual_type: FileType,
883    },
884    LinkMismatch {
885        expected_path: std::path::PathBuf,
886        actual_path: std::path::PathBuf,
887        expected_target: std::path::PathBuf,
888        actual_target: std::path::PathBuf,
889    },
890    ContentMismatch {
891        expected_path: std::path::PathBuf,
892        actual_path: std::path::PathBuf,
893        expected_content: crate::Data,
894        actual_content: crate::Data,
895    },
896}
897
898impl FileStatus {
899    fn is_ok(&self) -> bool {
900        match self {
901            Self::Ok { .. } => true,
902            Self::Failure(_)
903            | Self::TypeMismatch { .. }
904            | Self::LinkMismatch { .. }
905            | Self::ContentMismatch { .. } => false,
906        }
907    }
908}
909
910impl From<snapbox::dir::PathDiff> for FileStatus {
911    fn from(other: snapbox::dir::PathDiff) -> Self {
912        match other {
913            snapbox::dir::PathDiff::Failure(err) => FileStatus::Failure(err),
914            snapbox::dir::PathDiff::TypeMismatch {
915                expected_path,
916                actual_path,
917                expected_type,
918                actual_type,
919            } => FileStatus::TypeMismatch {
920                actual_path,
921                expected_path,
922                actual_type,
923                expected_type,
924            },
925            snapbox::dir::PathDiff::LinkMismatch {
926                expected_path,
927                actual_path,
928                expected_target,
929                actual_target,
930            } => FileStatus::LinkMismatch {
931                actual_path,
932                expected_path,
933                actual_target,
934                expected_target,
935            },
936            snapbox::dir::PathDiff::ContentMismatch {
937                expected_path,
938                actual_path,
939                expected_content,
940                actual_content,
941            } => FileStatus::ContentMismatch {
942                actual_path,
943                expected_path,
944                actual_content,
945                expected_content,
946            },
947        }
948    }
949}
950
951impl std::fmt::Display for FileStatus {
952    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
953        let palette = snapbox::report::Palette::color();
954
955        match &self {
956            Self::Ok {
957                expected_path,
958                actual_path: _actual_path,
959            } => {
960                writeln!(
961                    f,
962                    "{}: is {}",
963                    expected_path.display(),
964                    palette.info("good"),
965                )?;
966            }
967            Self::Failure(msg) => {
968                writeln!(f, "{}", palette.error(msg))?;
969            }
970            Self::TypeMismatch {
971                expected_path,
972                actual_path: _actual_path,
973                expected_type,
974                actual_type,
975            } => {
976                writeln!(
977                    f,
978                    "{}: Expected {}, was {}",
979                    expected_path.display(),
980                    palette.info(expected_type),
981                    palette.error(actual_type)
982                )?;
983            }
984            Self::LinkMismatch {
985                expected_path,
986                actual_path: _actual_path,
987                expected_target,
988                actual_target,
989            } => {
990                writeln!(
991                    f,
992                    "{}: Expected {}, was {}",
993                    expected_path.display(),
994                    palette.info(expected_target.display()),
995                    palette.error(actual_target.display())
996                )?;
997            }
998            Self::ContentMismatch {
999                expected_path,
1000                actual_path,
1001                expected_content,
1002                actual_content,
1003            } => {
1004                snapbox::report::write_diff(
1005                    f,
1006                    expected_content,
1007                    actual_content,
1008                    Some(&expected_path.display()),
1009                    Some(&actual_path.display()),
1010                    palette,
1011                )?;
1012            }
1013        }
1014
1015        Ok(())
1016    }
1017}
1018
1019#[derive(Clone, Debug, PartialEq, Eq)]
1020pub(crate) enum Mode {
1021    Fail,
1022    Overwrite,
1023    Dump(std::path::PathBuf),
1024}
1025
1026impl Mode {
1027    pub(crate) fn initialize(&self) -> Result<(), std::io::Error> {
1028        match self {
1029            Self::Fail => {}
1030            Self::Overwrite => {}
1031            Self::Dump(root) => {
1032                std::fs::create_dir_all(root)?;
1033                let gitignore_path = root.join(".gitignore");
1034                std::fs::write(gitignore_path, "*\n")?;
1035            }
1036        }
1037
1038        Ok(())
1039    }
1040}
1041
1042#[cfg_attr(not(feature = "filesystem"), allow(unused_variables))]
1043fn fs_context(
1044    path: &std::path::Path,
1045    cwd: Option<&std::path::Path>,
1046    sandbox: bool,
1047    mode: &Mode,
1048) -> Result<snapbox::dir::DirRoot, crate::Error> {
1049    if sandbox {
1050        #[cfg(feature = "filesystem")]
1051        match mode {
1052            Mode::Dump(root) => {
1053                let target = root.join(path.with_extension("out").file_name().unwrap());
1054                let mut context = snapbox::dir::DirRoot::mutable_at(&target)?;
1055                if let Some(cwd) = cwd {
1056                    context = context.with_template(cwd)?;
1057                }
1058                Ok(context)
1059            }
1060            Mode::Fail | Mode::Overwrite => {
1061                let mut context = snapbox::dir::DirRoot::mutable_temp()?;
1062                if let Some(cwd) = cwd {
1063                    context = context.with_template(cwd)?;
1064                }
1065                Ok(context)
1066            }
1067        }
1068        #[cfg(not(feature = "filesystem"))]
1069        Err("Sandboxing is disabled".into())
1070    } else {
1071        Ok(cwd
1072            .map(snapbox::dir::DirRoot::immutable)
1073            .unwrap_or_else(snapbox::dir::DirRoot::none))
1074    }
1075}