mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-24 15:01:38 +00:00
Add comprehensive syncback roundtrip and clean mode stress tests
- Completed the implementation of roundtrip tests to validate the syncback process, ensuring that building a project, syncing back, and rebuilding produces semantically identical outputs. - Created a new utility module for roundtrip testing, including functions for running builds and asserting directory equality. - Developed clean mode stress tests to verify that syncing back to a dirty directory yields identical results to syncing back to a fresh directory, covering various mutation scenarios. - Updated existing test modules to incorporate new test cases and ensure consistency in handling standalone scripts transitioning to directory format when children are added.
This commit is contained in:
parent
17d250f26a
commit
4a66eff938
10 changed files with 1517 additions and 50 deletions
522
tests/tests/clean_mode_stress.rs
Normal file
522
tests/tests/clean_mode_stress.rs
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
//! Stress tests proving clean mode produces consistent results.
|
||||
//!
|
||||
//! Core invariant: syncing back to a DIRTY directory with clean mode
|
||||
//! should produce IDENTICAL results to syncing back to an EMPTY directory.
|
||||
//!
|
||||
//! These tests apply various "mutations" to dirty up the filesystem, then verify
|
||||
//! clean mode fixes everything. This validates the clean mode implementation
|
||||
//! handles edge cases correctly.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use tempfile::tempdir;
|
||||
|
||||
use crate::rojo_test::io_util::BUILD_TESTS_PATH;
|
||||
use crate::rojo_test::roundtrip_util::{
|
||||
apply_mutation, assert_dirs_equal, copy_project_dir, run_rojo_build, run_rojo_syncback_clean,
|
||||
Mutation,
|
||||
};
|
||||
|
||||
/// Core test: dirty syncback should equal fresh syncback.
|
||||
///
|
||||
/// This is the fundamental invariant we're testing:
|
||||
/// 1. Build rbxl from project
|
||||
/// 2. Syncback to DIR_A (fresh)
|
||||
/// 3. Apply mutations to DIR_A (dirty it up)
|
||||
/// 4. Syncback again to DIR_A (clean mode should fix it)
|
||||
/// 5. Syncback to DIR_B (completely fresh)
|
||||
/// 6. Assert DIR_A == DIR_B
|
||||
fn clean_equals_fresh(test_name: &str, mutations: &[Mutation]) {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let project_path = Path::new(BUILD_TESTS_PATH).join(test_name);
|
||||
|
||||
// 1. Build rbxm from original project (model format, not place)
|
||||
let (_tmp_rbxm, rbxm_path) = run_rojo_build(&project_path, "test.rbxm");
|
||||
|
||||
// 2. Syncback to DIR_A (fresh start - copy full project structure)
|
||||
let dir_a = tempdir().expect("Failed to create dir_a");
|
||||
copy_project_dir(&project_path, dir_a.path());
|
||||
assert!(
|
||||
run_rojo_syncback_clean(dir_a.path(), &rbxm_path),
|
||||
"Initial syncback to dir_a failed for {}",
|
||||
test_name
|
||||
);
|
||||
|
||||
// 3. Apply mutations to DIR_A (dirty it up)
|
||||
for mutation in mutations {
|
||||
apply_mutation(dir_a.path(), mutation);
|
||||
}
|
||||
|
||||
// 4. Run clean syncback again on dirty DIR_A
|
||||
assert!(
|
||||
run_rojo_syncback_clean(dir_a.path(), &rbxm_path),
|
||||
"Clean syncback on dirty dir_a failed for {}",
|
||||
test_name
|
||||
);
|
||||
|
||||
// 5. Syncback to DIR_B (completely fresh - copy full project structure)
|
||||
let dir_b = tempdir().expect("Failed to create dir_b");
|
||||
copy_project_dir(&project_path, dir_b.path());
|
||||
assert!(
|
||||
run_rojo_syncback_clean(dir_b.path(), &rbxm_path),
|
||||
"Fresh syncback to dir_b failed for {}",
|
||||
test_name
|
||||
);
|
||||
|
||||
// 6. CRITICAL ASSERTION: cleaned DIR_A == fresh DIR_B
|
||||
assert_dirs_equal(dir_a.path(), dir_b.path());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ORPHAN FILE TESTS
|
||||
// =============================================================================
|
||||
|
||||
/// Clean mode should remove orphan .luau files that don't exist in the rbxl.
|
||||
#[test]
|
||||
fn clean_removes_orphan_luau_files() {
|
||||
clean_equals_fresh(
|
||||
"deep_nesting",
|
||||
&[Mutation::AddOrphanFile {
|
||||
relative_path: "src/orphan_script.luau",
|
||||
content: "-- I shouldn't exist\nreturn nil",
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
/// Clean mode should remove deeply nested orphan files.
|
||||
#[test]
|
||||
fn clean_removes_nested_orphan_files() {
|
||||
clean_equals_fresh(
|
||||
"deep_nesting",
|
||||
&[Mutation::AddOrphanFile {
|
||||
relative_path: "src/level-1/orphan_in_level1.luau",
|
||||
content: "-- Nested orphan",
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
/// Clean mode should remove orphan .txt files.
|
||||
#[test]
|
||||
fn clean_removes_orphan_txt_files() {
|
||||
clean_equals_fresh(
|
||||
"deep_nesting",
|
||||
&[Mutation::AddOrphanFile {
|
||||
relative_path: "src/orphan.txt",
|
||||
content: "I shouldn't exist",
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
/// Clean mode should remove orphan directories with content.
|
||||
#[test]
|
||||
fn clean_removes_orphan_directories() {
|
||||
clean_equals_fresh(
|
||||
"deep_nesting",
|
||||
&[Mutation::AddOrphanDirectory {
|
||||
relative_path: "src/fake_service",
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
/// Clean mode should remove deeply nested orphan directories.
|
||||
#[test]
|
||||
fn clean_removes_deeply_nested_orphan_directories() {
|
||||
clean_equals_fresh(
|
||||
"deep_nesting",
|
||||
&[
|
||||
Mutation::AddOrphanDirectory {
|
||||
relative_path: "src/level-1/level-2/orphan_folder",
|
||||
},
|
||||
Mutation::AddOrphanFile {
|
||||
relative_path: "src/level-1/level-2/orphan_folder/nested/deep.luau",
|
||||
content: "-- deeply nested orphan",
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FILE RESTORATION TESTS
|
||||
// =============================================================================
|
||||
|
||||
/// Clean mode should restore deleted files.
|
||||
#[test]
|
||||
fn clean_restores_deleted_files() {
|
||||
clean_equals_fresh(
|
||||
"module_init",
|
||||
&[Mutation::DeleteFile {
|
||||
relative_path: "folder/init.luau",
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
/// Clean mode should restore deleted files in nested structures.
|
||||
#[test]
|
||||
fn clean_restores_deleted_nested_files() {
|
||||
clean_equals_fresh(
|
||||
"init_with_children",
|
||||
&[
|
||||
Mutation::DeleteFile {
|
||||
relative_path: "src/init.luau",
|
||||
},
|
||||
Mutation::DeleteFile {
|
||||
relative_path: "src/other.luau",
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FILE RENAME/NAME CORRECTION TESTS
|
||||
// =============================================================================
|
||||
|
||||
/// Clean mode should fix renamed files (creates orphan + restores correct name).
|
||||
#[test]
|
||||
fn clean_fixes_renamed_files() {
|
||||
clean_equals_fresh(
|
||||
"module_in_folder",
|
||||
&[Mutation::RenameFile {
|
||||
from: "folder/aModule.luau",
|
||||
to: "folder/wrongName.luau",
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXTENSION CORRECTION TESTS
|
||||
// =============================================================================
|
||||
|
||||
/// Clean mode should fix wrong file extensions.
|
||||
#[test]
|
||||
fn clean_fixes_wrong_extensions() {
|
||||
clean_equals_fresh(
|
||||
"server_in_folder",
|
||||
&[Mutation::ChangeExtension {
|
||||
from: "folder/serverScript.server.luau",
|
||||
to: "folder/serverScript.modulescript",
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
/// Clean mode should fix .lua -> .luau extension issues.
|
||||
#[test]
|
||||
fn clean_fixes_lua_to_luau_extension() {
|
||||
clean_equals_fresh(
|
||||
"module_in_folder",
|
||||
&[Mutation::ChangeExtension {
|
||||
from: "folder/aModule.luau",
|
||||
to: "folder/aModule.lua",
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FORMAT CONVERSION TESTS (DIR <-> FILE)
|
||||
// =============================================================================
|
||||
|
||||
/// Clean mode should restore directory format from standalone file.
|
||||
#[test]
|
||||
fn clean_restores_dir_from_file() {
|
||||
clean_equals_fresh(
|
||||
"init_with_children",
|
||||
&[Mutation::ConvertDirToFile {
|
||||
dir: "src",
|
||||
file_content: "-- was a directory\nreturn {}",
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
/// Clean mode should restore standalone file from directory format.
|
||||
#[test]
|
||||
fn clean_restores_file_from_dir() {
|
||||
clean_equals_fresh(
|
||||
"module_in_folder",
|
||||
&[Mutation::ConvertFileToDir {
|
||||
file: "folder/aModule.luau",
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// META FILE TESTS
|
||||
// =============================================================================
|
||||
|
||||
/// Clean mode should fix corrupted meta files.
|
||||
#[test]
|
||||
fn clean_fixes_corrupt_meta_files() {
|
||||
clean_equals_fresh(
|
||||
"init_meta_properties",
|
||||
&[Mutation::CorruptMetaFile {
|
||||
relative_path: "Lighting/init.meta.json5",
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
/// Clean mode should remove orphan meta files.
|
||||
#[test]
|
||||
fn clean_removes_orphan_meta_files() {
|
||||
clean_equals_fresh(
|
||||
"deep_nesting",
|
||||
&[Mutation::AddOrphanFile {
|
||||
relative_path: "src/orphan.meta.json5",
|
||||
content: r#"{ "className": "Folder" }"#,
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTENT MODIFICATION TESTS
|
||||
// =============================================================================
|
||||
|
||||
/// Clean mode should restore modified file content.
|
||||
#[test]
|
||||
fn clean_restores_modified_content() {
|
||||
clean_equals_fresh(
|
||||
"module_init",
|
||||
&[Mutation::ModifyFileContent {
|
||||
relative_path: "folder/init.luau",
|
||||
new_content: "-- totally wrong content\nreturn 'hacked'",
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
/// Clean mode should restore modified content in multiple files.
|
||||
#[test]
|
||||
fn clean_restores_multiple_modified_files() {
|
||||
clean_equals_fresh(
|
||||
"init_with_children",
|
||||
&[
|
||||
Mutation::ModifyFileContent {
|
||||
relative_path: "src/init.luau",
|
||||
new_content: "-- wrong init",
|
||||
},
|
||||
Mutation::ModifyFileContent {
|
||||
relative_path: "src/other.luau",
|
||||
new_content: "-- wrong other",
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROJECT FILE TESTS
|
||||
// =============================================================================
|
||||
|
||||
/// Clean mode should remove spurious nested project files.
|
||||
#[test]
|
||||
fn clean_removes_spurious_project_files() {
|
||||
clean_equals_fresh(
|
||||
"deep_nesting",
|
||||
&[Mutation::AddNestedProjectFile {
|
||||
relative_path: "src/fake.project.json5",
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
/// Clean mode should remove multiple spurious project files.
|
||||
#[test]
|
||||
fn clean_removes_multiple_spurious_project_files() {
|
||||
clean_equals_fresh(
|
||||
"deep_nesting",
|
||||
&[
|
||||
Mutation::AddNestedProjectFile {
|
||||
relative_path: "src/fake1.project.json5",
|
||||
},
|
||||
Mutation::AddNestedProjectFile {
|
||||
relative_path: "src/level-1/fake2.project.json5",
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DUPLICATE FILE TESTS
|
||||
// =============================================================================
|
||||
|
||||
/// Clean mode should remove duplicate files with different extensions.
|
||||
#[test]
|
||||
fn clean_removes_duplicate_extensions() {
|
||||
clean_equals_fresh(
|
||||
"module_in_folder",
|
||||
&[Mutation::DuplicateWithDifferentExtension {
|
||||
original: "folder/aModule.luau",
|
||||
duplicate_ext: "lua",
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
/// Clean mode should handle multiple duplicates.
|
||||
#[test]
|
||||
fn clean_removes_multiple_duplicates() {
|
||||
clean_equals_fresh(
|
||||
"module_in_folder",
|
||||
&[
|
||||
Mutation::DuplicateWithDifferentExtension {
|
||||
original: "folder/aModule.luau",
|
||||
duplicate_ext: "lua",
|
||||
},
|
||||
Mutation::DuplicateWithDifferentExtension {
|
||||
original: "folder/aModule.luau",
|
||||
duplicate_ext: "modulescript",
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMBINED / STRESS TESTS
|
||||
// =============================================================================
|
||||
|
||||
/// Clean mode handles multiple different mutations at once.
|
||||
#[test]
|
||||
fn clean_handles_multiple_mutations() {
|
||||
clean_equals_fresh(
|
||||
"deep_nesting",
|
||||
&[
|
||||
Mutation::AddOrphanFile {
|
||||
relative_path: "orphan1.luau",
|
||||
content: "x",
|
||||
},
|
||||
Mutation::AddOrphanFile {
|
||||
relative_path: "src/orphan2.txt",
|
||||
content: "y",
|
||||
},
|
||||
Mutation::AddOrphanDirectory {
|
||||
relative_path: "fake_dir",
|
||||
},
|
||||
Mutation::AddNestedProjectFile {
|
||||
relative_path: "src/bad.project.json5",
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Clean mode handles extreme chaos with many mutations.
|
||||
#[test]
|
||||
fn clean_handles_extreme_chaos() {
|
||||
clean_equals_fresh(
|
||||
"deep_nesting",
|
||||
&[
|
||||
// Orphan files everywhere
|
||||
Mutation::AddOrphanFile {
|
||||
relative_path: "orphan_root.luau",
|
||||
content: "-- root level orphan",
|
||||
},
|
||||
Mutation::AddOrphanFile {
|
||||
relative_path: "src/orphan_src.luau",
|
||||
content: "-- src level orphan",
|
||||
},
|
||||
Mutation::AddOrphanFile {
|
||||
relative_path: "src/level-1/orphan_l1.luau",
|
||||
content: "-- l1 orphan",
|
||||
},
|
||||
Mutation::AddOrphanFile {
|
||||
relative_path: "src/level-1/level-2/orphan_l2.luau",
|
||||
content: "-- l2 orphan",
|
||||
},
|
||||
// Orphan directories
|
||||
Mutation::AddOrphanDirectory {
|
||||
relative_path: "fake_service",
|
||||
},
|
||||
Mutation::AddOrphanDirectory {
|
||||
relative_path: "src/fake_folder",
|
||||
},
|
||||
// Orphan meta files
|
||||
Mutation::AddOrphanFile {
|
||||
relative_path: "src/fake.meta.json5",
|
||||
content: "{}",
|
||||
},
|
||||
// Spurious project files
|
||||
Mutation::AddNestedProjectFile {
|
||||
relative_path: "src/rogue.project.json5",
|
||||
},
|
||||
// Various file types
|
||||
Mutation::AddOrphanFile {
|
||||
relative_path: "src/orphan.txt",
|
||||
content: "text orphan",
|
||||
},
|
||||
Mutation::AddOrphanFile {
|
||||
relative_path: "src/orphan.json5",
|
||||
content: "{}",
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Clean mode handles mutations across different project types.
|
||||
#[test]
|
||||
fn clean_handles_mutations_on_init_project() {
|
||||
clean_equals_fresh(
|
||||
"module_init",
|
||||
&[
|
||||
Mutation::AddOrphanFile {
|
||||
relative_path: "folder/orphan.luau",
|
||||
content: "-- orphan",
|
||||
},
|
||||
Mutation::ModifyFileContent {
|
||||
relative_path: "folder/init.luau",
|
||||
new_content: "-- modified",
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Clean mode handles mutations that affect nested content.
|
||||
#[test]
|
||||
fn clean_handles_mutations_on_nested_project() {
|
||||
clean_equals_fresh(
|
||||
"init_with_children",
|
||||
&[
|
||||
Mutation::AddOrphanFile {
|
||||
relative_path: "src/extra_child.luau",
|
||||
content: "-- extra",
|
||||
},
|
||||
Mutation::DeleteFile {
|
||||
relative_path: "src/other.luau",
|
||||
},
|
||||
Mutation::ModifyFileContent {
|
||||
relative_path: "src/init.luau",
|
||||
new_content: "-- wrong",
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EDGE CASE TESTS
|
||||
// =============================================================================
|
||||
|
||||
/// Clean mode handles empty orphan directories.
|
||||
#[test]
|
||||
fn clean_handles_empty_orphan_directory() {
|
||||
// The AddOrphanDirectory mutation adds a child file, but let's test
|
||||
// with just adding an empty-ish directory structure
|
||||
clean_equals_fresh(
|
||||
"deep_nesting",
|
||||
&[
|
||||
Mutation::AddOrphanFile {
|
||||
relative_path: "src/empty_dir/.gitkeep",
|
||||
content: "",
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Clean mode handles orphan files with special characters in names.
|
||||
#[test]
|
||||
fn clean_handles_special_character_names() {
|
||||
clean_equals_fresh(
|
||||
"deep_nesting",
|
||||
&[
|
||||
Mutation::AddOrphanFile {
|
||||
relative_path: "src/file-with-dashes.luau",
|
||||
content: "-- dashes",
|
||||
},
|
||||
Mutation::AddOrphanFile {
|
||||
relative_path: "src/file_with_underscores.luau",
|
||||
content: "-- underscores",
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
mod build;
|
||||
mod clean_mode;
|
||||
mod clean_mode_stress;
|
||||
mod serve;
|
||||
mod syncback;
|
||||
mod syncback_format_transitions;
|
||||
mod syncback_roundtrip;
|
||||
|
|
|
|||
273
tests/tests/syncback_roundtrip.rs
Normal file
273
tests/tests/syncback_roundtrip.rs
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
//! Build -> Syncback -> Rebuild roundtrip tests.
|
||||
//!
|
||||
//! These tests verify that:
|
||||
//! 1. Building a project to rbxl
|
||||
//! 2. Syncing back to a fresh directory
|
||||
//! 3. Rebuilding from the syncback result
|
||||
//!
|
||||
//! Produces semantically identical output. This validates that the syncback
|
||||
//! system correctly reconstructs project structure from binary place files.
|
||||
|
||||
use std::{fs, path::Path};
|
||||
|
||||
use rbx_dom_weak::{ustr, WeakDom};
|
||||
use tempfile::tempdir;
|
||||
|
||||
use crate::rojo_test::io_util::BUILD_TESTS_PATH;
|
||||
use crate::rojo_test::roundtrip_util::{copy_project_dir, run_rojo_build, run_rojo_syncback_clean};
|
||||
|
||||
/// Generate roundtrip tests for build-test projects.
|
||||
///
|
||||
/// Each test:
|
||||
/// 1. Builds the project to an rbxl file
|
||||
/// 2. Creates a fresh directory with just the project file
|
||||
/// 3. Syncs back from the rbxl to the fresh directory
|
||||
/// 4. Rebuilds from the syncback result
|
||||
/// 5. Compares the two rbxl files for semantic equality
|
||||
macro_rules! roundtrip_tests {
|
||||
($($test_name:ident),* $(,)?) => {$(
|
||||
#[test]
|
||||
fn $test_name() {
|
||||
run_roundtrip_test(stringify!($test_name));
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
fn run_roundtrip_test(build_test_name: &str) {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let project_path = Path::new(BUILD_TESTS_PATH).join(build_test_name);
|
||||
|
||||
// 1. Build original project -> rbxm (model format, not place)
|
||||
let (_tmp1, original_rbxm) = run_rojo_build(&project_path, "original.rbxm");
|
||||
|
||||
// 2. Create fresh syncback target with full project structure
|
||||
let syncback_dir = tempdir().expect("Failed to create temp dir");
|
||||
copy_project_dir(&project_path, syncback_dir.path());
|
||||
|
||||
// 3. Syncback from rbxm to directory
|
||||
assert!(
|
||||
run_rojo_syncback_clean(syncback_dir.path(), &original_rbxm),
|
||||
"Syncback should succeed for {}",
|
||||
build_test_name
|
||||
);
|
||||
|
||||
// 4. Build from syncback result -> rbxm
|
||||
let (_tmp2, roundtrip_rbxm) = run_rojo_build(syncback_dir.path(), "roundtrip.rbxm");
|
||||
|
||||
// 5. Compare both rbxm files (semantic comparison)
|
||||
assert_rbx_equal(&original_rbxm, &roundtrip_rbxm, build_test_name);
|
||||
}
|
||||
|
||||
/// Compare two rbxl files for semantic equality.
|
||||
///
|
||||
/// This compares the instance trees, checking that all instances have the same
|
||||
/// names, class names, and properties (excluding transient properties like Ref).
|
||||
fn assert_rbx_equal(file_a: &Path, file_b: &Path, test_name: &str) {
|
||||
let data_a = fs::read(file_a).expect("Failed to read original rbxl");
|
||||
let data_b = fs::read(file_b).expect("Failed to read roundtrip rbxl");
|
||||
|
||||
let dom_a = rbx_binary::from_reader(data_a.as_slice()).expect("Failed to parse original rbxl");
|
||||
let dom_b =
|
||||
rbx_binary::from_reader(data_b.as_slice()).expect("Failed to parse roundtrip rbxl");
|
||||
|
||||
// Compare the root's children (DataModel children)
|
||||
compare_children(&dom_a, dom_a.root_ref(), &dom_b, dom_b.root_ref(), test_name);
|
||||
}
|
||||
|
||||
/// Recursively compare children of two instances.
|
||||
fn compare_children(
|
||||
dom_a: &WeakDom,
|
||||
ref_a: rbx_dom_weak::types::Ref,
|
||||
dom_b: &WeakDom,
|
||||
ref_b: rbx_dom_weak::types::Ref,
|
||||
test_name: &str,
|
||||
) {
|
||||
let inst_a = dom_a.get_by_ref(ref_a).expect("Instance A should exist");
|
||||
let inst_b = dom_b.get_by_ref(ref_b).expect("Instance B should exist");
|
||||
|
||||
let children_a = inst_a.children();
|
||||
let children_b = inst_b.children();
|
||||
|
||||
// Build maps of children by name for comparison
|
||||
let mut map_a: std::collections::HashMap<&str, &rbx_dom_weak::Instance> =
|
||||
std::collections::HashMap::new();
|
||||
let mut map_b: std::collections::HashMap<&str, &rbx_dom_weak::Instance> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
for &child_ref in children_a {
|
||||
if let Some(child) = dom_a.get_by_ref(child_ref) {
|
||||
map_a.insert(&child.name, child);
|
||||
}
|
||||
}
|
||||
|
||||
for &child_ref in children_b {
|
||||
if let Some(child) = dom_b.get_by_ref(child_ref) {
|
||||
map_b.insert(&child.name, child);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that both have the same children
|
||||
let names_a: std::collections::HashSet<&str> = map_a.keys().copied().collect();
|
||||
let names_b: std::collections::HashSet<&str> = map_b.keys().copied().collect();
|
||||
|
||||
let only_in_a: Vec<_> = names_a.difference(&names_b).collect();
|
||||
let only_in_b: Vec<_> = names_b.difference(&names_a).collect();
|
||||
|
||||
if !only_in_a.is_empty() || !only_in_b.is_empty() {
|
||||
panic!(
|
||||
"[{}] Children differ under '{}':\n Only in original: {:?}\n Only in roundtrip: {:?}",
|
||||
test_name, inst_a.name, only_in_a, only_in_b
|
||||
);
|
||||
}
|
||||
|
||||
// Compare each child
|
||||
for name in names_a {
|
||||
let child_a = map_a[name];
|
||||
let child_b = map_b[name];
|
||||
|
||||
// Check class name matches
|
||||
if child_a.class != child_b.class {
|
||||
panic!(
|
||||
"[{}] Class mismatch for '{}': original={}, roundtrip={}",
|
||||
test_name, name, child_a.class, child_b.class
|
||||
);
|
||||
}
|
||||
|
||||
// Compare important properties (Source for scripts, Value for value types)
|
||||
compare_properties(child_a, child_b, test_name);
|
||||
|
||||
// Recurse into children
|
||||
let child_ref_a = dom_a
|
||||
.get_by_ref(ref_a)
|
||||
.unwrap()
|
||||
.children()
|
||||
.iter()
|
||||
.find(|&&r| dom_a.get_by_ref(r).map(|i| i.name.as_str()) == Some(name))
|
||||
.copied()
|
||||
.unwrap();
|
||||
let child_ref_b = dom_b
|
||||
.get_by_ref(ref_b)
|
||||
.unwrap()
|
||||
.children()
|
||||
.iter()
|
||||
.find(|&&r| dom_b.get_by_ref(r).map(|i| i.name.as_str()) == Some(name))
|
||||
.copied()
|
||||
.unwrap();
|
||||
|
||||
compare_children(dom_a, child_ref_a, dom_b, child_ref_b, test_name);
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare properties of two instances.
|
||||
///
|
||||
/// Focuses on properties that matter for roundtrip correctness:
|
||||
/// - Source (for scripts)
|
||||
/// - Value (for value types)
|
||||
/// - Other serializable properties
|
||||
fn compare_properties(
|
||||
inst_a: &rbx_dom_weak::Instance,
|
||||
inst_b: &rbx_dom_weak::Instance,
|
||||
test_name: &str,
|
||||
) {
|
||||
// Check Source property for scripts
|
||||
if let (Some(source_a), Some(source_b)) = (
|
||||
inst_a.properties.get(&ustr("Source")),
|
||||
inst_b.properties.get(&ustr("Source")),
|
||||
) {
|
||||
if source_a != source_b {
|
||||
panic!(
|
||||
"[{}] Source property differs for '{}' ({}):\nOriginal: {:?}\nRoundtrip: {:?}",
|
||||
test_name, inst_a.name, inst_a.class, source_a, source_b
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check Value property for value types
|
||||
if let (Some(value_a), Some(value_b)) = (
|
||||
inst_a.properties.get(&ustr("Value")),
|
||||
inst_b.properties.get(&ustr("Value")),
|
||||
) {
|
||||
if value_a != value_b {
|
||||
panic!(
|
||||
"[{}] Value property differs for '{}' ({}):\nOriginal: {:?}\nRoundtrip: {:?}",
|
||||
test_name, inst_a.name, inst_a.class, value_a, value_b
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check Attributes if present
|
||||
if let (Some(attrs_a), Some(attrs_b)) = (
|
||||
inst_a.properties.get(&ustr("Attributes")),
|
||||
inst_b.properties.get(&ustr("Attributes")),
|
||||
) {
|
||||
if attrs_a != attrs_b {
|
||||
panic!(
|
||||
"[{}] Attributes differ for '{}' ({}):\nOriginal: {:?}\nRoundtrip: {:?}",
|
||||
test_name, inst_a.name, inst_a.class, attrs_a, attrs_b
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ROUNDTRIP TESTS
|
||||
// =============================================================================
|
||||
//
|
||||
// These are build-test projects that should roundtrip cleanly through
|
||||
// build -> syncback -> rebuild.
|
||||
//
|
||||
// Some projects are excluded because they:
|
||||
// - Use sync rules with custom extensions (sync_rule_* projects)
|
||||
// - Have CSV files that may not roundtrip identically
|
||||
// - Have special project configurations that affect syncback
|
||||
|
||||
roundtrip_tests! {
|
||||
// Basic script types
|
||||
client_in_folder,
|
||||
client_init,
|
||||
module_in_folder,
|
||||
module_init,
|
||||
server_in_folder,
|
||||
server_init,
|
||||
|
||||
// Nested structures
|
||||
deep_nesting,
|
||||
init_with_children,
|
||||
|
||||
// Meta files and properties
|
||||
init_meta_class_name,
|
||||
init_meta_properties,
|
||||
script_meta_disabled,
|
||||
|
||||
// JSON models
|
||||
json_model_in_folder,
|
||||
json_model_legacy_name,
|
||||
|
||||
// Text files
|
||||
txt,
|
||||
txt_in_folder,
|
||||
|
||||
// Binary models (rbxm/rbxmx)
|
||||
rbxm_in_folder,
|
||||
rbxmx_in_folder,
|
||||
rbxmx_ref,
|
||||
|
||||
// Composed projects
|
||||
project_composed_default,
|
||||
project_composed_file,
|
||||
project_root_name,
|
||||
|
||||
// Special cases
|
||||
attributes,
|
||||
gitkeep,
|
||||
optional,
|
||||
weldconstraint,
|
||||
|
||||
// Service inference
|
||||
infer_service_name,
|
||||
infer_starter_player,
|
||||
|
||||
// Issue regression tests
|
||||
issue_546,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue