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)] 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 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 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 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 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 None => {}
367 Some(crate::schema::Bin::Ignore) => {
368 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 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 } 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}