moonlight-cli 0.1.2

Compare command behavior against one or two reference commands.
Documentation
use super::summary::{classification_key, EvalSummary};
use anyhow::bail;
use std::collections::BTreeMap;

#[derive(Debug, Clone, Copy)]
pub(super) enum EvalOutputFormat {
    Text,
    Json,
    Markdown,
}

impl std::str::FromStr for EvalOutputFormat {
    type Err = anyhow::Error;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        match value {
            "text" => Ok(Self::Text),
            "json" => Ok(Self::Json),
            "markdown" => Ok(Self::Markdown),
            other => bail!("invalid eval output format {other:?}"),
        }
    }
}

pub(super) fn print_summary(summary: &EvalSummary, format: EvalOutputFormat) -> anyhow::Result<()> {
    match format {
        EvalOutputFormat::Json => println!("{}", serde_json::to_string_pretty(summary)?),
        EvalOutputFormat::Text => print_text_summary(summary),
        EvalOutputFormat::Markdown => print_markdown_summary(summary),
    }
    Ok(())
}

fn print_text_summary(summary: &EvalSummary) {
    println!("Moonlight eval {}", summary.eval_id);
    println!("project: {}", summary.project);
    println!("baseline: {}", summary.baseline_ref);
    println!("candidate: {}", summary.candidate_source);
    println!();
    println!(
        "{} checks, {}",
        summary.total_checks,
        format_classification_counts(&summary.classifications)
    );
    for failure in &summary.failed_checks {
        println!();
        println!("FAIL {}", failure.check_id);
        println!(
            "  status: primary {}, candidate {}",
            display_status(failure.primary_status),
            display_status(failure.candidate_status)
        );
        println!(
            "  classification: {}",
            classification_key(&failure.classification)
        );
        println!("  diff: {}", failure.diff_summary);
        println!("  run: {}", failure.run_id);
    }
}

fn print_markdown_summary(summary: &EvalSummary) {
    println!("# Moonlight Eval {}", summary.eval_id);
    println!();
    println!("- Project: `{}`", summary.project);
    println!("- Baseline: `{}`", summary.baseline_ref);
    println!("- Candidate: `{}`", summary.candidate_source);
    println!("- Checks: `{}`", summary.total_checks);
    println!(
        "- Classifications: `{}`",
        format_classification_counts(&summary.classifications)
    );
    if summary.failed_checks.is_empty() {
        println!();
        println!("No failed checks.");
        return;
    }
    println!();
    println!("## Failed Checks");
    for failure in &summary.failed_checks {
        println!();
        println!("### {}", failure.check_id);
        println!();
        println!(
            "- Classification: `{}`",
            classification_key(&failure.classification)
        );
        println!(
            "- Status: primary `{}`, candidate `{}`",
            display_status(failure.primary_status),
            display_status(failure.candidate_status)
        );
        println!("- Diff: `{}`", failure.diff_summary.replace('`', "'"));
        println!("- Run: `{}`", failure.run_id);
    }
}

fn format_classification_counts(counts: &BTreeMap<String, usize>) -> String {
    if counts.is_empty() {
        return "no runs".to_string();
    }
    counts
        .iter()
        .map(|(classification, count)| format!("{count} {classification}"))
        .collect::<Vec<_>>()
        .join(", ")
}

fn display_status(status: Option<u16>) -> String {
    status
        .map(|value| value.to_string())
        .unwrap_or_else(|| "ERR".to_string())
}