adotenv 0.6.0

A tooling-grade .env parser: literal values (no variable expansion), exact byte spans, and edits that leave the rest of the file untouched.
Documentation
use alloc::{collections::BTreeMap, vec::Vec};

use proptest::prelude::*;

use crate::{
    parse_strict, parse_with, AssignmentState, BareKeys, Dialect, DifferenceKind, Duplicates,
};

#[test]
fn comparison_reports_value_state_missing_and_export_changes() {
    let left = parse_strict("A=1\nB=\nC=3\nexport D=4\nBARE\n").unwrap();
    let right = parse_strict("A=2\nB\nD=4\nBARE=value\n").unwrap();
    let comparison = left.compare(&right);

    let found: Vec<_> = comparison
        .differences()
        .iter()
        .map(|entry| (entry.key.as_str(), entry.kind))
        .collect();
    assert_eq!(
        found,
        [
            ("A", DifferenceKind::Value),
            ("B", DifferenceKind::State),
            ("C", DifferenceKind::OnlyLeft),
            ("D", DifferenceKind::Export),
            ("BARE", DifferenceKind::State),
        ]
    );
    let a = &comparison.differences()[0];
    assert_eq!(a.left.as_ref().unwrap().value.as_deref(), Some("1"));
    assert_eq!(a.right.as_ref().unwrap().value.as_deref(), Some("2"));
    let b = &comparison.differences()[1];
    assert_eq!(b.left.as_ref().unwrap().state, AssignmentState::Empty);
    assert_eq!(b.right.as_ref().unwrap().state, AssignmentState::Bare);
    assert!(comparison.differences()[2].right.is_none());
    assert!(comparison.differences()[3].left.as_ref().unwrap().exported);
    assert!(!comparison.differences()[3].right.as_ref().unwrap().exported);
}

#[test]
fn empty_and_absent_are_distinct() {
    let left = parse_strict("A=\n").unwrap();
    let right = parse_strict("").unwrap();
    let comparison = left.compare(&right);
    assert_eq!(comparison.len(), 1);
    let entry = &comparison.differences()[0];
    assert_eq!(entry.key, "A");
    assert_eq!(entry.kind, DifferenceKind::OnlyLeft);
    assert_eq!(entry.left.as_ref().unwrap().state, AssignmentState::Empty);

    let reverse = right.compare(&left);
    assert_eq!(reverse.differences()[0].kind, DifferenceKind::OnlyRight);
    assert_eq!(
        reverse.differences()[0].right.as_ref().unwrap().state,
        AssignmentState::Empty
    );
}

#[test]
fn duplicate_policy_selects_the_effective_declaration() {
    let first = Dialect {
        duplicates: Duplicates::First,
        ..Dialect::PORTABLE
    };
    let last = Dialect {
        duplicates: Duplicates::Last,
        ..Dialect::PORTABLE
    };
    let (left_first, _) = parse_with("A=1\nA=2\n", first);
    let (left_last, _) = parse_with("A=1\nA=2\n", last);
    let right = parse_strict("A=2\n").unwrap();
    assert_eq!(left_first.compare(&right).len(), 1);
    assert!(left_last.compare(&right).is_empty());
}

#[test]
fn nonparticipating_bare_keys_are_not_effective_keys() {
    let dialect = Dialect {
        bare_keys: BareKeys::Ignore,
        ..Dialect::PORTABLE
    };
    let (left, _) = parse_with("A\n", dialect);
    let right = parse_strict("").unwrap();
    assert!(left.compare(&right).is_empty());
}

#[test]
fn identical_documents_compare_empty_and_order_is_stable() {
    let left = parse_strict("Z=1\nA=2\n").unwrap();
    assert!(left.compare(&left).is_empty());

    let right = parse_strict("B=1\n").unwrap();
    let comparison = left.compare(&right);
    let keys: Vec<_> = comparison
        .differences()
        .iter()
        .map(|entry| entry.key.as_str())
        .collect();
    assert_eq!(keys, ["Z", "A", "B"]);
}

proptest! {
    #[test]
    fn comparison_is_symmetric_for_arbitrary_utf8(
        left in "\\PC{0,120}",
        right in "\\PC{0,120}",
    ) {
        let (left, _) = crate::parse(&left);
        let (right, _) = crate::parse(&right);
        let forward = left.compare(&right);
        let reverse = right.compare(&left);
        prop_assert_eq!(forward.len(), reverse.len());
        let by_key: BTreeMap<_, _> = reverse
            .differences()
            .iter()
            .map(|entry| (entry.key.as_str(), entry))
            .collect();
        for a in forward.differences() {
            let b = by_key.get(a.key.as_str()).expect("same key set");
            prop_assert_eq!(&a.left, &b.right);
            prop_assert_eq!(&a.right, &b.left);
            let expected = match b.kind {
                DifferenceKind::OnlyLeft => DifferenceKind::OnlyRight,
                DifferenceKind::OnlyRight => DifferenceKind::OnlyLeft,
                kind => kind,
            };
            prop_assert_eq!(a.kind, expected);
        }
    }
}