provenance-cli 0.2.3

Command-line interface for Provenance requirements traceability
use super::support::{
    create_requirement, create_rule, diagnostic, git, init, provenance, REQUIREMENTS_SHARD,
    RULES_SHARD,
};
use provenance_macros::verifies;
use serde_json::{json, Value};

fn commit(repository: &std::path::Path, message: &str) {
    git(repository, &["add", "."]);
    git(repository, &["commit", "-m", message]);
}

fn check(repository: &std::path::Path) -> std::process::Output {
    provenance()
        .args([
            "check",
            "--repo",
            repository.to_str().unwrap(),
            "--format",
            "json",
        ])
        .output()
        .unwrap()
}

fn rewrite_statement(repository: &std::path::Path, shard: &str, id: &str, statement: &str) {
    let path = repository.join(shard);
    let contents = std::fs::read_to_string(&path).unwrap();
    let rewritten = contents
        .lines()
        .map(|line| {
            let mut value: Value = serde_json::from_str(line).unwrap();
            if value["id"] == id {
                value["statement"] = json!(statement);
            }
            serde_json::to_string(&value).unwrap()
        })
        .collect::<Vec<_>>()
        .join("\n")
        + "\n";
    std::fs::write(path, rewritten).unwrap();
}

#[test]
#[verifies("rule_ste_manual_changed_statement_report", examples)]
fn check_reports_only_new_and_statement_changed_records_against_git_head() {
    let directory = tempfile::tempdir().unwrap();
    let repo = directory.path();
    git(repo, &["init", "--initial-branch", "main"]);
    git(repo, &["config", "user.email", "test@example.test"]);
    git(repo, &["config", "user.name", "Test"]);
    init(repo);
    create_requirement(repo, "req_legacy", "Legacy statement");
    create_requirement(repo, "req_changed", "Original statement");
    commit(repo, "base");
    rewrite_statement(repo, REQUIREMENTS_SHARD, "req_legacy", "Legacy; invalid");
    commit(repo, "pin legacy invalid text");

    rewrite_statement(repo, REQUIREMENTS_SHARD, "req_changed", "Café; changed");
    create_rule(repo, "rule_added", "Rule added");
    rewrite_statement(repo, RULES_SHARD, "rule_added", "Rule; added");
    let output = check(repo);

    assert!(output.status.success(), "manual findings are informational");
    let report: Value = serde_json::from_slice(&output.stdout).unwrap();
    assert_eq!(report["status"], "ok");
    assert_eq!(
        report["diagnostics"],
        json!([
            diagnostic("requirement", "req_changed", 5),
            diagnostic("rule", "rule_added", 4)
        ])
    );
}

#[test]
#[verifies("rule_ste_manual_changed_statement_report", examples)]
fn clean_repository_check_is_successful_and_has_no_statement_diagnostics() {
    let directory = tempfile::tempdir().unwrap();
    let repo = directory.path();
    git(repo, &["init", "--initial-branch", "main"]);
    git(repo, &["config", "user.email", "test@example.test"]);
    git(repo, &["config", "user.name", "Test"]);
    init(repo);
    create_requirement(repo, "req_clean", "Clean statement");
    commit(repo, "base");

    let first = check(repo);
    let second = check(repo);

    assert!(first.status.success());
    assert_eq!(first.stdout, second.stdout);
    let report: Value = serde_json::from_slice(&first.stdout).unwrap();
    assert_eq!(report, json!({"status": "ok", "diagnostics": []}));
}

#[test]
#[verifies("rule_ste_manual_changed_statement_report", examples)]
fn repository_without_git_head_reports_no_statement_diagnostics() {
    let directory = tempfile::tempdir().unwrap();
    let repo = directory.path();
    git(repo, &["init", "--initial-branch", "main"]);
    init(repo);
    create_requirement(repo, "req_unborn", "Clean statement");
    rewrite_statement(repo, REQUIREMENTS_SHARD, "req_unborn", "Changed; statement");

    let output = check(repo);

    assert!(output.status.success());
    let report: Value = serde_json::from_slice(&output.stdout).unwrap();
    assert_eq!(report, json!({"status": "ok", "diagnostics": []}));
}