helm-schema-ir 0.0.4

Generate an accurate JSON schema for any helm chart
Documentation
use crate::{
    ContractProvenance, ContractUse, Guard, GuardValue, ResourceRef, SourceSpan, ValueKind,
    YamlPath,
};
use test_util::prelude::sim_assert_eq;

use super::{canonicalize_contract_uses, expand_condition_disjuncts, normalize_contract_uses};

#[test]
fn disjunct_expansion_deduplicates_identical_rows_before_subsumption() {
    let row = ContractUse::new(
        "feature.enabled".to_string(),
        YamlPath(vec!["spec".to_string(), "enabled".to_string()]),
        ValueKind::Scalar,
        vec![Guard::Truthy {
            path: "feature.enabled".to_string(),
        }],
        None,
    );
    let mut uses = vec![row.clone(), row];

    expand_condition_disjuncts(&mut uses);

    sim_assert_eq!(have: uses.len(), want: 1);
}

#[test]
fn canonicalization_merges_provenance_for_semantically_identical_uses() {
    let mut uses = vec![
        ContractUse {
            source_expr: "image.tag".to_string(),
            path: YamlPath(vec!["spec".to_string(), "tag".to_string()]),
            kind: ValueKind::Scalar,
            condition: helm_schema_core::GuardDnf::from_guards(Vec::new()),
            resource: None,
            provenance: vec![ContractProvenance::new(
                "templates/a.yaml",
                SourceSpan::new(10, 20),
                Vec::new(),
            )],
            has_string_contract: false,
            template_supplied_member_keys: std::collections::BTreeSet::default(),
            split_segment: None,
            merge_layers: None,
            range_key: false,
            nil_omitting: false,
            omitted_members: std::collections::BTreeMap::default(),
            digest: false,
            merge_operand: false,
        },
        ContractUse {
            source_expr: "image.tag".to_string(),
            path: YamlPath(vec!["spec".to_string(), "tag".to_string()]),
            kind: ValueKind::Scalar,
            condition: helm_schema_core::GuardDnf::from_guards(Vec::new()),
            resource: None,
            provenance: vec![ContractProvenance::new(
                "templates/b.yaml",
                SourceSpan::new(30, 40),
                vec!["helper.render".to_string()],
            )],
            has_string_contract: false,
            template_supplied_member_keys: std::collections::BTreeSet::default(),
            split_segment: None,
            merge_layers: None,
            range_key: false,
            nil_omitting: false,
            omitted_members: std::collections::BTreeMap::default(),
            digest: false,
            merge_operand: false,
        },
    ];

    canonicalize_contract_uses(&mut uses);

    sim_assert_eq!(have: uses.len(), want: 1);
    sim_assert_eq!(have: uses[0].provenance.len(), want: 2);
}

#[test]
fn canonicalization_merges_complementary_conditions_across_render_sites() {
    let mut uses = vec![
        ContractUse::with_provenances(
            "image.tag".to_string(),
            YamlPath(vec!["spec".to_string(), "tag".to_string()]),
            ValueKind::Scalar,
            vec![Guard::Truthy {
                path: "feature.enabled".to_string(),
            }],
            None,
            vec![ContractProvenance::new(
                "templates/a.yaml",
                SourceSpan::new(10, 20),
                Vec::new(),
            )],
        ),
        ContractUse::with_provenances(
            "image.tag".to_string(),
            YamlPath(vec!["spec".to_string(), "tag".to_string()]),
            ValueKind::Scalar,
            vec![Guard::Not {
                path: "feature.enabled".to_string(),
            }],
            None,
            vec![ContractProvenance::new(
                "templates/b.yaml",
                SourceSpan::new(30, 40),
                Vec::new(),
            )],
        ),
    ];

    canonicalize_contract_uses(&mut uses);

    sim_assert_eq!(have: uses.len(), want: 1);
    sim_assert_eq!(have: uses[0].condition.guard_conjunctions(), want: vec![vec![]]);
    sim_assert_eq!(have: uses[0].provenance.len(), want: 2);
}

