diffo 0.2.0

Semantic diffing for Rust structs via serde
Documentation
use diffo::*;
use serde::Serialize;

#[test]
fn test_index_based_default() {
    let old = vec![1, 2, 3];
    let new = vec![1, 2, 4];

    let diff = diff(&old, &new).unwrap();
    // Should show change at index 2
    assert!(!diff.is_empty());
    assert!(diff.get("[2]").is_some());
}

#[test]
fn test_myers_insertion() {
    let old = vec![1, 2, 3];
    let new = vec![1, 5, 2, 3];

    let config = DiffConfig::new().default_sequence_algorithm(SequenceDiffAlgorithm::Myers);

    let diff = diff_with(&old, &new, &config).unwrap();

    // Myers should detect insertion at index 1
    assert!(!diff.is_empty());
    assert!(diff.get("[1]").is_some()); // 5 added
}

#[test]
fn test_myers_deletion() {
    let old = vec![1, 2, 3, 4];
    let new = vec![1, 3, 4];

    let config = DiffConfig::new().default_sequence_algorithm(SequenceDiffAlgorithm::Myers);

    let diff = diff_with(&old, &new, &config).unwrap();

    // Should show deletion
    assert!(!diff.is_empty());
}

#[test]
fn test_patience_reordering() {
    #[derive(Serialize, PartialEq, Eq, Hash)]
    struct Item {
        id: u64,
        name: String,
    }

    let old = vec![
        Item {
            id: 1,
            name: "alpha".into(),
        },
        Item {
            id: 2,
            name: "beta".into(),
        },
        Item {
            id: 3,
            name: "gamma".into(),
        },
    ];

    let new = vec![
        Item {
            id: 2,
            name: "beta".into(),
        },
        Item {
            id: 3,
            name: "gamma".into(),
        },
        Item {
            id: 4,
            name: "delta".into(),
        },
        Item {
            id: 1,
            name: "alpha".into(),
        },
    ];

    let config = DiffConfig::new().default_sequence_algorithm(SequenceDiffAlgorithm::Patience);

    let diff = diff_with(&old, &new, &config).unwrap();

    // Patience should handle reordering better
    assert!(!diff.is_empty());
}

#[test]
fn test_per_path_algorithm() {
    #[derive(Serialize)]
    struct Data {
        users: Vec<String>,
        logs: Vec<String>,
    }

    let old = Data {
        users: vec!["alice".into(), "bob".into()],
        logs: vec!["log1".into(), "log2".into(), "log3".into()],
    };

    let new = Data {
        users: vec!["alice".into(), "charlie".into(), "bob".into()],
        logs: vec!["log1".into(), "log2".into(), "log4".into()],
    };

    let config = DiffConfig::new()
        .sequence_algorithm("users", SequenceDiffAlgorithm::Patience)
        .sequence_algorithm("logs", SequenceDiffAlgorithm::IndexBased);

    let diff = diff_with(&old, &new, &config).unwrap();

    // Should have changes in both users and logs
    assert!(!diff.is_empty());
}

#[test]
fn test_all_algorithms_produce_valid_diff() {
    let old = vec![1, 2, 3, 4, 5];
    let new = vec![1, 6, 2, 3, 5];

    // Test each algorithm
    for algo in [
        SequenceDiffAlgorithm::IndexBased,
        SequenceDiffAlgorithm::Myers,
        SequenceDiffAlgorithm::Patience,
    ] {
        let config = DiffConfig::new().default_sequence_algorithm(algo);
        let diff = diff_with(&old, &new, &config).unwrap();

        // All algorithms should produce a valid diff (non-empty for this case)
        assert!(!diff.is_empty(), "Algorithm {:?} failed", algo);
    }
}

#[test]
fn test_identical_sequences() {
    let old = vec![1, 2, 3];
    let new = vec![1, 2, 3];

    // All algorithms should show no diff
    for algo in [
        SequenceDiffAlgorithm::IndexBased,
        SequenceDiffAlgorithm::Myers,
        SequenceDiffAlgorithm::Patience,
    ] {
        let config = DiffConfig::new().default_sequence_algorithm(algo);
        let diff = diff_with(&old, &new, &config).unwrap();

        assert!(diff.is_empty(), "Algorithm {:?} should show no diff", algo);
    }
}

#[test]
fn test_completely_different_sequences() {
    let old = vec![1, 2, 3];
    let new = vec![4, 5, 6];

    // All algorithms should detect complete change
    for algo in [
        SequenceDiffAlgorithm::IndexBased,
        SequenceDiffAlgorithm::Myers,
        SequenceDiffAlgorithm::Patience,
    ] {
        let config = DiffConfig::new().default_sequence_algorithm(algo);
        let diff = diff_with(&old, &new, &config).unwrap();

        assert!(
            !diff.is_empty(),
            "Algorithm {:?} should detect changes",
            algo
        );
        assert_eq!(diff.len(), 3, "Algorithm {:?} should show 3 changes", algo);
    }
}

#[test]
fn test_empty_sequences() {
    let old: Vec<i32> = vec![];
    let new: Vec<i32> = vec![];

    // All algorithms should handle empty sequences
    for algo in [
        SequenceDiffAlgorithm::IndexBased,
        SequenceDiffAlgorithm::Myers,
        SequenceDiffAlgorithm::Patience,
    ] {
        let config = DiffConfig::new().default_sequence_algorithm(algo);
        let diff = diff_with(&old, &new, &config).unwrap();

        assert!(
            diff.is_empty(),
            "Algorithm {:?} should handle empty sequences",
            algo
        );
    }
}

#[test]
fn test_one_empty_sequence() {
    let old = vec![1, 2, 3];
    let new: Vec<i32> = vec![];

    // All algorithms should detect removals
    for algo in [
        SequenceDiffAlgorithm::IndexBased,
        SequenceDiffAlgorithm::Myers,
        SequenceDiffAlgorithm::Patience,
    ] {
        let config = DiffConfig::new().default_sequence_algorithm(algo);
        let diff = diff_with(&old, &new, &config).unwrap();

        assert!(
            !diff.is_empty(),
            "Algorithm {:?} should detect removals",
            algo
        );
    }
}

#[test]
fn test_collection_size_limit_with_algorithms() {
    let old: Vec<i32> = (0..2000).collect();
    let new: Vec<i32> = (0..2000).collect();

    // Test with small limit for all algorithms
    for algo in [
        SequenceDiffAlgorithm::IndexBased,
        SequenceDiffAlgorithm::Myers,
        SequenceDiffAlgorithm::Patience,
    ] {
        let config = DiffConfig::new()
            .default_sequence_algorithm(algo)
            .collection_limit(100);

        let diff = diff_with(&old, &new, &config).unwrap();

        // Should be elided due to size limit
        assert!(
            !diff.is_empty(),
            "Algorithm {:?} should respect size limits",
            algo
        );
        let change = diff.changes().values().next().unwrap();
        assert!(
            change.is_elided(),
            "Algorithm {:?} should elide large collections",
            algo
        );
    }
}