dotenv-verbatim 0.3.1

A .env loader that takes the value verbatim: no expansion, no escapes, no inline comments; one malformed line is skipped, not the rest of the file
Documentation
//! The only tests that touch the process environment. Every case runs inside one `#[test]` on
//! purpose: `set_var` must not run concurrently with another test reading the environment, and
//! cargo runs `#[test]` functions of one binary in parallel threads.

use std::env;
use std::fs;
use std::path::PathBuf;

const ABSENT_KEY: &str = "DOTENV_VERBATIM_TEST_ABSENT";
const PRESET_KEY: &str = "DOTENV_VERBATIM_TEST_PRESET";
const DUPLICATE_KEY: &str = "DOTENV_VERBATIM_TEST_DUPLICATE";
const PRESET_DUPLICATE_KEY: &str = "DOTENV_VERBATIM_TEST_PRESET_DUPLICATE";
const BEFORE_BAD_LINE_KEY: &str = "DOTENV_VERBATIM_TEST_BEFORE_BAD_LINE";
const AFTER_BAD_LINE_KEY: &str = "DOTENV_VERBATIM_TEST_AFTER_BAD_LINE";
const MISSING_FILE_KEY: &str = "DOTENV_VERBATIM_TEST_MISSING_FILE";
const EMPTY_VALUE_KEY: &str = "DOTENV_VERBATIM_TEST_EMPTY_VALUE";

#[test]
fn load_reports_what_it_applied() {
    applies_a_variable_the_process_does_not_carry();
    reports_a_variable_the_process_carries_as_overridden();
    reports_a_second_occurrence_as_repeated();
    reports_an_overridden_key_repeated_in_the_file();
    reports_an_unusable_line_by_number_and_applies_the_rest();
    reports_a_missing_file_as_not_found();
    applies_an_empty_value_as_an_empty_string();
}

fn applies_a_variable_the_process_does_not_carry() {
    let loaded = load_content("absent", &format!("{ABSENT_KEY}=from-file\n"));

    assert_eq!(loaded.applied, vec![ABSENT_KEY]);
    assert!(loaded.overridden.is_empty());
    assert_eq!(env::var(ABSENT_KEY).unwrap(), "from-file");
}

fn reports_a_variable_the_process_carries_as_overridden() {
    env::set_var(PRESET_KEY, "from-process");

    let loaded = load_content("preset", &format!("{PRESET_KEY}=from-file\n"));

    assert_eq!(loaded.overridden, vec![PRESET_KEY]);
    assert!(loaded.applied.is_empty());
    assert_eq!(env::var(PRESET_KEY).unwrap(), "from-process");
}

fn reports_a_second_occurrence_as_repeated() {
    let content = format!("{DUPLICATE_KEY}=first\n{DUPLICATE_KEY}=second\n");

    let loaded = load_content("duplicate", &content);

    assert_eq!(loaded.applied, vec![DUPLICATE_KEY]);
    assert_eq!(loaded.repeated, vec![DUPLICATE_KEY]);
    assert_eq!(env::var(DUPLICATE_KEY).unwrap(), "first");
}

fn reports_an_overridden_key_repeated_in_the_file() {
    env::set_var(PRESET_DUPLICATE_KEY, "from-process");
    let content = format!("{PRESET_DUPLICATE_KEY}=first\n{PRESET_DUPLICATE_KEY}=second\n");

    let loaded = load_content("preset-duplicate", &content);

    assert_eq!(loaded.overridden, vec![PRESET_DUPLICATE_KEY]);
    assert_eq!(loaded.repeated, vec![PRESET_DUPLICATE_KEY]);
    assert!(loaded.applied.is_empty());
    assert_eq!(env::var(PRESET_DUPLICATE_KEY).unwrap(), "from-process");
}

fn reports_an_unusable_line_by_number_and_applies_the_rest() {
    let content = format!("{BEFORE_BAD_LINE_KEY}=1\nnosep\n{AFTER_BAD_LINE_KEY}=2\n");

    let loaded = load_content("unusable-line", &content);

    assert_eq!(loaded.skipped, vec![2]);
    assert_eq!(
        loaded.applied,
        vec![BEFORE_BAD_LINE_KEY, AFTER_BAD_LINE_KEY]
    );
    assert_eq!(env::var(AFTER_BAD_LINE_KEY).unwrap(), "2");
}

fn reports_a_missing_file_as_not_found() {
    // Written and removed, so the path once had usable content: nothing of it may be applied.
    let env_file = temp_path("missing");
    fs::write(&env_file, format!("{MISSING_FILE_KEY}=from-file\n")).unwrap();
    fs::remove_file(&env_file).unwrap();

    let loaded = dotenv_verbatim::load(&env_file);

    assert!(!loaded.found);
    assert!(loaded.applied.is_empty());
    assert!(loaded.overridden.is_empty());
    assert!(loaded.repeated.is_empty());
    assert!(loaded.skipped.is_empty());
    assert!(env::var_os(MISSING_FILE_KEY).is_none());
}

fn applies_an_empty_value_as_an_empty_string() {
    let loaded = load_content("empty-value", &format!("{EMPTY_VALUE_KEY}=\n"));

    assert_eq!(loaded.applied, vec![EMPTY_VALUE_KEY]);
    assert_eq!(env::var(EMPTY_VALUE_KEY).unwrap(), "");
}

/// Write the content to a temp file named after `tag`, load it, and remove the file.
fn load_content(tag: &str, content: &str) -> dotenv_verbatim::Loaded {
    let env_file = temp_path(tag);
    fs::write(&env_file, content).unwrap();

    let loaded = dotenv_verbatim::load(&env_file);

    fs::remove_file(&env_file).unwrap();
    assert!(loaded.found);
    loaded
}

fn temp_path(tag: &str) -> PathBuf {
    env::temp_dir().join(format!("dotenv-verbatim-{}-{tag}.env", std::process::id()))
}