adotenv 0.4.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::{string::String, vec, vec::Vec};
use proptest::prelude::*;

use crate::{
    parse, parse_with, AssignmentState, BareKeys, BomPolicy, Dialect, Duplicates, Multiline,
    PortabilityDeclaration, PortabilityPart, PortabilityReading,
};

fn declaration<'a>(
    reading: &'a PortabilityReading,
    key: &str,
) -> Option<&'a PortabilityDeclaration> {
    reading.parts.iter().find_map(|part| match part {
        PortabilityPart::Declaration(declaration) if declaration.key == key => Some(declaration),
        _ => None,
    })
}

#[test]
fn empty_and_repeated_configurations_have_no_differences() {
    let (doc, _) = parse_with("A=a#b\n", Dialect::PORTABLE);
    assert!(doc.portability(&[]).is_empty());
    assert!(doc
        .portability(&[Dialect::PORTABLE, Dialect::PORTABLE])
        .is_empty());
    assert!(doc.portability(&[Dialect::PORTABLE]).is_empty());
    let (empty, _) = parse_with("", Dialect::PORTABLE);
    assert!(empty.portability(&[Dialect::NODE_DOTENV]).is_empty());
}

#[test]
fn crlf_line_spans_exclude_the_ending() {
    let source = "A=a#b\r\nB=ok\r\n";
    let (doc, _) = parse_with(source, Dialect::PORTABLE);
    let report = doc.portability(&[Dialect::NODE_DOTENV]);
    assert_eq!(report.len(), 1);
    assert_eq!(&source[report.differences()[0].span.clone()], "A=a#b");
}

#[test]
fn value_and_comment_differences_are_reported_per_line() {
    let source = "A=a#b\nB=ok\n";
    let (doc, _) = parse_with(source, Dialect::PORTABLE);
    let report = doc.portability(&[Dialect::NODE_DOTENV, Dialect::DOCKER_RAW]);
    assert_eq!(report.len(), 1);
    let difference = &report.differences()[0];
    assert_eq!(difference.first_line, 1);
    assert_eq!(difference.last_line, 1);
    assert_eq!(&source[difference.span.clone()], "A=a#b");
    assert_eq!(difference.readings.len(), 3);
    assert_eq!(difference.readings[0].dialect, Dialect::PORTABLE);
    assert_eq!(difference.readings[1].dialect, Dialect::NODE_DOTENV);
    assert_eq!(difference.readings[2].dialect, Dialect::DOCKER_RAW);
    assert_eq!(
        declaration(&difference.readings[0], "A")
            .unwrap()
            .value
            .as_deref(),
        Some("a#b")
    );
    assert_eq!(
        declaration(&difference.readings[1], "A")
            .unwrap()
            .value
            .as_deref(),
        Some("a")
    );
    assert_eq!(
        declaration(&difference.readings[2], "A")
            .unwrap()
            .value
            .as_deref(),
        Some("a#b")
    );
}

#[test]
fn statement_grouping_differences_are_reported() {
    let (doc, _) = parse_with("A=\"x\"B=y\n", Dialect::PORTABLE);
    let report = doc.portability(&[Dialect::COMPOSE]);
    assert_eq!(report.len(), 1);
    let difference = &report.differences()[0];
    assert!(difference.readings[0]
        .parts
        .iter()
        .any(|part| matches!(part, PortabilityPart::Invalid)));
    assert_eq!(
        difference.readings[1]
            .parts
            .iter()
            .filter(|part| matches!(part, PortabilityPart::Declaration(_)))
            .count(),
        2
    );
}

#[test]
fn bare_lookup_participation_differs() {
    let (doc, _) = parse_with("A\n", Dialect::PORTABLE);
    let report = doc.portability(&[Dialect::NODE_DOTENV]);
    assert_eq!(report.len(), 1);
    let difference = &report.differences()[0];
    let portable = declaration(&difference.readings[0], "A").unwrap();
    let node = declaration(&difference.readings[1], "A").unwrap();
    assert_eq!(portable.state, AssignmentState::Bare);
    assert!(portable.participates);
    assert_eq!(node.state, AssignmentState::Bare);
    assert!(!node.participates);
}

#[test]
fn multiline_grouping_differences_cover_every_line() {
    let mut single_line = Dialect::PORTABLE;
    single_line.multiline = Multiline::NONE;
    let (doc, _) = parse_with("A=\"one\nB=two\"\nC=x\n", Dialect::PORTABLE);
    let report = doc.portability(&[single_line]);
    assert_eq!(report.len(), 2);
    assert_eq!(report.differences()[0].first_line, 1);
    assert_eq!(report.differences()[1].first_line, 2);
    assert_eq!(report.differences()[0].readings[0].parts[0], {
        let entry = doc.unique("A").unwrap();
        PortabilityPart::Declaration(PortabilityDeclaration {
            key: String::from(entry.key()),
            value: entry.value().map(String::from),
            state: entry.state(),
            exported: entry.exported(),
            participates: entry.participates_in_lookup(),
            effective_value: entry.value().map(String::from),
            wins_lookup: true,
        })
    });
}

