rustine 0.1.1

High-performance Gel syntax parser transforming to JSON/XML (Rust + PyO3)
Documentation
//! Byte-exact parity tests: verify that Rust output matches Python Gelatin
//! output character-for-character for all demo grammars × formats.
//!
//! The expected output files are generated by `fixtures/parity/regenerate_outputs.py`
//! using the Python Gelatin library.  Any difference indicates a serialization
//! or execution divergence from the reference implementation.
//!
//! Run with:  `cargo test --test parity_byte_exact`

use rustine::exec::{execute, serialize_tree, serialize_tree_to_writer, RuntimeFormat};
use rustine::parser::lexer::lex;
use rustine::parser::syntax::parse_gel_document;

/// Helper: lex → parse → execute → serialize_tree.
fn run(syntax: &str, input: &str, format: RuntimeFormat) -> String {
    let tokens = lex(syntax).expect("lex");
    let mut doc = parse_gel_document(&tokens).expect("parse");
    let exec = execute(&mut doc, "input", input).expect("execute");
    assert!(exec.error.is_none(), "execution error: {:?}", exec.error);
    serialize_tree(&exec, format)
}

/// Normalize: strip trailing whitespace.
fn norm(s: &str) -> String {
    s.trim_end().to_string()
}

// ==========================================================================
// simple demo
// ==========================================================================

const SIMPLE_SYNTAX: &str = include_str!("../fixtures/parity/simple/syntax1.gel");
const SIMPLE_INPUT: &str = include_str!("../fixtures/parity/simple/input1.txt");
const SIMPLE_JSON: &str = include_str!("../fixtures/parity/simple/output1.json");
const SIMPLE_XML: &str = include_str!("../fixtures/parity/simple/output1.xml");

#[test]
fn byte_exact_simple_json() {
    let actual = run(SIMPLE_SYNTAX, SIMPLE_INPUT, RuntimeFormat::Json);
    assert_eq!(norm(&actual), norm(SIMPLE_JSON), "simple JSON mismatch");
}

#[test]
fn byte_exact_simple_xml() {
    let actual = run(SIMPLE_SYNTAX, SIMPLE_INPUT, RuntimeFormat::Xml);
    assert_eq!(norm(&actual), norm(SIMPLE_XML), "simple XML mismatch");
}

// ==========================================================================
// csv demo
// ==========================================================================

const CSV_SYNTAX: &str = include_str!("../fixtures/parity/csv/syntax1.gel");
const CSV_INPUT: &str = include_str!("../fixtures/parity/csv/input1.txt");
const CSV_JSON: &str = include_str!("../fixtures/parity/csv/output1.json");
const CSV_XML: &str = include_str!("../fixtures/parity/csv/output1.xml");

#[test]
fn byte_exact_csv_json() {
    let actual = run(CSV_SYNTAX, CSV_INPUT, RuntimeFormat::Json);
    assert_eq!(norm(&actual), norm(CSV_JSON), "csv JSON mismatch");
}

#[test]
fn byte_exact_csv_xml() {
    let actual = run(CSV_SYNTAX, CSV_INPUT, RuntimeFormat::Xml);
    assert_eq!(norm(&actual), norm(CSV_XML), "csv XML mismatch");
}

// ==========================================================================
// linesplit demo
// ==========================================================================

const LS_SYNTAX: &str = include_str!("../fixtures/parity/linesplit/syntax1.gel");
const LS_INPUT: &str = include_str!("../fixtures/parity/linesplit/input1.txt");
const LS_JSON: &str = include_str!("../fixtures/parity/linesplit/output1.json");
const LS_XML: &str = include_str!("../fixtures/parity/linesplit/output1.xml");

#[test]
fn byte_exact_linesplit_json() {
    let actual = run(LS_SYNTAX, LS_INPUT, RuntimeFormat::Json);
    assert_eq!(norm(&actual), norm(LS_JSON), "linesplit JSON mismatch");
}

#[test]
fn byte_exact_linesplit_xml() {
    let actual = run(LS_SYNTAX, LS_INPUT, RuntimeFormat::Xml);
    assert_eq!(norm(&actual), norm(LS_XML), "linesplit XML mismatch");
}

// ==========================================================================
// tria demo
// ==========================================================================

const TRIA_SYNTAX: &str = include_str!("../fixtures/parity/tria/syntax1.gel");
const TRIA_INPUT: &str = include_str!("../fixtures/parity/tria/input1.txt");
const TRIA_JSON: &str = include_str!("../fixtures/parity/tria/output1.json");
const TRIA_XML: &str = include_str!("../fixtures/parity/tria/output1.xml");

#[test]
fn byte_exact_tria_json() {
    let actual = run(TRIA_SYNTAX, TRIA_INPUT, RuntimeFormat::Json);
    assert_eq!(norm(&actual), norm(TRIA_JSON), "tria JSON mismatch");
}

