Skip to main content

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}