#[test]
fn bare_empty_policy_difference_is_reported() {
    let mut empty = Dialect::PORTABLE;
    empty.bare_keys = BareKeys::Empty;
    let (doc, _) = parse_with("A\n", Dialect::PORTABLE);
    let report = doc.portability(&[empty]);
    assert_eq!(report.len(), 1);
    let difference = &report.differences()[0];
    let portable = declaration(&difference.readings[0], "A").unwrap();
    let empty = declaration(&difference.readings[1], "A").unwrap();
    assert_eq!(portable.effective_value, None);
    assert_eq!(empty.effective_value.as_deref(), Some(""));
    assert!(portable.wins_lookup);
    assert!(empty.wins_lookup);
}

#[test]
fn duplicate_lookup_winner_difference_is_reported() {
    let mut first = Dialect::PORTABLE;
    first.duplicates = Duplicates::First;
    let (doc, _) = parse_with("A=1\nA=2\n", Dialect::PORTABLE);
    let report = doc.portability(&[first]);
    assert_eq!(report.len(), 2);
    assert_eq!(report.differences()[0].first_line, 1);
    assert_eq!(report.differences()[1].first_line, 2);
    assert!(
        !declaration(&report.differences()[0].readings[0], "A")
            .unwrap()
            .wins_lookup
    );
    assert!(
        declaration(&report.differences()[0].readings[1], "A")
            .unwrap()
            .wins_lookup
    );
    assert!(
        declaration(&report.differences()[1].readings[0], "A")
            .unwrap()
            .wins_lookup
    );
    assert!(
        !declaration(&report.differences()[1].readings[1], "A")
            .unwrap()
            .wins_lookup
    );
}

#[test]
fn bom_policy_differences_are_visible() {
    let mut keep_bom = Dialect::PORTABLE;
    keep_bom.bom = BomPolicy::Keep;
    let (doc, _) = parse_with("\u{feff}A=x\n", Dialect::PORTABLE);
    let report = doc.portability(&[keep_bom]);
    assert_eq!(report.len(), 1);
    assert!(report.differences()[0].readings[1]
        .parts
        .iter()
        .any(|part| matches!(part, PortabilityPart::Invalid)));
}

#[test]
fn only_differing_lines_are_reported_in_order() {
    let source = "A=x\nB=a#b\nC=y\n";
    let (doc, _) = parse_with(source, Dialect::PORTABLE);
    let report = doc.portability(&[Dialect::NODE_DOTENV]);
    assert_eq!(report.len(), 1);
    let difference = &report.differences()[0];
    assert_eq!(difference.first_line, 2);
    assert_eq!(&source[difference.span.clone()], "B=a#b");
}

proptest! {
    #[test]
    fn portability_spans_are_valid_and_lines_are_ordered(
        chars in prop::collection::vec(
            prop_oneof![
                Just('A'),
                Just('='),
                Just('"'),
                Just('\''),
                Just('`'),
                Just('\\'),
                Just('\n'),
                Just('\r'),
                Just(' '),
                Just('#'),
                Just('$'),
                any::<char>(),
            ],
            0..400,
        )
    ) {
        let source: String = chars.into_iter().collect();
        let (doc, _) = parse(&source);
        let requested = [
            Dialect::NODE_DOTENV,
            Dialect::PYTHON_DOTENV,
            Dialect::COMPOSE,
            Dialect::DOCKER_RAW,
        ];
        let report = doc.portability(&requested);
        let line_count = doc.line_index().line_count();
        let mut previous = 0;
        for difference in report.differences() {
            prop_assert!(difference.first_line >= 1);
            prop_assert_eq!(difference.last_line, difference.first_line);
            prop_assert!(difference.last_line <= line_count);
            prop_assert!(difference.first_line > previous);
            previous = difference.last_line;
            prop_assert!(source.get(difference.span.clone()).is_some());
            prop_assert!(source.is_char_boundary(difference.span.start));
            prop_assert!(source.is_char_boundary(difference.span.end));
            prop_assert_eq!(difference.readings.len(), 5);
            prop_assert_eq!(difference.readings[0].dialect, Dialect::PORTABLE);
        }
        let reversed = doc.portability(&[
            Dialect::DOCKER_RAW,
            Dialect::COMPOSE,
            Dialect::PYTHON_DOTENV,
            Dialect::NODE_DOTENV,
        ]);
        let lines: Vec<_> = report
            .differences()
            .iter()
            .map(|difference| (difference.first_line, difference.span.clone()))
            .collect();
        let reversed_lines: Vec<_> = reversed
            .differences()
            .iter()
            .map(|difference| (difference.first_line, difference.span.clone()))
            .collect();
        prop_assert_eq!(lines, reversed_lines);
    }
}