moonlight-cli 0.1.0

Compare command behavior against one or two reference commands.
Documentation
use assert_cmd::cargo::cargo_bin;
use chrono::Utc;
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
use moonlight_core::{
    Adapter, BodyCapture, Classification, ComparisonRun, ComparisonSummary, RunInput,
    TargetObservation,
};
use std::{
    collections::BTreeMap,
    fs,
    path::{Path, PathBuf},
    process::Command,
};
use tempfile::{tempdir, TempDir};
use uuid::Uuid;

fn binary() -> PathBuf {
    cargo_bin("moonlight-cli")
}

fn command_json(value: &str) -> String {
    format!("printf '%s\\n' '{}'", value)
}

fn fresh_storage() -> (TempDir, PathBuf) {
    let dir = tempdir().expect("temp dir");
    let path = dir.path().join("runs.jsonl");
    (dir, path)
}

fn run_cli(args: &[&str]) {
    let output = Command::new(binary())
        .args(args)
        .output()
        .expect("run moonlight-cli");
    assert!(
        output.status.success(),
        "moonlight-cli failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
}

fn run_scenario(storage_path: &Path, args: &[&str]) {
    let storage = storage_path.to_string_lossy();
    let mut full_args = vec!["run", "--storage-path", storage.as_ref(), "--compact"];
    full_args.extend_from_slice(args);
    run_cli(&full_args);
}

fn body() -> BodyCapture {
    BodyCapture {
        size_bytes: 0,
        sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_string(),
        preview: String::new(),
        truncated: false,
    }
}

fn target(latency_ms: u128) -> TargetObservation {
    TargetObservation {
        status: Some(0),
        headers: BTreeMap::new(),
        body: body(),
        stderr: Some(body()),
        latency_ms,
        error: None,
    }
}

fn fixture_run(index: usize) -> ComparisonRun {
    ComparisonRun {
        id: Uuid::new_v4(),
        timestamp: Utc::now(),
        adapter: Adapter::Cli,
        input: RunInput::Cli {
            primary_command: format!("fixture-{index}-primary"),
            candidate_command: format!("fixture-{index}-candidate"),
            secondary_command: Some(format!("fixture-{index}-secondary")),
        },
        request_headers: BTreeMap::new(),
        request_body: body(),
        primary: target(10),
        candidate: target(20),
        secondary: Some(target(30)),
        comparison: ComparisonSummary {
            classification: if index.is_multiple_of(2) {
                Classification::Match
            } else {
                Classification::SuspiciousDifference
            },
            ..Default::default()
        },
    }
}

fn write_fixture(count: usize) -> (TempDir, PathBuf, Uuid) {
    let (dir, path) = fresh_storage();
    let mut show_id = Uuid::nil();
    let lines = (0..count)
        .map(|index| {
            let run = fixture_run(index);
            if index == count / 2 {
                show_id = run.id;
            }
            serde_json::to_string(&run).expect("serialize fixture run")
        })
        .collect::<Vec<_>>()
        .join("\n");
    fs::write(&path, format!("{lines}\n")).expect("write fixture");
    (dir, path, show_id)
}

fn bench_run_commands(c: &mut Criterion) {
    c.bench_function("run_match_small_json", |b| {
        let primary = command_json(r#"{"value":42}"#);
        let candidate = command_json(r#"{"value":42}"#);
        let secondary = command_json(r#"{"value":42}"#);
        b.iter_batched(
            fresh_storage,
            |(_dir, storage)| {
                run_scenario(
                    &storage,
                    &[
                        "--primary",
                        &primary,
                        "--candidate",
                        &candidate,
                        "--secondary",
                        &secondary,
                    ],
                );
            },
            BatchSize::SmallInput,
        );
    });

    c.bench_function("run_suspicious_difference_small_json", |b| {
        let primary = command_json(r#"{"value":42}"#);
        let candidate = command_json(r#"{"value":43}"#);
        b.iter_batched(
            fresh_storage,
            |(_dir, storage)| {
                run_scenario(
                    &storage,
                    &["--primary", &primary, "--candidate", &candidate],
                );
            },
            BatchSize::SmallInput,
        );
    });

    c.bench_function("run_reference_noise_small_json", |b| {
        let primary = command_json(r#"{"region":"a","value":1}"#);
        let candidate = command_json(r#"{"region":"a","value":1}"#);
        let secondary = command_json(r#"{"region":"b","value":1}"#);
        b.iter_batched(
            fresh_storage,
            |(_dir, storage)| {
                run_scenario(
                    &storage,
                    &[
                        "--primary",
                        &primary,
                        "--candidate",
                        &candidate,
                        "--secondary",
                        &secondary,
                    ],
                );
            },
            BatchSize::SmallInput,
        );
    });

    c.bench_function("run_large_stdout_64kb", |b| {
        let primary = "python3 -c 'print(\"a\" * 65536, end=\"\")'";
        let candidate = "python3 -c 'print(\"a\" * 65536, end=\"\")'";
        b.iter_batched(
            fresh_storage,
            |(_dir, storage)| {
                run_scenario(&storage, &["--primary", primary, "--candidate", candidate]);
            },
            BatchSize::SmallInput,
        );
    });
}

fn bench_read_commands(c: &mut Criterion) {
    let (_dir, storage, show_id) = write_fixture(1000);
    let storage = storage.to_string_lossy().into_owned();
    let show_id = show_id.to_string();

    c.bench_function("stats_1000_runs", |b| {
        b.iter(|| run_cli(&["stats", "--storage-path", &storage]));
    });

    c.bench_function("list_1000_runs", |b| {
        b.iter(|| run_cli(&["list", "--storage-path", &storage]));
    });

    c.bench_function("show_middle_run_1000_runs", |b| {
        b.iter(|| run_cli(&["show", &show_id, "--storage-path", &storage]));
    });
}

criterion_group!(benches, bench_run_commands, bench_read_commands);
criterion_main!(benches);