vfstool_lib 0.9.0

A library for constructing and manipulating virtual file systems in Rust, based on OpenMW's VFS implementation.
Documentation
use super::*;

#[test]
fn simulate_swap_changes_winner() {
    let low = TempDir::new("analysis_sim_low");
    let high = TempDir::new("analysis_sim_high");
    low.write("textures/a.dds", b"low");
    high.write("textures/a.dds", b"high");

    let (vfs, index) = VFS::from_directories_with_layer_index([low.path(), high.path()], None);
    let delta = index
        .simulate(
            &vfs,
            ReorderOp::Swap(low.path().to_path_buf(), high.path().to_path_buf()),
        )
        .expect("simulate should succeed");

    assert_eq!(delta.changed_winners, 1);
}

#[test]
fn simulate_move_before_changes_winner() {
    let a = TempDir::new("analysis_sim_move_before_a");
    let b = TempDir::new("analysis_sim_move_before_b");
    let c = TempDir::new("analysis_sim_move_before_c");
    a.write("textures/a.dds", b"a");
    b.write("textures/a.dds", b"b");
    c.write("textures/a.dds", b"c");

    let (vfs, index) = VFS::from_directories_with_layer_index([a.path(), b.path(), c.path()], None);
    let delta = index
        .simulate(
            &vfs,
            ReorderOp::MoveBefore {
                source: c.path().to_path_buf(),
                before: a.path().to_path_buf(),
            },
        )
        .expect("simulate move-before should succeed");

    assert_eq!(delta.changed_winners, 1);
}

#[test]
fn simulate_move_after_changes_winner() {
    let a = TempDir::new("analysis_sim_move_after_a");
    let b = TempDir::new("analysis_sim_move_after_b");
    a.write("textures/a.dds", b"a");
    b.write("textures/a.dds", b"b");

    let (vfs, index) = VFS::from_directories_with_layer_index([a.path(), b.path()], None);
    let delta = index
        .simulate(
            &vfs,
            ReorderOp::MoveAfter {
                source: a.path().to_path_buf(),
                after: b.path().to_path_buf(),
            },
        )
        .expect("simulate move-after should succeed");

    assert_eq!(delta.changed_winners, 1);
}

#[test]
fn simulate_full_order_rejects_duplicate_sources() {
    let a = TempDir::new("analysis_sim_full_dup_a");
    let b = TempDir::new("analysis_sim_full_dup_b");
    a.write("textures/a.dds", b"a");
    b.write("textures/a.dds", b"b");

    let (vfs, index) = VFS::from_directories_with_layer_index([a.path(), b.path()], None);
    let err = index
        .simulate(
            &vfs,
            ReorderOp::FullOrder(vec![a.path().to_path_buf(), a.path().to_path_buf()]),
        )
        .expect_err("duplicate full-order should error");

    assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
}

#[test]
fn simulate_with_buckets_reports_counts() {
    let low = TempDir::new("analysis_sim_bucket_low");
    let high = TempDir::new("analysis_sim_bucket_high");
    low.write("textures/a.dds", b"low");
    high.write("textures/a.dds", b"high");
    low.write("meshes/a.nif", b"low");
    high.write("meshes/a.nif", b"high");

    let (vfs, index) = VFS::from_directories_with_layer_index([low.path(), high.path()], None);
    let opts = SimOpts {
        sample_limit: 10,
        impact_buckets: vec!["textures/**".into(), "meshes/**".into()],
    };
    let delta = index
        .simulate_with_opts(
            &vfs,
            ReorderOp::Swap(low.path().to_path_buf(), high.path().to_path_buf()),
            &opts,
        )
        .expect("simulate with buckets should succeed");

    assert_eq!(delta.by_bucket.len(), 2);
    assert_eq!(delta.by_bucket[0].changed_winners, 1);
    assert_eq!(delta.by_bucket[1].changed_winners, 1);
}

#[test]
#[cfg(feature = "zip")]
fn simulate_full_order_preserves_manual_archive_winner() {
    use std::io::Write as _;

    let loose = TempDir::new("analysis_sim_manual_archive_loose");
    loose.write("textures/a.dds", b"loose");
    let archive_root = TempDir::new("analysis_sim_manual_archive_root");
    let archive_path = archive_root.path().join("override.zip");
    let file = fs::File::create(&archive_path).expect("zip should be created");
    let mut writer = zip::ZipWriter::new(file);
    writer
        .start_file("textures/a.dds", zip::write::SimpleFileOptions::default())
        .expect("zip entry should start");
    writer
        .write_all(b"archive")
        .expect("zip entry should write");
    writer.finish().expect("zip should finish");

    let mut vfs = VFS::from_directories([loose.path()], None);
    assert!(vfs.push_archive(&archive_path));
    let index = vfs.layer_index().clone();

    let delta = index
        .simulate(
            &vfs,
            ReorderOp::FullOrder(vec![loose.path().to_path_buf(), archive_path]),
        )
        .expect("simulation should succeed");

    assert_eq!(delta.changed_winners, 0);
    assert_eq!(delta.by_source_gain_loss[1].wins_after, 1);
}

#[test]
fn simulate_impact_scores_with_profile() {
    let low = TempDir::new("analysis_impact_low");
    let high = TempDir::new("analysis_impact_high");
    low.write("scripts/x.lua", b"print('a')\n");
    high.write("scripts/x.lua", b"print('b')\n");

    let (vfs, index) = VFS::from_directories_with_layer_index([low.path(), high.path()], None);
    let opts = SimOpts {
        sample_limit: 50,
        impact_buckets: vec!["scripts/**".into()],
    };
    let profile = ImpactProfile {
        heuristics: vec![ImpactHeuristic {
            name: "scripts-change".into(),
            path_glob: "scripts/**".into(),
            weight: 3.0,
            condition: HeuristicCondition::WinnerChanged,
        }],
    };

    let report = index
        .simulate_impact(
            &vfs,
            ReorderOp::Swap(low.path().to_path_buf(), high.path().to_path_buf()),
            &opts,
            &profile,
        )
        .expect("simulate impact should succeed");

    assert!(report.overall_score > 0.0);
    assert!(!report.top_risky_changes.is_empty());
    assert_eq!(report.by_bucket.len(), 1);
}

#[test]
fn simulate_impact_semantic_score_uses_before_after_winners_only() {
    let low = TempDir::new("analysis_impact_semantic_low");
    let mid = TempDir::new("analysis_impact_semantic_mid");
    let high = TempDir::new("analysis_impact_semantic_high");
    low.write("config/test.json", br#"{"value":2}"#);
    mid.write("config/test.json", br#"{"value":1}"#);
    high.write(
        "config/test.json",
        br#"{
  "value": 1
}"#,
    );

    let (vfs, index) =
        VFS::from_directories_with_layer_index([low.path(), mid.path(), high.path()], None);
    let opts = SimOpts {
        sample_limit: 50,
        impact_buckets: vec!["config/**".into()],
    };
    let profile = ImpactProfile {
        heuristics: vec![ImpactHeuristic {
            name: "semantic-change".into(),
            path_glob: "config/**".into(),
            weight: 5.0,
            condition: HeuristicCondition::WinnerChangedAndSemanticBehaviorChanging,
        }],
    };

    let report = index
        .simulate_impact(
            &vfs,
            ReorderOp::Swap(mid.path().to_path_buf(), high.path().to_path_buf()),
            &opts,
            &profile,
        )
        .expect("simulate impact should succeed");

    assert!(report.overall_score.abs() <= f32::EPSILON);
    assert!(report.top_risky_changes.is_empty());
}