#[test]
fn byte_exact_tria_xml() {
    let actual = run(TRIA_SYNTAX, TRIA_INPUT, RuntimeFormat::Xml);
    assert_eq!(norm(&actual), norm(TRIA_XML), "tria XML mismatch");
}

// ==========================================================================
// complex demo
// ==========================================================================

const COMPLEX_SYNTAX: &str = include_str!("../fixtures/parity/complex/syntax1.gel");
const COMPLEX_INPUT: &str = include_str!("../fixtures/parity/complex/input1.txt");
const COMPLEX_JSON: &str = include_str!("../fixtures/parity/complex/output1.json");
const COMPLEX_XML: &str = include_str!("../fixtures/parity/complex/output1.xml");

#[test]
fn byte_exact_complex_json() {
    let actual = run(COMPLEX_SYNTAX, COMPLEX_INPUT, RuntimeFormat::Json);
    assert_large_eq(&actual, COMPLEX_JSON, "complex JSON");
}

#[test]
fn byte_exact_complex_xml() {
    let actual = run(COMPLEX_SYNTAX, COMPLEX_INPUT, RuntimeFormat::Xml);
    assert_large_eq(&actual, COMPLEX_XML, "complex XML");
}

/// Compare two large strings without dumping the entire content on mismatch.
/// Shows the first divergence with a few lines of surrounding context.
fn assert_large_eq(actual: &str, expected: &str, label: &str) {
    let actual_n = norm(actual);
    let expected_n = norm(expected);
    if actual_n == expected_n {
        return;
    }
    let actual_lines: Vec<&str> = actual_n.lines().collect();
    let expected_lines: Vec<&str> = expected_n.lines().collect();

    for (i, (a, e)) in actual_lines.iter().zip(expected_lines.iter()).enumerate() {
        if a != e {
            let start = i.saturating_sub(3);
            let end = (i + 4).min(actual_lines.len()).min(expected_lines.len());
            let mut msg = format!(
                "{label} mismatch at line {} (actual {} lines, expected {} lines):\n",
                i + 1,
                actual_lines.len(),
                expected_lines.len()
            );
            for j in start..end {
                let marker = if j == i { ">>>" } else { "   " };
                msg.push_str(&format!(
                    "{marker} actual[{j}]:   {}\n{marker} expect[{j}]:   {}\n",
                    actual_lines.get(j).unwrap_or(&"<EOF>"),
                    expected_lines.get(j).unwrap_or(&"<EOF>"),
                ));
            }
            panic!("{msg}");
        }
    }
    // Lines matched up to the shorter length — lengths differ
    panic!(
        "{label} mismatch: actual has {} lines, expected has {} lines",
        actual_lines.len(),
        expected_lines.len()
    );
}

// ==========================================================================
// Streaming writer parity: verify serialize_tree_to_writer matches serialize_tree
// ==========================================================================

/// Helper: lex → parse → execute, return ExecutionResult.
fn run_exec(syntax: &str, input: &str) -> rustine::exec::ExecutionResult {
    let tokens = lex(syntax).expect("lex");
    let mut doc = parse_gel_document(&tokens).expect("parse");
    let exec = execute(&mut doc, "input", input).expect("execute");
    assert!(exec.error.is_none(), "execution error: {:?}", exec.error);
    exec
}

#[test]
fn streaming_writer_matches_string_json() {
    let exec = run_exec(SIMPLE_SYNTAX, SIMPLE_INPUT);
    let string_out = serialize_tree(&exec, RuntimeFormat::Json);
    let mut buf = Vec::new();
    serialize_tree_to_writer(&exec, RuntimeFormat::Json, &mut buf).expect("write");
    let writer_out = String::from_utf8(buf).expect("utf8");
    assert_eq!(string_out, writer_out, "JSON streaming mismatch");
}

#[test]
fn streaming_writer_matches_string_xml() {
    let exec = run_exec(SIMPLE_SYNTAX, SIMPLE_INPUT);
    let string_out = serialize_tree(&exec, RuntimeFormat::Xml);
    let mut buf = Vec::new();
    serialize_tree_to_writer(&exec, RuntimeFormat::Xml, &mut buf).expect("write");
    let writer_out = String::from_utf8(buf).expect("utf8");
    assert_eq!(string_out, writer_out, "XML streaming mismatch");
}

#[test]
fn streaming_writer_matches_string_yaml() {
    let exec = run_exec(SIMPLE_SYNTAX, SIMPLE_INPUT);
    let string_out = serialize_tree(&exec, RuntimeFormat::Yaml);
    let mut buf = Vec::new();
    serialize_tree_to_writer(&exec, RuntimeFormat::Yaml, &mut buf).expect("write");
    let writer_out = String::from_utf8(buf).expect("utf8");
    assert_eq!(string_out, writer_out, "YAML streaming mismatch");
}