provenance-cli 0.2.3

Command-line interface for Provenance requirements traceability
use crate::support::{
    create_requirement, error_json, imported_dictionary, init, provenance, write_reference,
    UNAPPROVED_WORD,
};
use provenance_macros::verifies;
use provenance_ste100::store_dictionary_index;
use serde_json::Value;

fn statement() -> String {
    format!("The {UNAPPROVED_WORD} item stops.")
}

#[test]
#[verifies("rule_ste_dictionary_unapproved_word", examples)]
#[verifies("rule_ste_dictionary_reference_resolution", examples)]
fn a_reference_with_a_loadable_index_rejects_an_unapproved_word() {
    let scratch = tempfile::tempdir().unwrap();
    let repo = scratch.path().join("repo");
    let index_directory = scratch.path().join("index");
    std::fs::create_dir_all(&repo).unwrap();
    init(&repo);
    let dictionary = imported_dictionary();
    store_dictionary_index(dictionary, &index_directory).unwrap();
    write_reference(&repo, dictionary);

    let output = create_requirement(&repo, &index_directory, "req_gate", &statement());

    assert!(
        !output.status.success(),
        "the write must fail: {}",
        String::from_utf8_lossy(&output.stdout)
    );
    let error = error_json(&output);
    assert_eq!(error["field"], "statement");
    let findings = error["findings"]
        .as_array()
        .expect("the error lists findings");
    assert_eq!(findings.len(), 1, "findings: {findings:?}");
    assert_eq!(findings[0]["rule"], "1.1");
    assert_eq!(findings[0]["kind"], "violation");
    let span = &findings[0]["span"];
    let start = usize::try_from(span["start"].as_u64().unwrap()).unwrap();
    let end = usize::try_from(span["end"].as_u64().unwrap()).unwrap();
    assert_eq!(&statement()[start..end], UNAPPROVED_WORD);
}

#[test]
#[verifies("rule_ste_dictionary_reference_resolution", examples)]
fn a_project_without_a_reference_accepts_the_same_statement() {
    let scratch = tempfile::tempdir().unwrap();
    let repo = scratch.path().join("repo");
    let index_directory = scratch.path().join("index");
    std::fs::create_dir_all(&repo).unwrap();
    init(&repo);
    store_dictionary_index(imported_dictionary(), &index_directory).unwrap();

    let output = create_requirement(&repo, &index_directory, "req_gate", &statement());

    assert!(
        output.status.success(),
        "the write must succeed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
}

#[test]
#[verifies("rule_ste_dictionary_reference_resolution", examples)]
fn a_reference_without_an_index_accepts_the_same_statement() {
    let scratch = tempfile::tempdir().unwrap();
    let repo = scratch.path().join("repo");
    let index_directory = scratch.path().join("index");
    std::fs::create_dir_all(&repo).unwrap();
    std::fs::create_dir_all(&index_directory).unwrap();
    init(&repo);
    write_reference(&repo, imported_dictionary());

    let output = create_requirement(&repo, &index_directory, "req_gate", &statement());

    assert!(
        output.status.success(),
        "a missing index must fall back to the data-free checks: {}",
        String::from_utf8_lossy(&output.stderr)
    );
}

#[test]
#[verifies("rule_ste_dictionary_reference_resolution", examples)]
#[verifies("rule_ste_strict_committed_statement_gate", examples)]
fn strict_check_uses_the_imported_project_dictionary() {
    let scratch = tempfile::tempdir().unwrap();
    let repo = scratch.path().join("repo");
    let index_directory = scratch.path().join("index");
    std::fs::create_dir_all(&repo).unwrap();
    git(&repo, &["init", "--initial-branch", "main"]);
    git(&repo, &["config", "user.email", "test@example.test"]);
    git(&repo, &["config", "user.name", "Test"]);
    init(&repo);
    let dictionary = imported_dictionary();
    store_dictionary_index(dictionary, &index_directory).unwrap();
    create_requirement(&repo, &index_directory, "req_dictionary", &statement())
        .status
        .success()
        .then_some(())
        .expect("the statement is accepted before the project reference exists");
    write_reference(&repo, dictionary);
    git(&repo, &["add", "."]);
    git(&repo, &["commit", "-m", "initial candidate"]);

    let output = provenance()
        .env("PROVENANCE_STE100_INDEX_DIR", &index_directory)
        .args([
            "check",
            "--repo",
            repo.to_str().unwrap(),
            "--strict",
            "--format",
            "json",
        ])
        .output()
        .unwrap();

    assert!(!output.status.success());
    let report: Value = serde_json::from_slice(&output.stdout).unwrap();
    assert_eq!(report["status"], "findings");
    assert_eq!(report["base_commit"], Value::Null);
    assert_eq!(report["diagnostics"][0]["id"], "req_dictionary");
    assert_eq!(report["diagnostics"][0]["rule"], "1.1");
}

fn git(repo: &std::path::Path, arguments: &[&str]) {
    let output = std::process::Command::new("git")
        .current_dir(repo)
        .args(arguments)
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "git {arguments:?} failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
}