Skip to main content

light_fw_cli_core/
process.rs

1//! Обёртка над запуском внешних процессов.
2//!
3//! Реальная работа с `cargo`/`rustc`/`rustup` идёт через трейт [`CommandRunner`],
4//! что позволяет тестировать логику CLI без запуска настоящих процессов
5//! (в тестах подставляется мок-раннер).
6
7use std::path::Path;
8use std::process::Command;
9
10use crate::error::{Error, Result};
11
12/// Результат запуска внешней команды.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct CommandOutput {
15    /// Код возврата (`-1`, если процесс завершён сигналом).
16    pub status: i32,
17    /// Стандартный вывод.
18    pub stdout: String,
19    /// Стандартный поток ошибок.
20    pub stderr: String,
21}
22
23impl CommandOutput {
24    /// Успешно ли завершилась команда.
25    pub fn success(&self) -> bool {
26        self.status == 0
27    }
28}
29
30/// Запускает внешние команды. Реализация по умолчанию — [`SystemRunner`].
31pub trait CommandRunner: Send + Sync {
32    /// Запускает `program` с аргументами `args` в каталоге `cwd`.
33    fn run(&self, program: &str, args: &[String], cwd: Option<&Path>) -> Result<CommandOutput>;
34}
35
36/// Раннер, запускающий реальные процессы через [`std::process::Command`].
37#[derive(Debug, Default, Clone, Copy)]
38pub struct SystemRunner;
39
40impl CommandRunner for SystemRunner {
41    fn run(&self, program: &str, args: &[String], cwd: Option<&Path>) -> Result<CommandOutput> {
42        let mut command = Command::new(program);
43        command.args(args);
44        if let Some(dir) = cwd {
45            command.current_dir(dir);
46        }
47        let output = command.output().map_err(|e| {
48            if e.kind() == std::io::ErrorKind::NotFound {
49                Error::CommandNotFound(program.to_string())
50            } else {
51                Error::Io(e)
52            }
53        })?;
54        Ok(CommandOutput {
55            status: output.status.code().unwrap_or(-1),
56            stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
57            stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
58        })
59    }
60}
61
62/// Извлекает semver-версию из первой строки вывода `* --version`.
63///
64/// Формат: `<program> <version> (<hash> <date>)`, например
65/// `rustc 1.99.0 (5f94df478 2026-08-27)`.
66pub fn parse_program_version(stdout: &str) -> Option<semver::Version> {
67    let mut parts = stdout.split_whitespace();
68    let _program = parts.next()?;
69    let version = parts.next()?;
70    semver::Version::parse(version).ok()
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn parses_version_line() {
79        let v =
80            parse_program_version("rustc 1.99.0 (5f94df478 2026-08-27)").expect("version parsed");
81        assert_eq!(v, semver::Version::new(1, 99, 0));
82    }
83
84    #[test]
85    fn version_parse_rejects_garbage() {
86        assert!(parse_program_version("no version here").is_none());
87        assert!(parse_program_version("").is_none());
88    }
89}