mathlex 0.4.1

Mathematical expression parser for LaTeX and plain text notation, producing a language-agnostic AST
Documentation
#![cfg(feature = "serde")]
//! Variant stability test — verifies that no ExprKind variant was removed.
//!
//! The manifest lists every variant name that existed when the fixture set was
//! generated. Adding variants is allowed; removing or renaming one fails this
//! test, which is the desired behaviour for wire-format stability.

use std::fs;
use std::path::Path;

/// All ExprKind variant names in the wire-format `"kind"` field.
///
/// This list is the ground truth. If a variant is renamed or removed, update
/// this list AND bump the crate major version (breaking wire-format change).
const KNOWN_VARIANTS: &[&str] = &[
    "Integer",
    "Float",
    "Rational",
    "Complex",
    "Quaternion",
    "Variable",
    "Constant",
    "Binary",
    "Unary",
    "Function",
    "Derivative",
    "PartialDerivative",
    "Integral",
    "MultipleIntegral",
    "ClosedIntegral",
    "Limit",
    "Sum",
    "Product",
    "Vector",
    "Matrix",
    "Equation",
    "Inequality",
    "ForAll",
    "Exists",
    "Logical",
    "MarkedVector",
    "DotProduct",
    "CrossProduct",
    "OuterProduct",
    "Gradient",
    "Divergence",
    "Curl",
    "Laplacian",
    "Nabla",
    "Determinant",
    "Trace",
    "Rank",
    "ConjugateTranspose",
    "MatrixInverse",
    "NumberSetExpr",
    "SetOperation",
    "SetRelationExpr",
    "SetBuilder",
    "EmptySet",
    "PowerSet",
    "Tensor",
    "KroneckerDelta",
    "LeviCivita",
    "FunctionSignature",
    "Composition",
    "Differential",
    "WedgeProduct",
    "Relation",
];

#[test]
fn manifest_file_lists_all_known_variants() {
    let manifest_path =
        Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/serde/variant_manifest.txt");

    if std::env::var("MATHLEX_UPDATE_GOLDEN").is_ok() {
        fs::create_dir_all(manifest_path.parent().unwrap()).unwrap();
        let content = KNOWN_VARIANTS.join("\n") + "\n";
        fs::write(&manifest_path, content).unwrap();
        return;
    }

    let manifest = fs::read_to_string(&manifest_path).unwrap_or_else(|_| {
        panic!(
            "Missing variant manifest: {}\nRun with MATHLEX_UPDATE_GOLDEN=1 to generate",
            manifest_path.display()
        )
    });

    let manifest_variants: Vec<&str> = manifest.lines().filter(|l| !l.is_empty()).collect();

    for variant in manifest_variants.iter() {
        assert!(
            KNOWN_VARIANTS.contains(variant),
            "Variant '{}' from manifest is no longer in KNOWN_VARIANTS — \
             this is a breaking wire-format change. Update KNOWN_VARIANTS \
             and bump the major version.",
            variant
        );
    }
}

#[test]
fn all_known_variants_have_golden_fixture() {
    let fixture_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/serde");

    if std::env::var("MATHLEX_UPDATE_GOLDEN").is_ok() {
        // Fixtures are generated by golden_fixtures tests; nothing to do here.
        return;
    }

    // Convert CamelCase variant name to snake_case fixture stem.
    // e.g. "PartialDerivative" -> "partial_derivative"
    fn to_snake_case(s: &str) -> String {
        let mut out = String::new();
        for (i, ch) in s.chars().enumerate() {
            if ch.is_uppercase() && i > 0 {
                out.push('_');
            }
            out.extend(ch.to_lowercase());
        }
        out
    }

    // Map variant names to their expected fixture file names.
    // Some variants share a fixture prefix — we check by scanning for any
    // file whose stem starts with the snake_case variant name.
    for variant in KNOWN_VARIANTS {
        let snake = to_snake_case(variant);
        // Find any fixture whose stem equals or starts with the snake_case variant name.
        let found = fixture_dir
            .read_dir()
            .map(|rd| {
                rd.filter_map(|e| e.ok())
                    .filter_map(|e| {
                        e.path()
                            .file_stem()
                            .and_then(|s| s.to_str().map(|s| s.to_owned()))
                    })
                    .any(|stem| stem == snake || stem.starts_with(&format!("{}_", snake)))
            })
            .unwrap_or(false);

        assert!(
            found,
            "No golden fixture found for variant '{}' (expected file starting with \
             'tests/fixtures/serde/{}.json'). \
             Run with MATHLEX_UPDATE_GOLDEN=1 after adding a test in golden_fixtures.rs.",
            variant, snake
        );
    }
}