Skip to main content

greentic_dev/delegate/
component.rs

1use std::ffi::OsString;
2
3use crate::config::{self, GreenticConfig};
4use crate::passthrough::resolve_binary;
5use crate::util::process::{self, CommandOutput, CommandSpec, StreamMode};
6use anyhow::{Context, Result, anyhow, bail};
7
8const TOOL_NAME: &str = "greentic-component";
9
10pub struct ComponentDelegate {
11    program: OsString,
12}
13
14impl ComponentDelegate {
15    pub fn from_config(config: &GreenticConfig) -> Result<Self> {
16        let resolved = resolve_program(config)?;
17        Ok(Self {
18            program: resolved.program,
19        })
20    }
21
22    pub fn run_passthrough(&self, args: &[String]) -> Result<()> {
23        let argv: Vec<OsString> = args.iter().map(OsString::from).collect();
24        let label = args.first().map(|s| s.as_str()).unwrap_or("<component>");
25        let output = self.exec(argv, false)?;
26        self.ensure_success(label, false, &output)
27    }
28
29    fn exec(&self, args: Vec<OsString>, capture: bool) -> Result<CommandOutput> {
30        let mut spec = CommandSpec::new(self.program.clone());
31        spec.args = args;
32        if capture {
33            spec.stdout = StreamMode::Capture;
34            spec.stderr = StreamMode::Capture;
35        } else {
36            spec.stdout = StreamMode::Inherit;
37            spec.stderr = StreamMode::Inherit;
38        }
39        process::run(spec)
40            .with_context(|| format!("failed to spawn `{}`", self.program.to_string_lossy()))
41    }
42
43    fn ensure_success(&self, label: &str, capture: bool, output: &CommandOutput) -> Result<()> {
44        if output.status.success() {
45            return Ok(());
46        }
47
48        if capture
49            && let Some(stderr) = output.stderr.as_ref()
50            && !stderr.is_empty()
51        {
52            eprintln!("{}", String::from_utf8_lossy(stderr));
53        }
54        let code = output.status.code().unwrap_or_default();
55        bail!(
56            "`{}` {label} failed with exit code {code}",
57            self.program.to_string_lossy()
58        );
59    }
60}
61
62struct ResolvedProgram {
63    program: OsString,
64}
65
66fn resolve_program(config: &GreenticConfig) -> Result<ResolvedProgram> {
67    if let Some(custom) = config.tools.greentic_component.path.as_ref() {
68        if !custom.exists() {
69            bail!(
70                "configured greentic-component path `{}` does not exist",
71                custom.display()
72            );
73        }
74        return Ok(ResolvedProgram {
75            program: custom.as_os_str().to_os_string(),
76        });
77    }
78
79    match resolve_binary(TOOL_NAME) {
80        Ok(path) => Ok(ResolvedProgram {
81            program: path.into_os_string(),
82        }),
83        Err(error) => {
84            let config_hint = config::config_path()
85                .map(|path| path.display().to_string())
86                .unwrap_or_else(|| "$XDG_CONFIG_HOME/greentic-dev/config.toml".to_string());
87            Err(anyhow!(
88                "failed to locate `{TOOL_NAME}` on PATH ({error}). Install it via `cargo install \
89                 greentic-component` or set [tools.greentic-component].path in {config_hint}."
90            ))
91        }
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::{ComponentDelegate, resolve_program};
98    use crate::config::GreenticConfig;
99    use crate::util::process::CommandOutput;
100    use std::ffi::OsString;
101    use std::process::ExitStatus;
102    use tempfile::tempdir;
103
104    #[cfg(unix)]
105    fn success_status() -> ExitStatus {
106        use std::os::unix::process::ExitStatusExt;
107        ExitStatus::from_raw(0)
108    }
109
110    #[cfg(unix)]
111    fn failure_status(code: i32) -> ExitStatus {
112        use std::os::unix::process::ExitStatusExt;
113        ExitStatus::from_raw(code << 8)
114    }
115
116    #[test]
117    fn resolve_program_uses_existing_configured_path() {
118        let dir = tempdir().unwrap();
119        let custom = dir.path().join("greentic-component");
120        std::fs::write(&custom, "stub").unwrap();
121
122        let mut config = GreenticConfig::default();
123        config.tools.greentic_component.path = Some(custom.clone());
124
125        let resolved = resolve_program(&config).unwrap();
126        assert_eq!(resolved.program, custom.into_os_string());
127    }
128
129    #[test]
130    fn resolve_program_rejects_missing_configured_path() {
131        let mut config = GreenticConfig::default();
132        config.tools.greentic_component.path =
133            Some(tempdir().unwrap().path().join("missing-component"));
134
135        let err = resolve_program(&config)
136            .err()
137            .expect("expected missing path");
138        assert!(err.to_string().contains("does not exist"));
139    }
140
141    #[test]
142    fn ensure_success_accepts_successful_status() {
143        let delegate = ComponentDelegate {
144            program: OsString::from("greentic-component"),
145        };
146        let output = CommandOutput {
147            status: success_status(),
148            stdout: None,
149            stderr: None,
150        };
151
152        delegate.ensure_success("doctor", false, &output).unwrap();
153    }
154
155    #[test]
156    fn ensure_success_reports_failure_code() {
157        let delegate = ComponentDelegate {
158            program: OsString::from("greentic-component"),
159        };
160        let output = CommandOutput {
161            status: failure_status(7),
162            stdout: None,
163            stderr: Some(b"boom".to_vec()),
164        };
165
166        let err = delegate
167            .ensure_success("doctor", true, &output)
168            .unwrap_err();
169        assert!(err.to_string().contains("exit code 7"));
170    }
171}