weavatrix-edit 0.1.3

Deterministic, Unicode-safe edit-plan validation and application for source refactoring
Documentation
use weavatrix_edit::{
    ApplyLimits, BatchLimits, ByteEdit, ByteEditBatch, ErrorCode, Provenance, prepare_byte_edits,
    prepare_byte_edits_owned, prepare_byte_edits_owned_with_limits, prepare_byte_edits_with_limits,
};

const PROVEN: &str = Provenance::EXACT_LSP;

fn limits() -> BatchLimits {
    BatchLimits {
        max_source_bytes: 100,
        max_edits: 100,
        max_before_bytes: 100,
        max_replacement_bytes: 100,
        max_output_bytes: 100,
    }
}

#[test]
fn owned_prepare_matches_borrowed_success_and_failure() {
    let source = "a😀bc";
    let edits = vec![
        ByteEdit::insert(1, "<", PROVEN),
        ByteEdit::insert(1, ">", PROVEN),
        ByteEdit::replace(5..6, "b", "BETA", PROVEN),
    ];
    let borrowed = prepare_byte_edits(source, &edits).unwrap();
    let owned = prepare_byte_edits_owned(source, edits).unwrap();
    assert_eq!(owned.apply(), borrowed.apply());

    let failures = [
        vec![ByteEdit::replace(0..1, "wrong", "A", PROVEN)],
        vec![ByteEdit::replace(2..5, "", "x", PROVEN)],
        vec![
            ByteEdit::replace(0..2, "a", "A", PROVEN),
            ByteEdit::replace(1..5, "😀", "E", PROVEN),
        ],
    ];
    for edits in failures {
        let borrowed = prepare_byte_edits(source, &edits).unwrap_err();
        let owned = prepare_byte_edits_owned(source, edits).unwrap_err();
        assert_eq!(owned.code(), borrowed.code());
        assert_eq!(owned.edit_index(), borrowed.edit_index());
        assert_eq!(owned.related_edit_index(), borrowed.related_edit_index());
    }
}

#[test]
fn builder_preserves_original_coordinates_boundaries_and_insert_order() {
    let mut batch = ByteEditBatch::new("abcd").unwrap();
    batch.push(ByteEdit::insert(1, "<", PROVEN)).unwrap();
    batch.push(ByteEdit::insert(1, "[", PROVEN)).unwrap();
    batch
        .push(ByteEdit::replace(1..3, "bc", "BC", PROVEN))
        .unwrap();
    batch.push(ByteEdit::insert(3, "]", PROVEN)).unwrap();

    assert_eq!(batch.len(), 4);
    assert!(!batch.is_empty());
    assert_eq!(batch.finish().unwrap().apply().text, "a<[BC]d");
}

#[test]
fn interior_insert_conflicts_are_order_independent_and_transactional() {
    let source = "abcd";
    let insert = ByteEdit::insert(2, "X", PROVEN);
    let replace = ByteEdit::replace(1..3, "bc", "Y", PROVEN);

    let mut singles = ByteEditBatch::new(source).unwrap();
    singles.push(insert.clone()).unwrap();
    let error = singles.push(replace.clone()).unwrap_err();
    assert_eq!(error.code(), ErrorCode::OverlappingEdits);
    assert_eq!(error.edit_index(), Some(1));
    assert_eq!(error.related_edit_index(), Some(0));
    assert_eq!(singles.len(), 1);
    assert_eq!(singles.finish().unwrap().apply().text, "abXcd");

    for edits in [
        vec![insert.clone(), replace.clone()],
        vec![replace.clone(), insert.clone()],
    ] {
        let mut batch = ByteEditBatch::new(source).unwrap();
        let error = batch.push_batch(edits).unwrap_err();
        assert_eq!(error.code(), ErrorCode::OverlappingEdits);
        assert!(batch.is_empty());
    }
}

#[test]
fn batch_matches_borrowed_boundary_matrix() {
    let source = "abcd";
    let cases = [
        vec![
            ByteEdit::insert(2, "X", PROVEN),
            ByteEdit::replace(1..3, "bc", "Y", PROVEN),
        ],
        vec![
            ByteEdit::replace(1..3, "bc", "Y", PROVEN),
            ByteEdit::insert(2, "X", PROVEN),
        ],
        vec![
            ByteEdit::insert(1, "L", PROVEN),
            ByteEdit::replace(1..3, "bc", "Y", PROVEN),
        ],
        vec![
            ByteEdit::replace(1..3, "bc", "Y", PROVEN),
            ByteEdit::insert(3, "R", PROVEN),
        ],
        vec![
            ByteEdit::insert(2, "A", PROVEN),
            ByteEdit::insert(2, "B", PROVEN),
        ],
    ];
    for edits in cases {
        let expected = prepare_byte_edits(source, &edits);
        let mut batch = ByteEditBatch::new(source).unwrap();
        let actual = batch.push_batch(edits);
        match (expected, actual) {
            (Ok(expected), Ok(())) => {
                assert_eq!(batch.finish().unwrap().apply(), expected.apply());
            }
            (Err(expected), Err(actual)) => assert_eq!(actual.code(), expected.code()),
            outcome => panic!("batch parity mismatch: {outcome:?}"),
        }
    }
}

