ripr 0.9.0

Find static mutation-exposure gaps before expensive mutation testing
Documentation
use crate::cli::commands_context::ensure_command_root;
use crate::cli::parse::expect_value;
use crate::output;
use std::path::{Path, PathBuf};

#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct Options {
    pub(super) root: PathBuf,
    pub(super) result: PathBuf,
}

pub(super) fn parse_options(args: &[String]) -> Result<Options, String> {
    let mut root = PathBuf::from(".");
    let mut result = None;

    let mut i = 0usize;
    while i < args.len() {
        match args[i].as_str() {
            "--root" => {
                i += 1;
                let value = expect_value(args, i, "--root")?;
                if value.trim().is_empty() {
                    return Err("swarm ingest --root requires a non-empty path".to_string());
                }
                root = PathBuf::from(value);
            }
            "--result" => {
                i += 1;
                let value = expect_value(args, i, "--result")?;
                if value.trim().is_empty() {
                    return Err("swarm ingest --result requires a non-empty path".to_string());
                }
                result = Some(PathBuf::from(value));
            }
            "--format" => {
                i += 1;
                let value = expect_value(args, i, "--format")?;
                if value != "json" {
                    return Err(format!(
                        "unknown swarm ingest format {value:?}; expected `json`"
                    ));
                }
            }
            "--json" => {}
            other => return Err(format!("unknown swarm ingest argument {other:?}")),
        }
        i += 1;
    }

    Ok(Options {
        root,
        result: result.ok_or_else(|| "swarm ingest requires --result <path>".to_string())?,
    })
}

pub(super) fn run(options: Options) -> Result<(), String> {
    ensure_command_root(&options.root, "swarm ingest")?;
    let result_path = validate_result_path(&options.root, &options.result)?;
    let contents = std::fs::read_to_string(&result_path).map_err(|err| {
        format!(
            "read swarm ingest --result {} failed: {err}",
            options.result.display()
        )
    })?;
    let rendered = output::swarm_ingest::render_swarm_ingest_json(
        &contents,
        &output::outcome::display_path(&options.result),
    )?;
    print!("{rendered}");
    Ok(())
}

fn validate_result_path(root: &Path, path: &Path) -> Result<PathBuf, String> {
    let root = root.canonicalize().map_err(|err| {
        format!(
            "canonicalize swarm ingest root {} failed: {err}",
            root.display()
        )
    })?;
    let candidate = if path.is_absolute() {
        path.to_path_buf()
    } else {
        root.join(path)
    };
    let candidate = candidate.canonicalize().map_err(|err| {
        format!(
            "canonicalize swarm ingest --result {} failed: {err}",
            path.display()
        )
    })?;

    if !candidate.starts_with(&root) {
        return Err(format!(
            "swarm ingest --result {} must stay under root {}",
            path.display(),
            root.display()
        ));
    }

    Ok(candidate)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn args(values: &[&str]) -> Vec<String> {
        values.iter().map(|value| value.to_string()).collect()
    }

    #[test]
    fn parses_result_and_format() {
        assert_eq!(
            parse_options(&args(&[
                "--root",
                ".",
                "--result",
                "target/ripr/workflow/agent-result.json",
                "--format",
                "json",
            ])),
            Ok(Options {
                root: PathBuf::from("."),
                result: PathBuf::from("target/ripr/workflow/agent-result.json"),
            })
        );
        assert_eq!(
            parse_options(&args(&["--format", "md"])),
            Err("unknown swarm ingest format \"md\"; expected `json`".to_string())
        );
        assert_eq!(
            parse_options(&args(&[])),
            Err("swarm ingest requires --result <path>".to_string())
        );
    }
}