xianyun_pki/truststore/command_runner.rs
1//! A minimal stand-in for `internal/executor.CommandRunner`.
2//!
3//! The real `executor` package is a separate Go package with its own test
4//! suite (`internal/executor/*_test.go`), which is out of scope for this
5//! port (see `PORT_NOTES.md`). `internal/truststore` only ever calls two of
6//! its methods — `RunSudo` and `ReadStdout` — so this trait models exactly
7//! those two, as a placeholder until a `xianyun-executor` crate exists for
8//! `truststore` to depend on instead.
9
10use std::fmt;
11
12/// Ported from the two `CommandRunner` methods `truststore` actually calls.
13pub trait CommandRunner {
14 /// Ported from `CommandRunner.RunSudo`.
15 ///
16 /// # Errors
17 ///
18 /// Returns [`CmdError`] if the command could not be run or exited
19 /// non-zero.
20 fn run_sudo(&self, name: &str, args: &[String]) -> Result<(), CmdError>;
21
22 /// Ported from `CommandRunner.ReadStdout`.
23 ///
24 /// # Errors
25 ///
26 /// Returns [`CmdError`] if the command could not be run or exited
27 /// non-zero.
28 fn read_stdout(&self, name: &str, args: &[String]) -> Result<String, CmdError>;
29}
30
31/// A minimal stand-in for the Go error values these tests return through
32/// the mock (`fs.ErrPermission`, `fs.ErrNotExist`, etc.). Not a rich error
33/// type — just enough to drive the ported assertions.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct CmdError(pub String);
36
37impl fmt::Display for CmdError {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 write!(f, "{}", self.0)
40 }
41}
42
43impl std::error::Error for CmdError {}
44
45#[cfg(test)]
46pub(crate) mod mock {
47 use super::*;
48 use std::cell::RefCell;
49
50 /// A same-call-shape stand-in for mockery's generated
51 /// `executormocks.MockCommandRunner`. Expectations are matched by
52 /// method name and exact args, consumed on first match (mockery's
53 /// default `.Once()`-free behaviour is "match any number of times",
54 /// but every ported test here calls each expectation exactly once, so
55 /// consuming keeps mismatched extra calls loud instead of silent).
56 /// A queued expectation: the expected `(name, args)` call, paired
57 /// with the canned result to return.
58 type Expectation<T> = (String, Vec<String>, T);
59
60 #[derive(Default)]
61 pub(crate) struct MockCommandRunner {
62 run_sudo: RefCell<Vec<Expectation<Result<(), CmdError>>>>,
63 read_stdout: RefCell<Vec<Expectation<Result<String, CmdError>>>>,
64 }
65
66 impl MockCommandRunner {
67 pub(crate) fn new() -> Self {
68 Self::default()
69 }
70
71 pub(crate) fn expect_run_sudo(
72 &self,
73 name: &str,
74 args: &[&str],
75 result: Result<(), CmdError>,
76 ) {
77 self.run_sudo.borrow_mut().push((
78 name.to_string(),
79 args.iter().map(|s| s.to_string()).collect(),
80 result,
81 ));
82 }
83
84 // Only `system_darwin`'s tests call this (inspecting `security
85 // find-certificate` output via `report_superseded`/`same_key_cert`),
86 // so it and `read_stdout` below are unreachable on a non-macOS
87 // build, including when measuring coverage on Linux.
88 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
89 pub(crate) fn expect_read_stdout(
90 &self,
91 name: &str,
92 args: &[&str],
93 result: Result<String, CmdError>,
94 ) {
95 self.read_stdout.borrow_mut().push((
96 name.to_string(),
97 args.iter().map(|s| s.to_string()).collect(),
98 result,
99 ));
100 }
101 }
102
103 impl CommandRunner for MockCommandRunner {
104 fn run_sudo(&self, name: &str, args: &[String]) -> Result<(), CmdError> {
105 let mut expectations = self.run_sudo.borrow_mut();
106 let idx = expectations
107 .iter()
108 .position(|(n, a, _)| n == name && a == args)
109 .unwrap_or_else(|| panic!("unexpected RunSudo({name:?}, {args:?}) call"));
110
111 expectations.remove(idx).2
112 }
113
114 fn read_stdout(&self, name: &str, args: &[String]) -> Result<String, CmdError> {
115 let mut expectations = self.read_stdout.borrow_mut();
116 let idx = expectations
117 .iter()
118 .position(|(n, a, _)| n == name && a == args)
119 .unwrap_or_else(|| panic!("unexpected ReadStdout({name:?}, {args:?}) call"));
120
121 expectations.remove(idx).2
122 }
123 }
124}