#[test]
fn push_batch_rolls_back_and_checks_all_before_values_before_overlap() {
    let mut batch = ByteEditBatch::new("abcdef").unwrap();
    batch.push(ByteEdit::insert(0, "!", PROVEN)).unwrap();
    let error = batch
        .push_batch(vec![
            ByteEdit::replace(1..4, "bcd", "B", PROVEN),
            ByteEdit::replace(2..5, "cde", "C", PROVEN),
            ByteEdit::replace(5..6, "wrong", "F", PROVEN),
        ])
        .unwrap_err();

    assert_eq!(error.code(), ErrorCode::BeforeMismatch);
    assert_eq!(error.edit_index(), Some(3));
    assert_eq!(batch.len(), 1);
    assert_eq!(batch.finish().unwrap().apply().text, "!abcdef");
}

#[test]
fn every_batch_budget_is_hard_and_failed_admission_rolls_back() {
    let mut source_limited = limits();
    source_limited.max_source_bytes = 3;
    assert_eq!(
        ByteEditBatch::with_limits("abcd", source_limited)
            .unwrap_err()
            .code(),
        ErrorCode::PlanTooLarge
    );

    let mut edit_limited = limits();
    edit_limited.max_edits = 1;
    let mut batch = ByteEditBatch::with_limits("ab", edit_limited).unwrap();
    batch.push(ByteEdit::insert(0, "x", PROVEN)).unwrap();
    let error = batch.push(ByteEdit::insert(2, "y", PROVEN)).unwrap_err();
    assert_eq!(
        (error.code(), error.edit_index()),
        (ErrorCode::PlanTooLarge, Some(1))
    );

    let mut before_limited = limits();
    before_limited.max_before_bytes = 1;
    assert_eq!(
        ByteEditBatch::with_limits("ab", before_limited)
            .unwrap()
            .push(ByteEdit::replace(0..2, "ab", "x", PROVEN))
            .unwrap_err()
            .code(),
        ErrorCode::PlanTooLarge
    );

    let mut replacement_limited = limits();
    replacement_limited.max_replacement_bytes = 2;
    let mut batch = ByteEditBatch::with_limits("abc", replacement_limited).unwrap();
    batch.push(ByteEdit::insert(0, "x", PROVEN)).unwrap();
    let error = batch
        .push_batch(vec![
            ByteEdit::insert(1, "y", PROVEN),
            ByteEdit::insert(2, "z", PROVEN),
        ])
        .unwrap_err();
    assert_eq!(
        (error.code(), error.edit_index()),
        (ErrorCode::PlanTooLarge, Some(2))
    );
    assert_eq!(batch.len(), 1);

    let mut output_limited = limits();
    output_limited.max_output_bytes = 4;
    let mut batch = ByteEditBatch::with_limits("abcd", output_limited).unwrap();
    assert_eq!(
        batch
            .push(ByteEdit::insert(0, "x", PROVEN))
            .unwrap_err()
            .code(),
        ErrorCode::OutputTooLarge
    );
    batch
        .push_batch(vec![
            ByteEdit::insert(0, "x", PROVEN),
            ByteEdit::delete(2..3, "c", PROVEN),
        ])
        .unwrap();
    assert_eq!(batch.finish().unwrap().apply().text, "xabd");
}

#[test]
fn output_limit_uses_final_size_and_allows_a_shrinking_batch() {
    let apply_limits = ApplyLimits {
        max_source_bytes: 8,
        max_edits: 2,
        max_output_bytes: 4,
    };
    let edits = vec![
        ByteEdit::insert(0, "x", PROVEN),
        ByteEdit::delete(2..3, "c", PROVEN),
    ];
    assert_eq!(
        prepare_byte_edits_with_limits("abcd", &edits, apply_limits)
            .unwrap()
            .apply()
            .text,
        "xabd"
    );
    assert_eq!(
        prepare_byte_edits_owned_with_limits("abcd", edits, apply_limits)
            .unwrap()
            .apply()
            .text,
        "xabd"
    );

    let too_small = ApplyLimits {
        max_output_bytes: 3,
        ..apply_limits
    };
    assert_eq!(
        prepare_byte_edits_with_limits("abcd", &[], too_small)
            .unwrap_err()
            .code(),
        ErrorCode::OutputTooLarge
    );

    let mut batch_limits = limits();
    batch_limits.max_output_bytes = 3;
    let mut batch = ByteEditBatch::with_limits("abcd", batch_limits).unwrap();
    assert_eq!(
        batch.finish().unwrap_err().code(),
        ErrorCode::OutputTooLarge
    );
    batch = ByteEditBatch::with_limits("abcd", batch_limits).unwrap();
    batch
        .push_batch(vec![ByteEdit::delete(0..1, "a", PROVEN)])
        .unwrap();
    assert_eq!(batch.finish().unwrap().apply().text, "bcd");
}