Skip to main content

greentic_dev/delegate/
component.rs

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