#[test]
fn canonicalization_collapses_conditions_from_the_same_render_site() {
    let provenance = ContractProvenance::new(
        "templates/deployment.yaml",
        SourceSpan::new(10, 20),
        vec!["helper.render".to_string()],
    );
    let mut uses = vec![
        ContractUse::with_provenances(
            "image.tag".to_string(),
            YamlPath(vec!["spec".to_string(), "tag".to_string()]),
            ValueKind::Scalar,
            vec![Guard::Truthy {
                path: "feature.enabled".to_string(),
            }],
            None,
            vec![provenance.clone()],
        ),
        ContractUse::with_provenances(
            "image.tag".to_string(),
            YamlPath(vec!["spec".to_string(), "tag".to_string()]),
            ValueKind::Scalar,
            vec![Guard::Not {
                path: "feature.enabled".to_string(),
            }],
            None,
            vec![provenance],
        ),
    ];

    canonicalize_contract_uses(&mut uses);

    sim_assert_eq!(have: uses.len(), want: 1);
    sim_assert_eq!(have: uses[0].condition.guard_conjunctions(), want: vec![vec![]]);
}

#[test]
fn normalization_drops_same_site_branch_subsumed_by_self_truthy_branch() {
    let provenance = ContractProvenance::new(
        "<inline:utils>",
        SourceSpan::new(1195, 1576),
        vec!["common.utils.getValueFromKey".to_string()],
    );
    let resource = Some(ResourceRef::concrete(
        "v1".to_string(),
        "Secret".to_string(),
    ));
    let base_guards = vec![Guard::NotEq {
        path: "auth.username".to_string(),
        value: GuardValue::string("postgres"),
    }];
    let mut self_truthy_guards = base_guards.clone();
    self_truthy_guards.insert(
        0,
        Guard::Truthy {
            path: "auth.password".to_string(),
        },
    );
    let mut uses = vec![
        ContractUse::with_provenances(
            "auth.password".to_string(),
            YamlPath(Vec::new()),
            ValueKind::Scalar,
            base_guards,
            resource.clone(),
            vec![provenance.clone()],
        ),
        ContractUse::with_provenances(
            "auth.password".to_string(),
            YamlPath(Vec::new()),
            ValueKind::Scalar,
            self_truthy_guards,
            resource,
            vec![provenance],
        ),
    ];

    normalize_contract_uses(&mut uses);

    sim_assert_eq!(have: uses.len(), want: 1);
    assert!(
        uses[0]
            .single_guard_conjunction()
            .iter()
            .any(|guard| { matches!(guard, Guard::Truthy { path } if path == "auth.password") })
    );
}

#[test]
fn normalization_drops_subsumed_truthy_branch_across_provenance_sites() {
    let resource = Some(ResourceRef::concrete(
        "v1".to_string(),
        "Secret".to_string(),
    ));
    let base_guards = vec![Guard::NotEq {
        path: "auth.username".to_string(),
        value: GuardValue::string("postgres"),
    }];
    let mut self_truthy_guards = base_guards.clone();
    self_truthy_guards.push(Guard::Truthy {
        path: "auth.password".to_string(),
    });
    let mut uses = vec![
        ContractUse::with_provenances(
            "auth.password".to_string(),
            YamlPath(Vec::new()),
            ValueKind::Scalar,
            base_guards.clone(),
            resource.clone(),
            vec![ContractProvenance::new(
                "templates/first.yaml",
                SourceSpan::new(10, 20),
                Vec::new(),
            )],
        ),
        ContractUse::with_provenances(
            "auth.password".to_string(),
            YamlPath(Vec::new()),
            ValueKind::Scalar,
            self_truthy_guards,
            resource,
            vec![ContractProvenance::new(
                "templates/second.yaml",
                SourceSpan::new(30, 40),
                Vec::new(),
            )],
        ),
    ];

    normalize_contract_uses(&mut uses);

    sim_assert_eq!(have: uses.len(), want: 1);
    sim_assert_eq!(
        have: uses[0].single_guard_conjunction(),
        want: base_guards
    );
    sim_assert_eq!(have: uses[0].provenance.len(), want: 2);
}