qa 0.1.1

Requirements traceability for safety-critical Rust software
Documentation
use qa::RequirementType::*;
use qa::{Format, Matrix, traces};

// --- Test-local requirements ---

qa::requirements! {
    pub REQ_MACRO_SINGLE: Functional {
        description: "requirement! macro defines a single requirement",
    }

    pub REQ_MACRO_BATCH: Functional {
        description: "requirements! macro defines multiple requirements in one block",
    }

    pub REQ_METADATA: Functional {
        description: "Freeform metadata is accessible via Requirement::get",
        source: "design spec",
        priority: "high",
        category: "ergonomics",
    }

    pub REQ_TRACES_ATTR: Functional {
        description: "#[traces] links tests to requirements",
    }

    pub REQ_MULTI_TRACE: Functional {
        description: "A test can trace multiple requirements",
    }

    pub REQ_CUSTOM_TYPE: Functional {
        description: "Custom requirement types via RequirementType::Custom",
    }
}

const REGULATORY: qa::RequirementType = qa::RequirementType::Custom("regulatory");

qa::requirement! {
    pub REQ_CUSTOM_CONST: REGULATORY {
        description: "Custom types work via const aliases",
    }
}

// --- Tests tracing the crate's own requirements ---

#[test]
#[traces(REQ_DEFINE_AS_CODE, REQ_MACRO_SINGLE)]
fn test_requirement_macro_creates_static() {
    assert_eq!(REQ_MACRO_SINGLE.id, "REQ_MACRO_SINGLE");
    assert_eq!(REQ_MACRO_SINGLE.kind, Functional);
    assert_eq!(
        REQ_MACRO_SINGLE.description,
        "requirement! macro defines a single requirement"
    );
}

#[test]
#[traces(REQ_DEFINE_AS_CODE, REQ_MACRO_BATCH)]
fn test_requirements_batch_macro() {
    assert_eq!(REQ_MACRO_BATCH.id, "REQ_MACRO_BATCH");
    assert_eq!(REQ_MACRO_BATCH.kind, Functional);
}

#[test]
#[traces(REQ_DEFINE_AS_CODE, REQ_METADATA)]
fn test_metadata_access() {
    assert_eq!(REQ_METADATA.get("source"), Some("design spec"));
    assert_eq!(REQ_METADATA.get("priority"), Some("high"));
    assert_eq!(REQ_METADATA.get("category"), Some("ergonomics"));
    assert_eq!(REQ_METADATA.get("nonexistent"), None);
}

#[test]
#[traces(REQ_CUSTOM_TYPE, REQ_CUSTOM_CONST)]
fn test_custom_requirement_type() {
    assert_eq!(
        REQ_CUSTOM_CONST.kind,
        qa::RequirementType::Custom("regulatory")
    );
    assert_eq!(format!("{}", REQ_CUSTOM_CONST.kind), "regulatory");
}

#[test]
#[traces(REQ_TRACES_ATTR, REQ_TEST_TRACING)]
fn test_traces_registers_in_distributed_slice() {
    let matrix = Matrix::collect();
    let traces = matrix.traces_for("REQ_TRACES_ATTR");
    assert!(
        traces
            .iter()
            .any(|t| t.test_name == "test_traces_registers_in_distributed_slice"),
        "expected this test to appear in traces for REQ_TRACES_ATTR"
    );
}

#[test]
#[traces(REQ_MULTI_TRACE, REQ_TRACES_ATTR)]
fn test_multi_requirement_trace() {
    let matrix = Matrix::collect();
    let t1 = matrix.traces_for("REQ_MULTI_TRACE");
    let t2 = matrix.traces_for("REQ_TRACES_ATTR");
    assert!(
        t1.iter()
            .any(|t| t.test_name == "test_multi_requirement_trace")
    );
    assert!(
        t2.iter()
            .any(|t| t.test_name == "test_multi_requirement_trace")
    );
}

#[test]
#[traces(REQ_MATRIX_GENERATION)]
fn test_matrix_collect() {
    let matrix = Matrix::collect();
    assert!(
        !matrix.requirements().is_empty(),
        "matrix should contain registered requirements"
    );
    assert!(
        !matrix.traces().is_empty(),
        "matrix should contain registered traces"
    );
}

#[test]
#[traces(REQ_GAP_ANALYSIS)]
fn test_orphan_trace_detection() {
    // REQ_NONEXISTENT is referenced by a trace but not defined
    // We can't easily create an orphan trace in this test since #[traces]
    // uses string IDs. Instead, verify the API works on the current matrix.
    let matrix = Matrix::collect();
    // All our traces reference real requirements, so orphans should be empty
    // (unless there's a typo somewhere, which would be a real bug!)
    let orphans = matrix.orphan_traces();
    assert!(
        orphans.is_empty(),
        "unexpected orphan traces: {:?}",
        orphans.iter().map(|t| t.requirement_id).collect::<Vec<_>>()
    );
}

#[test]
#[traces(REQ_MATRIX_GENERATION)]
fn test_matrix_coverage() {
    let matrix = Matrix::collect();
    let coverage = matrix.coverage();
    assert!(
        coverage > 0.0,
        "coverage should be > 0 with traced requirements"
    );
    assert!(coverage <= 1.0);
}

#[test]
#[traces(REQ_MATRIX_GENERATION)]
fn test_markdown_rendering() {
    let matrix = Matrix::collect();
    let md = matrix.render(Format::Markdown);
    assert!(md.contains("# Traceability Matrix"));
    assert!(md.contains("REQ_MACRO_SINGLE"));
    assert!(md.contains("**Coverage**"));
}

#[test]
#[traces(REQ_MATRIX_GENERATION)]
fn test_tabled_rendering() {
    let matrix = Matrix::collect();
    matrix.print(); // should not panic
}

#[test]
#[traces(REQ_GAP_ANALYSIS)]
fn test_filter_by_metadata() {
    let matrix = Matrix::collect();
    let filtered = matrix.filter_by("priority", "high");
    assert!(
        filtered
            .requirements()
            .iter()
            .all(|r| r.get("priority") == Some("high"))
    );
}

#[test]
#[traces(REQ_GAP_ANALYSIS)]
fn test_group_by_metadata() {
    let matrix = Matrix::collect();
    let groups = matrix.group_by("priority");
    // At least REQ_METADATA has priority: "high"
    assert!(
        groups.contains_key("high"),
        "expected a 'high' priority group"
    );
}

// --- no_std verification (actual check is CI cross-compile) ---

#[test]
#[traces(REQ_NO_STD)]
fn test_no_std_types_are_send_sync() {
    fn assert_send_sync<T: Send + Sync>() {}
    assert_send_sync::<qa::Requirement>();
    assert_send_sync::<qa::TestTrace>();
    assert_send_sync::<qa::RequirementType>();
}

// --- Composability tests ---

#[test]
#[should_panic(expected = "intentional")]
#[traces(REQ_COMPOSABILITY)]
fn test_traces_with_should_panic() {
    panic!("intentional");
}

// --- Dogfood: generate the traceability matrix for qa itself ---

#[test]
#[traces(REQ_MATRIX_GENERATION, REQ_GAP_ANALYSIS)]
fn traceability_matrix() {
    let matrix = Matrix::collect();
    matrix.print();

    let md = matrix.render(Format::Markdown);
    assert!(!md.is_empty());

    // Verify all of qa's own requirements are traced
    let untraced = matrix.untraced();
    if !untraced.is_empty() {
        let listing: Vec<_> = untraced.iter().map(|r| r.id).collect();
        panic!("untraced requirements: {listing:?}");
    }
}