mdtablefix 0.5.1

`mdtablefix` unb0rks and reflows Markdown tables so that each column has a uniform width. When the `--wrap` option is used, it also wraps paragraphs and list items to 80 columns.
Documentation
//! Matrix tests for CLI option interactions.

use std::collections::{BTreeMap, BTreeSet};

#[path = "cli_matrix/support.rs"]
mod support;

use support::{
    ALL_FLAGS,
    BASE_MATRIX_CASES,
    ExecutionMode,
    PhysicalCase,
    WrapVariant,
    assert_transform_invariants,
    fixture_path,
    has_flag,
    is_case_id,
    logical_cases,
    non_wrap_signature,
    physical_cases,
    run_physical_case,
};

#[test]
fn matrix_case_ids_are_unique() {
    let mut ids = BTreeSet::new();
    for case in BASE_MATRIX_CASES {
        assert!(is_case_id(case.id), "invalid case id {}", case.id);
        assert!(ids.insert(case.id), "duplicate case id {}", case.id);
    }
}

#[test]
fn matrix_case_ids_accept_documented_characters() {
    assert!(is_case_id("row-001_alpha2"));
}

#[test]
fn matrix_case_fixtures_are_dat_files() {
    for case in BASE_MATRIX_CASES {
        let fixture = fixture_path(case.fixture);
        assert!(fixture.exists(), "missing fixture {}", fixture.display());
        assert_eq!(
            fixture.extension().and_then(|ext| ext.to_str()),
            Some("dat")
        );
    }
}

#[test]
fn matrix_cases_expand_to_stdout_and_in_place() {
    let mut modes_by_logical_id: BTreeMap<String, BTreeSet<ExecutionMode>> = BTreeMap::new();
    for case in physical_cases() {
        modes_by_logical_id
            .entry(case.logical.id)
            .or_default()
            .insert(case.mode);
    }

    for (id, modes) in modes_by_logical_id {
        assert_eq!(
            modes,
            BTreeSet::from([ExecutionMode::Stdout, ExecutionMode::InPlace]),
            "logical case {id} must run in both modes",
        );
    }
}

#[test]
fn matrix_cases_expand_to_wrapped_and_unwrapped() {
    let mut wraps_by_signature: BTreeMap<String, BTreeSet<WrapVariant>> = BTreeMap::new();
    for case in logical_cases() {
        let variant = if case.is_wrapped {
            WrapVariant::Wrapped
        } else {
            WrapVariant::Unwrapped
        };
        wraps_by_signature
            .entry(non_wrap_signature(case.fixture, &case.flags))
            .or_default()
            .insert(variant);
    }

    for (signature, variants) in wraps_by_signature {
        assert_eq!(
            variants,
            BTreeSet::from([WrapVariant::Wrapped, WrapVariant::Unwrapped]),
            "non-wrap signature {signature} must have both wrap variants",
        );
    }
}

#[test]
fn matrix_cases_cover_all_transform_pairs() {
    for (left_index, left) in ALL_FLAGS.iter().enumerate() {
        for right in ALL_FLAGS.iter().skip(left_index + 1) {
            let mut combinations = BTreeSet::new();
            for case in BASE_MATRIX_CASES {
                combinations.insert((has_flag(case, *left), has_flag(case, *right)));
            }
            assert_eq!(
                combinations,
                BTreeSet::from([(false, false), (false, true), (true, false), (true, true)]),
                "missing pair coverage for {} and {}",
                left.as_arg(),
                right.as_arg(),
            );
        }
    }
}

#[test]
fn matrix_cases_enable_and_disable_each_transform() {
    for flag in ALL_FLAGS {
        let mut states = BTreeSet::new();
        for case in BASE_MATRIX_CASES {
            states.insert(has_flag(case, *flag));
        }
        assert_eq!(
            states,
            BTreeSet::from([false, true]),
            "{} must appear enabled and disabled",
            flag.as_arg(),
        );
    }
}

#[test]
fn cli_matrix_snapshots() -> anyhow::Result<()> {
    for logical in logical_cases() {
        let stdout_case = PhysicalCase {
            logical: logical.clone(),
            mode: ExecutionMode::Stdout,
        };
        let in_place_case = PhysicalCase {
            logical,
            mode: ExecutionMode::InPlace,
        };

        let stdout_result = run_physical_case(&stdout_case).expect("run physical case");
        assert!(
            stdout_result.output.status.success(),
            "{} failed with stderr:\n{}",
            stdout_case.snapshot_name(),
            String::from_utf8_lossy(&stdout_result.output.stderr),
        );
        assert_transform_invariants(&stdout_case.logical, &stdout_result.output.stdout)?;

        let in_place_result = run_physical_case(&in_place_case).expect("run physical case");
        assert!(
            in_place_result.output.status.success(),
            "{} failed with stderr:\n{}",
            in_place_case.snapshot_name(),
            String::from_utf8_lossy(&in_place_result.output.stderr),
        );
        assert_transform_invariants(&in_place_case.logical, &in_place_result.file_content)?;

        assert_eq!(
            stdout_result.output.stdout, in_place_result.file_content,
            "{} stdout must match in-place file output",
            stdout_case.logical.id,
        );

        insta::assert_snapshot!(
            stdout_case.snapshot_name(),
            stdout_result.envelope(&stdout_case)
        );
        insta::assert_snapshot!(
            in_place_case.snapshot_name(),
            in_place_result.envelope(&in_place_case),
        );
    }
    Ok(())
}