1use snapbox::{NormalizeNewlines, NormalizePaths};
6use std::collections::BTreeMap;
7use std::collections::VecDeque;
8
9pub type TryCmdLoader = fn(&std::path::Path) -> Result<TryCmd, crate::Error>;
11
12#[derive(Clone)]
14pub struct TryCmdLoaders(BTreeMap<std::ffi::OsString, TryCmdLoader>);
15
16impl std::fmt::Debug for TryCmdLoaders {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 let inner = f
21 .debug_map()
22 .entries(self.0.keys().map(|k| (k, "<function>")))
23 .finish()?;
24
25 f.debug_struct("TryCmdLoaders").field("0", &inner).finish()
26 }
27}
28
29impl Default for TryCmdLoaders {
30 fn default() -> Self {
31 let mut res = BTreeMap::new();
32
33 res.insert("toml".to_string().into(), TryCmd::load_toml as _);
34 res.insert("trycmd".to_string().into(), TryCmd::load_trycmd as _);
35 res.insert("md".to_string().into(), TryCmd::load_trycmd as _);
36
37 Self(res)
38 }
39}
40
41impl std::ops::Deref for TryCmdLoaders {
42 type Target =
43 BTreeMap<std::ffi::OsString, fn(&std::path::Path) -> Result<TryCmd, crate::Error>>;
44
45 fn deref(&self) -> &Self::Target {
46 &self.0
47 }
48}
49
50impl std::ops::DerefMut for TryCmdLoaders {
51 fn deref_mut(&mut self) -> &mut Self::Target {
52 &mut self.0
53 }
54}
55
56#[derive(Clone, Default, Debug, PartialEq, Eq)]
58pub struct TryCmd {
59 pub steps: Vec<Step>,
60 pub fs: Filesystem,
61}
62
63impl TryCmd {
64 pub fn load_toml(path: &std::path::Path) -> Result<Self, crate::Error> {
66 let raw = std::fs::read_to_string(path)
67 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
68 let one_shot = OneShot::parse_toml(&raw)?;
69 let mut sequence: Self = one_shot.into();
70 let is_binary = match sequence.steps[0].binary {
71 true => snapbox::DataFormat::Binary,
72 false => snapbox::DataFormat::Text,
73 };
74
75 if sequence.steps[0].stdin.is_none() {
76 let stdin_path = path.with_extension("stdin");
77 let stdin = if stdin_path.exists() {
78 Some(crate::Data::read_from(&stdin_path, Some(is_binary))?)
80 } else {
81 None
82 };
83 sequence.steps[0].stdin = stdin;
84 }
85
86 if sequence.steps[0].expected_stdout.is_none() {
87 let stdout_path = path.with_extension("stdout");
88 let stdout = if stdout_path.exists() {
89 Some(
90 crate::Data::read_from(&stdout_path, Some(is_binary))?
91 .normalize(NormalizePaths)
92 .normalize(NormalizeNewlines),
93 )
94 } else {
95 None
96 };
97 sequence.steps[0].expected_stdout = stdout;
98 }
99
100 if sequence.steps[0].expected_stderr.is_none() {
101 let stderr_path = path.with_extension("stderr");
102 let stderr = if stderr_path.exists() {
103 Some(
104 crate::Data::read_from(&stderr_path, Some(is_binary))?
105 .normalize(NormalizePaths)
106 .normalize(NormalizeNewlines),
107 )
108 } else {
109 None
110 };
111 sequence.steps[0].expected_stderr = stderr;
112 }
113
114 Ok(sequence)
115 }
116
117 pub fn load_trycmd(path: &std::path::Path) -> Result<Self, crate::Error> {
119 let raw = std::fs::read_to_string(path)
120 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
121 let normalized = snapbox::utils::normalize_lines(&raw);
122 Self::parse_trycmd(&normalized)
123 }
124
125 pub(crate) fn load(
127 loaders: &TryCmdLoaders,
128 path: &std::path::Path,
129 ) -> Result<Self, crate::Error> {
130 let mut sequence = if let Some(ext) = path.extension() {
131 let loader = loaders
132 .iter()
133 .find_map(|(x, loader)| if ext == x { Some(loader) } else { None });
134
135 if let Some(loader) = loader {
136 loader(path)?
137 } else {
138 return Err(format!("Unsupported extension: {}", ext.to_string_lossy()).into());
139 }
140 } else {
141 return Err("No extension".into());
142 };
143
144 sequence.fs.base = sequence.fs.base.take().map(|base| {
145 path.parent()
146 .unwrap_or_else(|| std::path::Path::new("."))
147 .join(base)
148 });
149 sequence.fs.cwd = sequence.fs.cwd.take().map(|cwd| {
150 path.parent()
151 .unwrap_or_else(|| std::path::Path::new("."))
152 .join(cwd)
153 });
154
155 if sequence.fs.base.is_none() {
156 let base_path = path.with_extension("in");
157 if base_path.exists() {
158 sequence.fs.base = Some(base_path);
159 } else if sequence.fs.cwd.is_some() {
160 sequence.fs.base = sequence.fs.cwd.clone();
161 }
162 }
163 if sequence.fs.cwd.is_none() {
164 sequence.fs.cwd = sequence.fs.base.clone();
165 }
166 if sequence.fs.sandbox.is_none() {
167 sequence.fs.sandbox = Some(path.with_extension("out").exists());
168 }
169
170 sequence.fs.base = sequence
171 .fs
172 .base
173 .take()
174 .map(|p| snapbox::path::resolve_dir(p).map_err(|e| e.to_string()))
175 .transpose()?;
176 sequence.fs.cwd = sequence
177 .fs
178 .cwd
179 .take()
180 .map(|p| snapbox::path::resolve_dir(p).map_err(|e| e.to_string()))
181 .transpose()?;
182
183 Ok(sequence)
184 }
185
186 pub(crate) fn overwrite(
187 &self,
188 path: &std::path::Path,
189 id: Option<&str>,
190 stdout: Option<&crate::Data>,
191 stderr: Option<&crate::Data>,
192 exit: Option<std::process::ExitStatus>,
193 ) -> Result<(), crate::Error> {
194 if let Some(ext) = path.extension() {
195 if ext == std::ffi::OsStr::new("toml") {
196 assert_eq!(id, None);
197
198 overwrite_toml_output(path, id, stdout, "stdout", "stdout")?;
199 overwrite_toml_output(path, id, stderr, "stderr", "stderr")?;
200
201 if let Some(status) = exit {
202 let raw = std::fs::read_to_string(path)
203 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
204 let overwritten = overwrite_toml_status(status, raw)
205 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
206 std::fs::write(path, overwritten)
207 .map_err(|e| format!("Failed to write {}: {}", path.display(), e))?;
208 }
209 } else if ext == std::ffi::OsStr::new("trycmd") || ext == std::ffi::OsStr::new("md") {
210 if stderr.is_some() && stderr != Some(&crate::Data::new()) {
211 panic!("stderr should have been merged: {:?}", stderr);
212 }
213 if let (Some(id), Some(stdout)) = (id, stdout) {
214 let step = self
215 .steps
216 .iter()
217 .find(|s| s.id.as_deref() == Some(id))
218 .expect("id is valid");
219 let mut line_nums = step
220 .expected_stdout_source
221 .clone()
222 .expect("always present for .trycmd");
223
224 let raw = std::fs::read_to_string(path)
225 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
226 let mut normalized = snapbox::utils::normalize_lines(&raw);
227
228 overwrite_trycmd_status(exit, step, &mut line_nums, &mut normalized)?;
229
230 let mut stdout = stdout.render().expect("at least Text");
231 stdout.push('\n');
233 replace_lines(&mut normalized, line_nums, &stdout)?;
234
235 std::fs::write(path, normalized.into_bytes())
236 .map_err(|e| format!("Failed to write {}: {}", path.display(), e))?;
237 }
238 } else {
239 return Err(format!("Unsupported extension: {}", ext.to_string_lossy()).into());
240 }
241 } else {
242 return Err("No extension".into());
243 }
244
245 Ok(())
246 }
247
248 fn parse_trycmd(s: &str) -> Result<Self, crate::Error> {
249 let mut steps = Vec::new();
250
251 let mut lines: VecDeque<_> = snapbox::utils::LinesWithTerminator::new(s)
252 .enumerate()
253 .map(|(i, l)| (i + 1, l))
254 .collect();
255 'outer: loop {
256 let mut fence_pattern = "```".to_owned();
257 while let Some((_, line)) = lines.pop_front() {
258 let tick_end = line
259 .char_indices()
260 .find_map(|(i, c)| (c != '`').then_some(i))
261 .unwrap_or(line.len());
262 if 3 <= tick_end {
263 fence_pattern = line[..tick_end].to_owned();
264 let raw = line[tick_end..].trim();
265 if raw.is_empty() {
266 break;
268 } else {
269 let mut info = raw.split(',');
270 let lang = info.next().unwrap();
271 match lang {
272 "trycmd" | "console" => {
273 if info.any(|i| i == "ignore") {
274 snapbox::debug!("ignore from infostring: {:?}", info);
275 } else {
276 break;
277 }
278 }
279 _ => {
280 snapbox::debug!("ignore from lang: {:?}", lang);
281 }
282 }
283 }
284
285 while let Some((_, line)) = lines.pop_front() {
287 if line.starts_with(&fence_pattern) {
288 continue 'outer;
289 }
290 }
291 }
292 }
293
294 'code: loop {
295 let mut cmdline = Vec::new();
296 let mut expected_status_source = None;
297 let mut expected_status = Some(CommandStatus::Success);
298 let mut stdout = String::new();
299 let cmd_start;
300 let mut stdout_start;
301
302 if let Some((line_num, line)) = lines.pop_front() {
303 if line.starts_with(&fence_pattern) {
304 break;
305 } else if let Some(raw) = line.strip_prefix("$ ") {
306 cmdline.extend(shlex::Shlex::new(raw.trim()));
307 cmd_start = line_num;
308 stdout_start = line_num + 1;
309 } else {
310 return Err(
311 format!("Expected `$` on line {}, got `{}`", line_num, line).into()
312 );
313 }
314 } else {
315 break 'outer;
316 }
317 while let Some((line_num, line)) = lines.pop_front() {
318 if let Some(raw) = line.strip_prefix("> ") {
319 cmdline.extend(shlex::Shlex::new(raw.trim()));
320 stdout_start = line_num + 1;
321 } else {
322 lines.push_front((line_num, line));
323 break;
324 }
325 }
326 if let Some((line_num, line)) = lines.pop_front() {
327 if let Some(raw) = line.strip_prefix("? ") {
328 expected_status_source = Some(line_num);
329 expected_status = Some(raw.trim().parse::<CommandStatus>()?);
330 stdout_start = line_num + 1;
331 } else {
332 lines.push_front((line_num, line));
333 }
334 }
335 let mut post_stdout_start = stdout_start;
336 let mut block_done = false;
337 while let Some((line_num, line)) = lines.pop_front() {
338 if line.starts_with("$ ") {
339 lines.push_front((line_num, line));
340 post_stdout_start = line_num;
341 break;
342 } else if line.starts_with(&fence_pattern) {
343 block_done = true;
344 post_stdout_start = line_num;
345 break;
346 } else {
347 stdout.push_str(line);
348 post_stdout_start = line_num + 1;
349 }
350 }
351 if stdout.ends_with('\n') {
352 stdout.pop();
355 }
356
357 let mut env = Env::default();
358
359 let bin = loop {
360 if cmdline.is_empty() {
361 return Err(format!("No bin specified on line {}", cmd_start).into());
362 }
363 let next = cmdline.remove(0);
364 if let Some((key, value)) = next.split_once('=') {
365 env.add.insert(key.to_owned(), value.to_owned());
366 } else {
367 break next;
368 }
369 };
370 let step = Step {
371 id: Some(cmd_start.to_string()),
372 bin: Some(Bin::Name(bin)),
373 args: cmdline,
374 env,
375 stdin: None,
376 stderr_to_stdout: true,
377 expected_status_source,
378 expected_status,
379 expected_stdout_source: Some(stdout_start..post_stdout_start),
380 expected_stdout: Some(crate::Data::text(stdout)),
381 expected_stderr_source: None,
382 expected_stderr: None,
383 binary: false,
384 timeout: None,
385 };
386 steps.push(step);
387 if block_done {
388 break 'code;
389 }
390 }
391 }
392
393 Ok(Self {
394 steps,
395 ..Default::default()
396 })
397 }
398}
399
400fn overwrite_toml_output(
401 path: &std::path::Path,
402 _id: Option<&str>,
403 output: Option<&crate::Data>,
404 output_ext: &str,
405 output_field: &str,
406) -> Result<(), crate::Error> {
407 if let Some(output) = output {
408 let output_path = path.with_extension(output_ext);
409 if output_path.exists() {
410 output.write_to(&output_path)?;
411 } else if let Some(output) = output.render() {
412 let raw = std::fs::read_to_string(path)
413 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
414 let mut doc = raw
415 .parse::<toml_edit::Document>()
416 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
417 if let Some(output_value) = doc.get_mut(output_field) {
418 *output_value = toml_edit::value(output);
419 }
420 std::fs::write(path, doc.to_string())
421 .map_err(|e| format!("Failed to write {}: {}", path.display(), e))?;
422 } else {
423 output.write_to(&output_path)?;
424
425 let raw = std::fs::read_to_string(path)
426 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
427 let mut doc = raw
428 .parse::<toml_edit::Document>()
429 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
430 doc[output_field] = toml_edit::Item::None;
431 std::fs::write(path, doc.to_string())
432 .map_err(|e| format!("Failed to write {}: {}", path.display(), e))?;
433 }
434 }
435
436 Ok(())
437}
438
439fn overwrite_toml_status(
440 status: std::process::ExitStatus,
441 raw: String,
442) -> Result<String, toml_edit::TomlError> {
443 let mut doc = raw.parse::<toml_edit::Document>()?;
444 if let Some(code) = status.code() {
445 if status.success() {
446 match doc.get("status") {
447 Some(toml_edit::Item::Value(toml_edit::Value::String(ref expected)))
448 if expected.value() == "success" => {}
449 Some(
450 toml_edit::Item::Value(toml_edit::Value::InlineTable(_))
451 | toml_edit::Item::Table(_),
452 ) => {
453 if !matches!(
454 doc["status"].get("code"),
455 Some(toml_edit::Item::Value(toml_edit::Value::Integer(ref expected)))
456 if expected.value() == &0)
457 {
458 doc["status"] = toml_edit::Item::None;
460 }
461 }
462 _ => {
463 doc["status"] = toml_edit::Item::None;
465 }
466 }
467 } else {
468 let code = code as i64;
469 match doc.get("status") {
470 Some(toml_edit::Item::Value(toml_edit::Value::String(ref expected))) => {
471 if expected.value() != "failed" {
472 doc["status"] = toml_edit::value("failed");
473 }
474 }
475 Some(
476 toml_edit::Item::Value(toml_edit::Value::InlineTable(_))
477 | toml_edit::Item::Table(_),
478 ) => {
479 if !matches!(
480 doc["status"].get("code"),
481 Some(toml_edit::Item::Value(toml_edit::Value::Integer(ref expected)))
482 if expected.value() == &code)
483 {
484 doc["status"]["code"] = toml_edit::value(code);
485 }
486 }
487 _ => {
488 let mut status = toml_edit::InlineTable::default();
489 status.set_dotted(true);
490 status.insert("code", code.into());
491 doc["status"] = toml_edit::value(status);
492 }
493 }
494 }
495 } else if !matches!(
496 doc.get("status"),
497 Some(toml_edit::Item::Value(toml_edit::Value::String(ref expected)))
498 if expected.value() == "interrupted")
499 {
500 doc["status"] = toml_edit::value("interrupted");
501 }
502
503 Ok(doc.to_string())
504}
505
506fn overwrite_trycmd_status(
507 exit: Option<std::process::ExitStatus>,
508 step: &Step,
509 stdout_line_nums: &mut std::ops::Range<usize>,
510 normalized: &mut String,
511) -> Result<(), snapbox::Error> {
512 let status = match exit {
513 Some(status) => status,
514 _ => {
515 return Ok(());
516 }
517 };
518
519 let formatted_status = if let Some(code) = status.code() {
520 if status.success() {
521 if let (true, Some(line_num)) = (
522 step.expected_status != Some(CommandStatus::Success),
523 step.expected_status_source,
524 ) {
525 replace_lines(normalized, line_num..(line_num + 1), "")?;
526 *stdout_line_nums = (stdout_line_nums.start - 1)..(stdout_line_nums.end - 1);
527 }
528 None
529 } else {
530 match step.expected_status {
531 Some(CommandStatus::Success | CommandStatus::Interrupted) => {
532 Some(format!("? {code}"))
533 }
534 Some(CommandStatus::Code(expected)) if expected != code => {
535 Some(format!("? {code}"))
536 }
537 _ => None,
538 }
539 }
540 } else {
541 if step.expected_status == Some(CommandStatus::Interrupted) {
542 None
543 } else {
544 Some("? interrupted".into())
545 }
546 };
547
548 if let Some(status) = formatted_status {
549 if let Some(line_num) = step.expected_status_source {
550 replace_lines(normalized, line_num..(line_num + 1), &status)?;
551 } else {
552 let line_num = stdout_line_nums.start;
553 replace_lines(normalized, line_num..line_num, &status)?;
554 *stdout_line_nums = (line_num + 1)..(stdout_line_nums.end + 1);
555 }
556 }
557
558 Ok(())
559}
560
561fn replace_lines(
563 data: &mut String,
564 line_nums: std::ops::Range<usize>,
565 text: &str,
566) -> Result<(), crate::Error> {
567 let mut output_lines = String::new();
568
569 for (line_num, line) in snapbox::utils::LinesWithTerminator::new(data)
570 .enumerate()
571 .map(|(i, l)| (i + 1, l))
572 {
573 if line_num == line_nums.start {
574 output_lines.push_str(text);
575 if !text.is_empty() && !text.ends_with('\n') {
576 output_lines.push('\n');
577 }
578 }
579 if !line_nums.contains(&line_num) {
580 output_lines.push_str(line);
581 }
582 }
583
584 *data = output_lines;
585 Ok(())
586}
587
588impl std::str::FromStr for TryCmd {
589 type Err = crate::Error;
590
591 fn from_str(s: &str) -> Result<Self, Self::Err> {
592 Self::parse_trycmd(s)
593 }
594}
595
596impl From<OneShot> for TryCmd {
597 fn from(other: OneShot) -> Self {
598 let OneShot {
599 bin,
600 args,
601 env,
602 stdin,
603 stdout,
604 stderr,
605 stderr_to_stdout,
606 status,
607 binary,
608 timeout,
609 fs,
610 } = other;
611 Self {
612 steps: vec![Step {
613 id: None,
614 bin,
615 args: args.into_vec(),
616 env,
617 stdin: stdin.map(crate::Data::text),
618 stderr_to_stdout,
619 expected_status_source: None,
620 expected_status: status,
621 expected_stdout_source: None,
622 expected_stdout: stdout.map(crate::Data::text),
623 expected_stderr_source: None,
624 expected_stderr: stderr.map(crate::Data::text),
625 binary,
626 timeout,
627 }],
628 fs,
629 }
630 }
631}
632
633#[derive(Clone, Default, Debug, PartialEq, Eq)]
635pub struct Step {
636 pub id: Option<String>,
638 pub bin: Option<Bin>,
640 pub args: Vec<String>,
642 pub env: Env,
644 pub stdin: Option<crate::Data>,
646 pub stderr_to_stdout: bool,
648 pub expected_status_source: Option<usize>,
649 pub expected_status: Option<CommandStatus>,
651 pub expected_stdout_source: Option<std::ops::Range<usize>>,
652 pub expected_stdout: Option<crate::Data>,
654 pub expected_stderr_source: Option<std::ops::Range<usize>>,
655 pub expected_stderr: Option<crate::Data>,
657 pub binary: bool,
659 pub timeout: Option<std::time::Duration>,
661}
662
663impl Step {
664 pub(crate) fn to_command(
665 &self,
666 cwd: Option<&std::path::Path>,
667 ) -> Result<snapbox::cmd::Command, crate::Error> {
668 let bin = match &self.bin {
669 Some(Bin::Path(path)) => Ok(path.clone()),
670 Some(Bin::Name(name)) => Err(format!("Unknown bin.name = {}", name).into()),
671 Some(Bin::Ignore) => Err("Internal error: tried to run an ignored bin".into()),
672 Some(Bin::Error(err)) => Err(err.clone()),
673 None => Err("No bin specified".into()),
674 }?;
675 if !bin.exists() {
676 return Err(format!("Bin doesn't exist: {}", bin.display()).into());
677 }
678
679 let mut cmd = snapbox::cmd::Command::new(bin).args(&self.args);
680 if let Some(cwd) = cwd {
681 cmd = cmd.current_dir(cwd);
682 }
683 if let Some(stdin) = &self.stdin {
684 cmd = cmd.stdin(stdin);
685 }
686 if self.stderr_to_stdout {
687 cmd = cmd.stderr_to_stdout();
688 }
689 if let Some(timeout) = self.timeout {
690 cmd = cmd.timeout(timeout)
691 }
692 cmd = self.env.apply(cmd);
693
694 Ok(cmd)
695 }
696
697 pub(crate) fn expected_status(&self) -> CommandStatus {
698 self.expected_status.unwrap_or_default()
699 }
700}
701
702#[derive(Clone, Default, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
704#[serde(rename_all = "kebab-case")]
705#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
706pub struct OneShot {
707 pub(crate) bin: Option<Bin>,
708 #[serde(default)]
709 pub(crate) args: Args,
710 #[serde(default)]
711 pub(crate) env: Env,
712 #[serde(default)]
713 pub(crate) stdin: Option<String>,
714 #[serde(default)]
715 pub(crate) stdout: Option<String>,
716 #[serde(default)]
717 pub(crate) stderr: Option<String>,
718 #[serde(default)]
719 pub(crate) stderr_to_stdout: bool,
720 pub(crate) status: Option<CommandStatus>,
721 #[serde(default)]
722 pub(crate) binary: bool,
723 #[serde(default)]
724 #[serde(deserialize_with = "humantime_serde::deserialize")]
725 pub(crate) timeout: Option<std::time::Duration>,
726 #[serde(default)]
727 pub(crate) fs: Filesystem,
728}
729
730impl OneShot {
731 fn parse_toml(s: &str) -> Result<Self, crate::Error> {
732 toml_edit::de::from_str(s).map_err(|e| e.to_string().into())
733 }
734}
735
736#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
737#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
738#[serde(untagged)]
739pub(crate) enum Args {
740 Joined(JoinedArgs),
741 Split(Vec<String>),
742}
743
744impl Args {
745 fn new() -> Self {
746 Self::Split(Default::default())
747 }
748
749 fn as_slice(&self) -> &[String] {
750 match self {
751 Self::Joined(j) => j.inner.as_slice(),
752 Self::Split(v) => v.as_slice(),
753 }
754 }
755
756 fn into_vec(self) -> Vec<String> {
757 match self {
758 Self::Joined(j) => j.inner,
759 Self::Split(v) => v,
760 }
761 }
762}
763
764impl Default for Args {
765 fn default() -> Self {
766 Self::new()
767 }
768}
769
770impl std::ops::Deref for Args {
771 type Target = [String];
772
773 fn deref(&self) -> &Self::Target {
774 self.as_slice()
775 }
776}
777
778#[derive(Clone, Default, Debug, PartialEq, Eq)]
779#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
780pub(crate) struct JoinedArgs {
781 inner: Vec<String>,
782}
783
784impl JoinedArgs {
785 #[cfg(test)]
786 pub(crate) fn from_vec(inner: Vec<String>) -> Self {
787 JoinedArgs { inner }
788 }
789
790 #[allow(clippy::inherent_to_string_shadow_display)]
791 fn to_string(&self) -> String {
792 shlex::join(self.inner.iter().map(|s| s.as_str()))
793 }
794}
795
796impl std::str::FromStr for JoinedArgs {
797 type Err = std::convert::Infallible;
798
799 fn from_str(s: &str) -> Result<Self, Self::Err> {
800 let inner = shlex::Shlex::new(s).collect();
801 Ok(Self { inner })
802 }
803}
804
805impl std::fmt::Display for JoinedArgs {
806 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
807 self.to_string().fmt(f)
808 }
809}
810
811impl<'de> serde::de::Deserialize<'de> for JoinedArgs {
812 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
813 where
814 D: serde::de::Deserializer<'de>,
815 {
816 let s = String::deserialize(deserializer)?;
817 std::str::FromStr::from_str(&s).map_err(serde::de::Error::custom)
818 }
819}
820
821impl serde::ser::Serialize for JoinedArgs {
822 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
823 where
824 S: serde::ser::Serializer,
825 {
826 serializer.serialize_str(&self.to_string())
827 }
828}
829
830#[derive(Clone, Default, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
832#[serde(rename_all = "kebab-case")]
833#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
834pub struct Filesystem {
835 pub cwd: Option<std::path::PathBuf>,
837 pub base: Option<std::path::PathBuf>,
839 pub sandbox: Option<bool>,
841}
842
843impl Filesystem {
844 pub(crate) fn sandbox(&self) -> bool {
845 self.sandbox.unwrap_or_default()
846 }
847
848 pub(crate) fn rel_cwd(&self) -> Result<&std::path::Path, crate::Error> {
849 if let (Some(orig_cwd), Some(orig_base)) = (self.cwd.as_deref(), self.base.as_deref()) {
850 let rel_cwd = orig_cwd.strip_prefix(orig_base).map_err(|_| {
851 crate::Error::new(format!(
852 "fs.cwd ({}) must be within fs.base ({})",
853 orig_cwd.display(),
854 orig_base.display()
855 ))
856 })?;
857 Ok(rel_cwd)
858 } else {
859 Ok(std::path::Path::new(""))
860 }
861 }
862}
863
864#[derive(Clone, Default, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
866#[serde(rename_all = "kebab-case")]
867#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
868pub struct Env {
869 #[serde(default)]
870 pub inherit: Option<bool>,
871 #[serde(default)]
872 pub add: BTreeMap<String, String>,
873 #[serde(default)]
874 pub remove: Vec<String>,
875}
876
877impl Env {
878 pub fn update(&mut self, other: &Self) {
879 if self.inherit.is_none() {
880 self.inherit = other.inherit;
881 }
882 self.add
883 .extend(other.add.iter().map(|(k, v)| (k.clone(), v.clone())));
884 self.remove.extend(other.remove.iter().cloned());
885 }
886
887 pub(crate) fn apply(&self, mut command: snapbox::cmd::Command) -> snapbox::cmd::Command {
888 if !self.inherit() {
889 command = command.env_clear();
890 }
891 for remove in &self.remove {
892 command = command.env_remove(remove);
893 }
894 command.envs(&self.add)
895 }
896
897 pub(crate) fn inherit(&self) -> bool {
898 self.inherit.unwrap_or(true)
899 }
900}
901
902#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
904#[serde(rename_all = "kebab-case")]
905#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
906pub enum Bin {
907 Path(std::path::PathBuf),
908 Name(String),
909 Ignore,
910 #[serde(skip)]
911 Error(crate::Error),
912}
913
914impl From<std::path::PathBuf> for Bin {
915 fn from(other: std::path::PathBuf) -> Self {
916 Self::Path(other)
917 }
918}
919
920impl<'a> From<&'a std::path::PathBuf> for Bin {
921 fn from(other: &'a std::path::PathBuf) -> Self {
922 Self::Path(other.clone())
923 }
924}
925
926impl<'a> From<&'a std::path::Path> for Bin {
927 fn from(other: &'a std::path::Path) -> Self {
928 Self::Path(other.to_owned())
929 }
930}
931
932impl<P, E> From<Result<P, E>> for Bin
933where
934 P: Into<Bin>,
935 E: std::fmt::Display,
936{
937 fn from(other: Result<P, E>) -> Self {
938 match other {
939 Ok(path) => path.into(),
940 Err(err) => {
941 let err = crate::Error::new(err.to_string());
942 Bin::Error(err)
943 }
944 }
945 }
946}
947
948#[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
950#[serde(rename_all = "kebab-case")]
951#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
952#[derive(Default)]
953pub enum CommandStatus {
954 #[default]
955 Success,
956 Failed,
957 Interrupted,
958 Skipped,
959 Code(i32),
960}
961
962impl std::str::FromStr for CommandStatus {
963 type Err = crate::Error;
964
965 fn from_str(s: &str) -> Result<Self, Self::Err> {
966 match s {
967 "success" => Ok(Self::Success),
968 "failed" => Ok(Self::Failed),
969 "interrupted" => Ok(Self::Interrupted),
970 "skipped" => Ok(Self::Skipped),
971 _ => s
972 .parse::<i32>()
973 .map(Self::Code)
974 .map_err(|_| crate::Error::new(format!("Expected an exit code, got {}", s))),
975 }
976 }
977}
978
979#[cfg(test)]
980mod test {
981 use super::*;
982
983 #[test]
984 fn parse_trycmd_empty() {
985 let expected = TryCmd {
986 steps: vec![],
987 ..Default::default()
988 };
989 let actual = TryCmd::parse_trycmd("").unwrap();
990 assert_eq!(expected, actual);
991 }
992
993 #[test]
994 fn parse_trycmd_empty_fence() {
995 let expected = TryCmd {
996 steps: vec![],
997 ..Default::default()
998 };
999 let actual = TryCmd::parse_trycmd(
1000 "
1001```
1002```
1003",
1004 )
1005 .unwrap();
1006 assert_eq!(expected, actual);
1007 }
1008
1009 #[test]
1010 fn parse_trycmd_command() {
1011 let expected = TryCmd {
1012 steps: vec![Step {
1013 id: Some("3".into()),
1014 bin: Some(Bin::Name("cmd".into())),
1015 expected_status: Some(CommandStatus::Success),
1016 stderr_to_stdout: true,
1017 expected_stdout_source: Some(4..4),
1018 expected_stdout: Some(crate::Data::new()),
1019 expected_stderr: None,
1020 ..Default::default()
1021 }],
1022 ..Default::default()
1023 };
1024 let actual = TryCmd::parse_trycmd(
1025 "
1026```
1027$ cmd
1028```
1029",
1030 )
1031 .unwrap();
1032 assert_eq!(expected, actual);
1033 }
1034
1035 #[test]
1036 fn parse_trycmd_command_line() {
1037 let expected = TryCmd {
1038 steps: vec![Step {
1039 id: Some("3".into()),
1040 bin: Some(Bin::Name("cmd".into())),
1041 args: vec!["arg1".into(), "arg with space".into()],
1042 expected_status: Some(CommandStatus::Success),
1043 stderr_to_stdout: true,
1044 expected_stdout_source: Some(4..4),
1045 expected_stdout: Some(crate::Data::new()),
1046 expected_stderr: None,
1047 ..Default::default()
1048 }],
1049 ..Default::default()
1050 };
1051 let actual = TryCmd::parse_trycmd(
1052 "
1053```
1054$ cmd arg1 'arg with space'
1055```
1056",
1057 )
1058 .unwrap();
1059 assert_eq!(expected, actual);
1060 }
1061
1062 #[test]
1063 fn parse_trycmd_multi_line() {
1064 let expected = TryCmd {
1065 steps: vec![Step {
1066 id: Some("3".into()),
1067 bin: Some(Bin::Name("cmd".into())),
1068 args: vec!["arg1".into(), "arg with space".into()],
1069 expected_status: Some(CommandStatus::Success),
1070 stderr_to_stdout: true,
1071 expected_stdout_source: Some(5..5),
1072 expected_stdout: Some(crate::Data::new()),
1073 expected_stderr: None,
1074 ..Default::default()
1075 }],
1076 ..Default::default()
1077 };
1078 let actual = TryCmd::parse_trycmd(
1079 "
1080```
1081$ cmd arg1
1082> 'arg with space'
1083```
1084",
1085 )
1086 .unwrap();
1087 assert_eq!(expected, actual);
1088 }
1089
1090 #[test]
1091 fn parse_trycmd_env() {
1092 let expected = TryCmd {
1093 steps: vec![Step {
1094 id: Some("3".into()),
1095 bin: Some(Bin::Name("cmd".into())),
1096 env: Env {
1097 add: IntoIterator::into_iter([
1098 ("KEY1".into(), "VALUE1".into()),
1099 ("KEY2".into(), "VALUE2 with space".into()),
1100 ])
1101 .collect(),
1102 ..Default::default()
1103 },
1104 expected_status: Some(CommandStatus::Success),
1105 stderr_to_stdout: true,
1106 expected_stdout_source: Some(4..4),
1107 expected_stdout: Some(crate::Data::new()),
1108 expected_stderr: None,
1109 ..Default::default()
1110 }],
1111 ..Default::default()
1112 };
1113 let actual = TryCmd::parse_trycmd(
1114 "
1115```
1116$ KEY1=VALUE1 KEY2='VALUE2 with space' cmd
1117```
1118",
1119 )
1120 .unwrap();
1121 assert_eq!(expected, actual);
1122 }
1123
1124 #[test]
1125 fn parse_trycmd_status() {
1126 let expected = TryCmd {
1127 steps: vec![Step {
1128 id: Some("3".into()),
1129 bin: Some(Bin::Name("cmd".into())),
1130 expected_status_source: Some(4),
1131 expected_status: Some(CommandStatus::Skipped),
1132 stderr_to_stdout: true,
1133 expected_stdout_source: Some(5..5),
1134 expected_stdout: Some(crate::Data::new()),
1135 expected_stderr: None,
1136 ..Default::default()
1137 }],
1138 ..Default::default()
1139 };
1140 let actual = TryCmd::parse_trycmd(
1141 "
1142```
1143$ cmd
1144? skipped
1145```
1146",
1147 )
1148 .unwrap();
1149 assert_eq!(expected, actual);
1150 }
1151
1152 #[test]
1153 fn parse_trycmd_status_code() {
1154 let expected = TryCmd {
1155 steps: vec![Step {
1156 id: Some("3".into()),
1157 bin: Some(Bin::Name("cmd".into())),
1158 expected_status_source: Some(4),
1159 expected_status: Some(CommandStatus::Code(-1)),
1160 stderr_to_stdout: true,
1161 expected_stdout_source: Some(5..5),
1162 expected_stdout: Some(crate::Data::new()),
1163 expected_stderr: None,
1164 ..Default::default()
1165 }],
1166 ..Default::default()
1167 };
1168 let actual = TryCmd::parse_trycmd(
1169 "
1170```
1171$ cmd
1172? -1
1173```
1174",
1175 )
1176 .unwrap();
1177 assert_eq!(expected, actual);
1178 }
1179
1180 #[test]
1181 fn parse_trycmd_stdout() {
1182 let expected = TryCmd {
1183 steps: vec![Step {
1184 id: Some("3".into()),
1185 bin: Some(Bin::Name("cmd".into())),
1186 expected_status: Some(CommandStatus::Success),
1187 stderr_to_stdout: true,
1188 expected_stdout_source: Some(4..6),
1189 expected_stdout: Some(crate::Data::text("Hello World\n")),
1190 expected_stderr: None,
1191 ..Default::default()
1192 }],
1193 ..Default::default()
1194 };
1195 let actual = TryCmd::parse_trycmd(
1196 "
1197```
1198$ cmd
1199Hello World
1200
1201```",
1202 )
1203 .unwrap();
1204 assert_eq!(expected, actual);
1205 }
1206
1207 #[test]
1208 fn parse_trycmd_escaped_stdout() {
1209 let expected = TryCmd {
1210 steps: vec![Step {
1211 id: Some("3".into()),
1212 bin: Some(Bin::Name("cmd".into())),
1213 expected_status: Some(CommandStatus::Success),
1214 stderr_to_stdout: true,
1215 expected_stdout_source: Some(4..7),
1216 expected_stdout: Some(crate::Data::text("```\nHello World\n```")),
1217 expected_stderr: None,
1218 ..Default::default()
1219 }],
1220 ..Default::default()
1221 };
1222 let actual = TryCmd::parse_trycmd(
1223 "
1224````
1225$ cmd
1226```
1227Hello World
1228```
1229````",
1230 )
1231 .unwrap();
1232 assert_eq!(expected, actual);
1233 }
1234
1235 #[test]
1236 fn parse_trycmd_multi_step() {
1237 let expected = TryCmd {
1238 steps: vec![
1239 Step {
1240 id: Some("3".into()),
1241 bin: Some(Bin::Name("cmd1".into())),
1242 expected_status_source: Some(4),
1243 expected_status: Some(CommandStatus::Code(1)),
1244 stderr_to_stdout: true,
1245 expected_stdout_source: Some(5..5),
1246 expected_stdout: Some(crate::Data::new()),
1247 expected_stderr: None,
1248 ..Default::default()
1249 },
1250 Step {
1251 id: Some("5".into()),
1252 bin: Some(Bin::Name("cmd2".into())),
1253 expected_status: Some(CommandStatus::Success),
1254 stderr_to_stdout: true,
1255 expected_stdout_source: Some(6..6),
1256 expected_stdout: Some(crate::Data::new()),
1257 expected_stderr: None,
1258 ..Default::default()
1259 },
1260 ],
1261 ..Default::default()
1262 };
1263 let actual = TryCmd::parse_trycmd(
1264 "
1265```
1266$ cmd1
1267? 1
1268$ cmd2
1269```
1270",
1271 )
1272 .unwrap();
1273 assert_eq!(expected, actual);
1274 }
1275
1276 #[test]
1277 fn parse_trycmd_info_string() {
1278 let expected = TryCmd {
1279 steps: vec![
1280 Step {
1281 id: Some("3".into()),
1282 bin: Some(Bin::Name("bare-cmd".into())),
1283 expected_status_source: Some(4),
1284 expected_status: Some(CommandStatus::Code(1)),
1285 stderr_to_stdout: true,
1286 expected_stdout_source: Some(5..5),
1287 expected_stdout: Some(crate::Data::new()),
1288 expected_stderr: None,
1289 ..Default::default()
1290 },
1291 Step {
1292 id: Some("8".into()),
1293 bin: Some(Bin::Name("trycmd-cmd".into())),
1294 expected_status_source: Some(9),
1295 expected_status: Some(CommandStatus::Code(1)),
1296 stderr_to_stdout: true,
1297 expected_stdout_source: Some(10..10),
1298 expected_stdout: Some(crate::Data::new()),
1299 expected_stderr: None,
1300 ..Default::default()
1301 },
1302 Step {
1303 id: Some("18".into()),
1304 bin: Some(Bin::Name("console-cmd".into())),
1305 expected_status_source: Some(19),
1306 expected_status: Some(CommandStatus::Code(1)),
1307 stderr_to_stdout: true,
1308 expected_stdout_source: Some(20..20),
1309 expected_stdout: Some(crate::Data::new()),
1310 expected_stderr: None,
1311 ..Default::default()
1312 },
1313 ],
1314 ..Default::default()
1315 };
1316 let actual = TryCmd::parse_trycmd(
1317 "
1318```
1319$ bare-cmd
1320? 1
1321```
1322
1323```trycmd
1324$ trycmd-cmd
1325? 1
1326```
1327
1328```sh
1329$ sh-cmd
1330? 1
1331```
1332
1333```console
1334$ console-cmd
1335? 1
1336```
1337
1338```ignore
1339$ rust-cmd1
1340? 1
1341```
1342
1343```trycmd,ignore
1344$ rust-cmd1
1345? 1
1346```
1347
1348```rust
1349$ rust-cmd1
1350? 1
1351```
1352",
1353 )
1354 .unwrap();
1355 assert_eq!(expected, actual);
1356 }
1357
1358 #[test]
1359 fn parse_toml_minimal() {
1360 let expected = OneShot {
1361 ..Default::default()
1362 };
1363 let actual = OneShot::parse_toml("").unwrap();
1364 assert_eq!(expected, actual);
1365 }
1366
1367 #[test]
1368 fn parse_toml_minimal_env() {
1369 let expected = OneShot {
1370 ..Default::default()
1371 };
1372 let actual = OneShot::parse_toml("[env]").unwrap();
1373 assert_eq!(expected, actual);
1374 }
1375
1376 #[test]
1377 fn parse_toml_bin_name() {
1378 let expected = OneShot {
1379 bin: Some(Bin::Name("cmd".into())),
1380 ..Default::default()
1381 };
1382 let actual = OneShot::parse_toml("bin.name = 'cmd'").unwrap();
1383 assert_eq!(expected, actual);
1384 }
1385
1386 #[test]
1387 fn parse_toml_bin_path() {
1388 let expected = OneShot {
1389 bin: Some(Bin::Path("/usr/bin/cmd".into())),
1390 ..Default::default()
1391 };
1392 let actual = OneShot::parse_toml("bin.path = '/usr/bin/cmd'").unwrap();
1393 assert_eq!(expected, actual);
1394 }
1395
1396 #[test]
1397 fn parse_toml_args_split() {
1398 let expected = OneShot {
1399 args: Args::Split(vec!["arg1".into(), "arg with space".into()]),
1400 ..Default::default()
1401 };
1402 let actual = OneShot::parse_toml(r#"args = ["arg1", "arg with space"]"#).unwrap();
1403 assert_eq!(expected, actual);
1404 }
1405
1406 #[test]
1407 fn parse_toml_args_joined() {
1408 let expected = OneShot {
1409 args: Args::Joined(JoinedArgs::from_vec(vec![
1410 "arg1".into(),
1411 "arg with space".into(),
1412 ])),
1413 ..Default::default()
1414 };
1415 let actual = OneShot::parse_toml(r#"args = "arg1 'arg with space'""#).unwrap();
1416 assert_eq!(expected, actual);
1417 }
1418
1419 #[test]
1420 fn parse_toml_status_success() {
1421 let expected = OneShot {
1422 status: Some(CommandStatus::Success),
1423 ..Default::default()
1424 };
1425 let actual = OneShot::parse_toml("status = 'success'").unwrap();
1426 assert_eq!(expected, actual);
1427 }
1428
1429 #[test]
1430 fn parse_toml_status_code() {
1431 let expected = OneShot {
1432 status: Some(CommandStatus::Code(42)),
1433 ..Default::default()
1434 };
1435 let actual = OneShot::parse_toml("status.code = 42").unwrap();
1436 assert_eq!(expected, actual);
1437 }
1438
1439 #[test]
1440 fn replace_lines_same_line_count() {
1441 let input = "One\nTwo\nThree";
1442 let line_nums = 2..3;
1443 let replacement = "World\n";
1444 let expected = "One\nWorld\nThree";
1445
1446 let mut actual = input.to_owned();
1447 replace_lines(&mut actual, line_nums, replacement).unwrap();
1448 assert_eq!(expected, actual);
1449 }
1450
1451 #[test]
1452 fn replace_lines_grow() {
1453 let input = "One\nTwo\nThree";
1454 let line_nums = 2..3;
1455 let replacement = "World\nTrees\n";
1456 let expected = "One\nWorld\nTrees\nThree";
1457
1458 let mut actual = input.to_owned();
1459 replace_lines(&mut actual, line_nums, replacement).unwrap();
1460 assert_eq!(expected, actual);
1461 }
1462
1463 #[test]
1464 fn replace_lines_shrink() {
1465 let input = "One\nTwo\nThree";
1466 let line_nums = 2..3;
1467 let replacement = "";
1468 let expected = "One\nThree";
1469
1470 let mut actual = input.to_owned();
1471 replace_lines(&mut actual, line_nums, replacement).unwrap();
1472 assert_eq!(expected, actual);
1473 }
1474
1475 #[test]
1476 fn replace_lines_no_trailing() {
1477 let input = "One\nTwo\nThree";
1478 let line_nums = 2..3;
1479 let replacement = "World";
1480 let expected = "One\nWorld\nThree";
1481
1482 let mut actual = input.to_owned();
1483 replace_lines(&mut actual, line_nums, replacement).unwrap();
1484 assert_eq!(expected, actual);
1485 }
1486
1487 #[test]
1488 fn replace_lines_empty_range() {
1489 let input = "One\nTwo\nThree";
1490 let line_nums = 2..2;
1491 let replacement = "World\n";
1492 let expected = "One\nWorld\nTwo\nThree";
1493
1494 let mut actual = input.to_owned();
1495 replace_lines(&mut actual, line_nums, replacement).unwrap();
1496 assert_eq!(expected, actual);
1497 }
1498
1499 #[test]
1500 fn overwrite_toml_status_success() {
1501 let expected = r#"
1502bin.name = "cmd"
1503"#;
1504 let actual = overwrite_toml_status(
1505 exit_code_to_status(0),
1506 r#"
1507bin.name = "cmd"
1508status = "failed"
1509"#
1510 .into(),
1511 )
1512 .unwrap();
1513 assert_eq!(expected, actual);
1514 }
1515
1516 #[test]
1517 fn overwrite_toml_status_failed() {
1518 let expected = r#"
1519bin.name = "cmd"
1520status.code = 1
1521"#;
1522 let actual = overwrite_toml_status(
1523 exit_code_to_status(1),
1524 r#"
1525bin.name = "cmd"
1526"#
1527 .into(),
1528 )
1529 .unwrap();
1530 assert_eq!(expected, actual);
1531 }
1532
1533 #[test]
1534 fn overwrite_toml_status_keeps_style() {
1535 let expected = r#"
1536bin.name = "cmd"
1537status = { code = 1 } # comment
1538"#;
1539 let actual = overwrite_toml_status(
1540 exit_code_to_status(1),
1541 r#"
1542bin.name = "cmd"
1543status = { code = 2 } # comment
1544"#
1545 .into(),
1546 )
1547 .unwrap();
1548 assert_eq!(expected, actual);
1549 }
1550
1551 #[test]
1552 fn overwrite_trycmd_status_success() {
1553 let expected = r#"
1554```
1555$ cmd arg
1556foo
1557bar
1558```
1559"#;
1560
1561 let mut actual = r"
1562```
1563$ cmd arg
1564? failed
1565foo
1566bar
1567```
1568"
1569 .to_owned();
1570
1571 let step = &TryCmd::parse_trycmd(&actual).unwrap().steps[0];
1572 overwrite_trycmd_status(
1573 Some(exit_code_to_status(0)),
1574 step,
1575 &mut step.expected_stdout_source.clone().unwrap(),
1576 &mut actual,
1577 )
1578 .unwrap();
1579
1580 assert_eq!(expected, actual);
1581 }
1582
1583 #[test]
1584 fn overwrite_trycmd_status_failed() {
1585 let expected = r#"
1586```
1587$ cmd arg
1588? 1
1589foo
1590bar
1591```
1592"#;
1593
1594 let mut actual = r"
1595```
1596$ cmd arg
1597? 2
1598foo
1599bar
1600```
1601"
1602 .to_owned();
1603
1604 let step = &TryCmd::parse_trycmd(&actual).unwrap().steps[0];
1605 overwrite_trycmd_status(
1606 Some(exit_code_to_status(1)),
1607 step,
1608 &mut step.expected_stdout_source.clone().unwrap(),
1609 &mut actual,
1610 )
1611 .unwrap();
1612
1613 assert_eq!(expected, actual);
1614 }
1615
1616 #[test]
1617 fn overwrite_trycmd_status_keeps_style() {
1618 let expected = r#"
1619```
1620$ cmd arg
1621? success
1622foo
1623bar
1624```
1625"#;
1626
1627 let mut actual = r"
1628```
1629$ cmd arg
1630? success
1631foo
1632bar
1633```
1634"
1635 .to_owned();
1636
1637 let step = &TryCmd::parse_trycmd(&actual).unwrap().steps[0];
1638 overwrite_trycmd_status(
1639 Some(exit_code_to_status(0)),
1640 step,
1641 &mut step.expected_stdout_source.clone().unwrap(),
1642 &mut actual,
1643 )
1644 .unwrap();
1645
1646 assert_eq!(expected, actual);
1647 }
1648
1649 #[cfg(unix)]
1650 fn exit_code_to_status(code: u8) -> std::process::ExitStatus {
1651 use std::os::unix::process::ExitStatusExt;
1652 std::process::ExitStatus::from_raw((code as i32) << 8)
1653 }
1654
1655 #[cfg(windows)]
1656 fn exit_code_to_status(code: u8) -> std::process::ExitStatus {
1657 use std::os::windows::process::ExitStatusExt;
1658 std::process::ExitStatus::from_raw(code as u32)
1659 }
1660
1661 #[test]
1662 fn exit_code_to_status_works() {
1663 assert_eq!(exit_code_to_status(42).code(), Some(42));
1664 }
1665}