main-spex 0.21.0

AI-first development framework that turns specifications into deterministic, verifiable code
#[cfg(test)]
mod tests {
    use super::super::*;
    use crate::spec::{CoverageIgnore, Status};
    use crate::support::testing;

    const FULL_COVERAGE_OF_AC_A: &str =
        "// @spec contract a\n// @spec impl a#ac-a\n// @spec test a#ac-a\n";

    fn spec(ac_ids: &[&str]) -> Spec {
        Spec {
            acceptance_criteria: ac_ids.iter().map(|id| testing::criterion(id)).collect(),
            status: Status::Active,
            ..testing::spec("a")
        }
    }

    fn not_done(id: &str) -> AcceptanceCriterion {
        AcceptanceCriterion {
            is_done: false,
            ..testing::criterion(id)
        }
    }

    fn ignoring(layer: CoverageLayer, ac: Option<&str>) -> CoverageIgnore {
        CoverageIgnore {
            layer,
            ac: ac.map(testing::ac_id),
        }
    }

    fn coverage<'a>(spec: &'a Spec, annotations: &str) -> SpecCoverage<'a> {
        SpecCoverage::new(spec, &AnnotationIndex::scan(annotations))
    }

    #[test]
    fn only_active_and_in_progress_specs_are_tracked() {
        for status in [Status::Planned, Status::Blocked, Status::Deprecated] {
            let spec = Spec {
                status,
                ..spec(&["ac-a"])
            };
            let coverage = coverage(&spec, "");
            assert!(!coverage.is_tracked());
            assert!(!coverage.has_gap());
            assert!(!coverage.has_pending());
            assert!(coverage.criteria.is_empty());
        }
        for status in [Status::Active, Status::InProgress] {
            let spec = Spec {
                status,
                ..spec(&["ac-a"])
            };
            assert!(coverage(&spec, "").is_tracked());
        }
    }

    #[test]
    fn contract_is_missing_until_a_spec_level_annotation_is_found() {
        let spec = spec(&[]);
        assert!(coverage(&spec, "").contract_missing);
        assert!(!coverage(&spec, "// @spec contract a\n").contract_missing);
    }

    #[test]
    fn ac_qualified_contract_annotation_satisfies_the_spec_level_contract() {
        let spec = spec(&[]);
        assert!(!coverage(&spec, "// @spec contract a#ac-a\n").contract_missing);
    }

    #[test]
    fn ignored_contract_is_not_missing_and_leaves_no_gap() {
        let spec = Spec {
            coverage_ignore: vec![ignoring(CoverageLayer::Contract, None)],
            ..spec(&["ac-a"])
        };
        let coverage = coverage(&spec, "// @spec impl a#ac-a\n// @spec test a#ac-a\n");
        assert!(!coverage.contract_missing);
        assert!(!coverage.has_gap());
    }

    #[test]
    fn contract_is_never_missing_under_none() {
        let spec = Spec {
            contract: ContractLevel::None,
            ..spec(&[])
        };
        assert!(!coverage(&spec, "").contract_missing);
    }

    #[test]
    fn for_each_ac_requires_a_contract_per_criterion_not_per_spec() {
        let spec = Spec {
            contract: ContractLevel::ForEachAc,
            ..spec(&["ac-one", "ac-two"])
        };
        assert!(!coverage(&spec, "").contract_missing);

        let coverage = coverage(&spec, "// @spec contract a#ac-one\n");
        assert_eq!(
            coverage.criteria[0].missing,
            vec![CoverageLayer::Impl, CoverageLayer::Test]
        );
        assert_eq!(
            coverage.criteria[1].missing,
            vec![
                CoverageLayer::Contract,
                CoverageLayer::Impl,
                CoverageLayer::Test
            ]
        );
    }

    #[test]
    fn for_each_ac_contract_can_be_waived_for_one_criterion() {
        let spec = Spec {
            contract: ContractLevel::ForEachAc,
            coverage_ignore: vec![ignoring(CoverageLayer::Contract, Some("ac-one"))],
            ..spec(&["ac-one"])
        };
        let coverage = coverage(&spec, "");
        assert_eq!(
            coverage.criteria[0].missing,
            vec![CoverageLayer::Impl, CoverageLayer::Test]
        );
    }

    #[test]
    fn criterion_is_missing_the_layers_without_an_ac_qualified_annotation() {
        let spec = spec(&["ac-a", "ac-b"]);
        let coverage = coverage(&spec, FULL_COVERAGE_OF_AC_A);
        assert!(coverage.criteria[0].is_covered());
        assert!(!coverage.criteria[1].is_covered());
        assert_eq!(
            coverage.criteria[1].missing,
            vec![CoverageLayer::Impl, CoverageLayer::Test]
        );
    }

    #[test]
    fn per_ac_ignore_waives_one_layer_of_one_criterion() {
        let spec = Spec {
            coverage_ignore: vec![ignoring(CoverageLayer::Impl, Some("ac-a"))],
            ..spec(&["ac-a"])
        };
        let coverage = coverage(&spec, "");
        assert_eq!(coverage.criteria[0].missing, vec![CoverageLayer::Test]);
        assert!(!coverage.criteria[0].is_covered());
    }

    #[test]
    fn whole_layer_ignore_waives_the_layer_for_every_criterion() {
        let spec = Spec {
            coverage_ignore: vec![ignoring(CoverageLayer::Test, None)],
            ..spec(&["ac-a"])
        };
        let coverage = coverage(&spec, "");
        assert_eq!(coverage.criteria[0].missing, vec![CoverageLayer::Impl]);
    }

    #[test]
    fn unimplemented_criterion_marked_done_is_a_gap() {
        let spec = spec(&["ac-a", "ac-b"]);
        let coverage = coverage(&spec, FULL_COVERAGE_OF_AC_A);
        assert_eq!(coverage.health(), SpecHealth::Gap);
        assert_eq!(coverage.implemented_count(), 1);
        assert_eq!(coverage.criteria.len(), 2);
        assert!(!coverage.is_fully_implemented());
    }

    #[test]
    fn implemented_count_ignores_missing_tests() {
        let spec = spec(&["ac-a", "ac-b"]);
        let coverage = coverage(&spec, "// @spec contract a\n// @spec impl a#ac-a\n");
        assert_eq!(coverage.implemented_count(), 1);
        assert_eq!(coverage.health(), SpecHealth::Gap);
    }

    #[test]
    fn spec_with_every_layer_present_is_complete() {
        let spec = spec(&["ac-a"]);
        let coverage = coverage(&spec, FULL_COVERAGE_OF_AC_A);
        assert_eq!(coverage.health(), SpecHealth::Complete);
        assert!(coverage.is_fully_implemented());
    }

    #[test]
    fn unimplemented_criterion_not_done_is_pending_not_a_gap() {
        let spec = Spec {
            acceptance_criteria: vec![testing::criterion("ac-a"), not_done("ac-b")],
            ..spec(&[])
        };
        let coverage = coverage(&spec, FULL_COVERAGE_OF_AC_A);
        assert_eq!(coverage.health(), SpecHealth::Pending);
        assert!(!coverage.has_gap());
        assert!(coverage.has_pending());
    }

    #[test]
    fn unimplemented_criterion_marked_done_is_not_pending() {
        let spec = spec(&["ac-a"]);
        let coverage = coverage(&spec, "// @spec contract a\n");
        assert!(coverage.has_gap());
        assert!(!coverage.has_pending());
    }

    #[test]
    fn missing_per_ac_contract_is_a_gap_under_for_each_ac() {
        let spec = Spec {
            contract: ContractLevel::ForEachAc,
            ..spec(&["ac-a", "ac-b"])
        };
        let annotated_but_for_contract_of_ac_b = "// @spec contract a#ac-a\n// @spec impl a#ac-a\n// @spec test a#ac-a\n// @spec impl a#ac-b\n// @spec test a#ac-b\n";
        let coverage = coverage(&spec, annotated_but_for_contract_of_ac_b);
        assert_eq!(coverage.health(), SpecHealth::Gap);
        assert_eq!(coverage.criteria[1].missing, vec![CoverageLayer::Contract]);
    }

    #[test]
    fn complete_per_ac_contracts_leave_no_gap_under_for_each_ac() {
        let spec = Spec {
            contract: ContractLevel::ForEachAc,
            ..spec(&["ac-a"])
        };
        let coverage = coverage(
            &spec,
            "// @spec contract a#ac-a\n// @spec impl a#ac-a\n// @spec test a#ac-a\n",
        );
        assert_eq!(coverage.health(), SpecHealth::Complete);
    }

    #[test]
    fn missing_per_ac_contract_is_a_gap_when_not_done_but_implemented_under_for_each_ac() {
        let spec = Spec {
            contract: ContractLevel::ForEachAc,
            acceptance_criteria: vec![not_done("ac-a")],
            ..spec(&[])
        };
        let coverage = coverage(&spec, "// @spec impl a#ac-a\n// @spec test a#ac-a\n");
        assert_eq!(coverage.health(), SpecHealth::Gap);
        assert_eq!(coverage.gapped_criteria().len(), 1);
        assert!(coverage.pending_criteria().is_empty());
    }

    #[test]
    fn missing_per_ac_contract_is_pending_when_not_done_and_unimplemented_under_for_each_ac() {
        let spec = Spec {
            contract: ContractLevel::ForEachAc,
            acceptance_criteria: vec![not_done("ac-a")],
            ..spec(&[])
        };
        let coverage = coverage(&spec, "// @spec test a#ac-a\n");
        assert_eq!(coverage.health(), SpecHealth::Pending);
        assert_eq!(coverage.pending_criteria().len(), 1);
    }

    fn not_done_spec() -> Spec {
        Spec {
            acceptance_criteria: vec![not_done("ac-a")],
            ..spec(&[])
        }
    }

    #[test]
    fn not_done_criterion_with_impl_but_no_test_is_a_gap() {
        let spec = not_done_spec();
        let coverage = coverage(&spec, "// @spec contract a\n// @spec impl a#ac-a\n");
        assert!(coverage.criteria[0].is_started);
        assert!(coverage.criteria[0].is_gap());
        assert_eq!(coverage.health(), SpecHealth::Gap);
        assert_eq!(coverage.criteria[0].missing, vec![CoverageLayer::Test]);
    }

    #[test]
    fn not_done_criterion_without_impl_stays_pending_whatever_else_is_annotated() {
        let spec = not_done_spec();
        for annotations in ["", "// @spec test a#ac-a\n", "// @spec contract a\n"] {
            let coverage = coverage(&spec, annotations);
            assert!(!coverage.criteria[0].is_started, "{annotations}");
            assert!(coverage.criteria[0].is_pending(), "{annotations}");
            assert!(!coverage.has_gap(), "{annotations}");
        }
    }

    #[test]
    fn not_done_criterion_with_every_layer_is_covered() {
        let spec = not_done_spec();
        let coverage = coverage(&spec, FULL_COVERAGE_OF_AC_A);
        assert!(coverage.criteria[0].is_covered());
        assert_eq!(coverage.health(), SpecHealth::Complete);
    }

    #[test]
    fn not_done_criterion_with_its_impl_waived_stays_pending() {
        let spec = Spec {
            coverage_ignore: vec![ignoring(CoverageLayer::Impl, Some("ac-a"))],
            ..not_done_spec()
        };
        let coverage = coverage(&spec, "");
        assert!(coverage.criteria[0].is_pending());
        assert!(!coverage.has_gap());
    }

    #[test]
    fn single_contract_is_pending_while_every_criterion_is_not_done_and_unimplemented() {
        let spec = Spec {
            acceptance_criteria: vec![not_done("ac-a"), not_done("ac-b")],
            ..spec(&[])
        };
        let coverage = coverage(&spec, "// @spec test a#ac-a\n");
        assert!(coverage.contract_missing);
        assert!(coverage.has_contract_pending());
        assert!(!coverage.has_contract_gap());
        assert!(!coverage.has_gap());
        assert_eq!(coverage.health(), SpecHealth::Pending);
    }

    #[test]
    fn single_contract_is_a_gap_once_a_criterion_has_an_impl() {
        let spec = Spec {
            acceptance_criteria: vec![not_done("ac-a"), not_done("ac-b")],
            ..spec(&[])
        };
        let coverage = coverage(&spec, "// @spec impl a#ac-b\n// @spec test a#ac-b\n");
        assert!(coverage.has_contract_gap());
        assert!(!coverage.has_contract_pending());
        assert_eq!(coverage.health(), SpecHealth::Gap);
    }

    #[test]
    fn single_contract_is_a_gap_once_a_criterion_is_done() {
        let spec = Spec {
            acceptance_criteria: vec![testing::criterion("ac-a"), not_done("ac-b")],
            ..spec(&[])
        };
        let coverage = coverage(&spec, "");
        assert!(coverage.has_contract_gap());
        assert!(!coverage.has_contract_pending());
    }

    #[test]
    fn single_contract_is_a_gap_for_a_spec_without_criteria() {
        let spec = spec(&[]);
        let coverage = coverage(&spec, "");
        assert!(coverage.has_contract_gap());
    }

    #[test]
    fn present_single_contract_is_neither_a_gap_nor_pending() {
        let spec = not_done_spec();
        let coverage = coverage(&spec, "// @spec contract a\n");
        assert!(!coverage.has_contract_gap());
        assert!(!coverage.has_contract_pending());
    }

    #[test]
    fn gapped_and_pending_criteria_are_split_by_done_flag() {
        let spec = Spec {
            acceptance_criteria: vec![testing::criterion("ac-a"), not_done("ac-b")],
            ..spec(&[])
        };
        let coverage = coverage(&spec, "// @spec contract a\n");
        let ids = |criteria: Vec<&CriterionCoverage>| -> Vec<String> {
            criteria
                .iter()
                .map(|criterion| criterion.id.as_str().to_string())
                .collect()
        };
        assert_eq!(ids(coverage.gapped_criteria()), vec!["ac-a"]);
        assert_eq!(ids(coverage.pending_criteria()), vec!["ac-b"]);
    }
}