diff --git a/server/tests/snapshots.rs b/server/tests/snapshots.rs index 1707495d..14e4796f 100644 --- a/server/tests/snapshots.rs +++ b/server/tests/snapshots.rs @@ -1,15 +1,28 @@ +mod test_util; + use std::{ - fs::{self, File}, + fs, path::{Path, PathBuf}, + sync::Arc, + thread, + time::Duration, }; +use log::error; +use tempfile::{tempdir, TempDir}; use pretty_assertions::assert_eq; use librojo::{ imfs::Imfs, - project::{Project, ProjectNode}, + project::Project, + live_session::LiveSession, rbx_snapshot::{SnapshotContext, snapshot_project_tree}, - snapshot_reconciler::{RbxSnapshotInstance}, +}; + +use crate::test_util::{ + copy_recursive, + snapshot::*, + tree::trees_equal, }; macro_rules! generate_snapshot_tests { @@ -18,6 +31,8 @@ macro_rules! generate_snapshot_tests { paste::item! { #[test] fn []() { + let _ = env_logger::try_init(); + let tests_folder = Path::new(env!("CARGO_MANIFEST_DIR")).join("../test-projects"); let project_folder = tests_folder.join(stringify!($name)); run_snapshot_test(&project_folder); @@ -29,13 +44,79 @@ macro_rules! generate_snapshot_tests { generate_snapshot_tests!( empty, + multi_partition_game, nested_partitions, single_partition_game, single_partition_model, transmute_partition ); -const SNAPSHOT_EXPECTED_NAME: &str = "expected-snapshot.json"; +#[test] +fn multi_partition_game() { + let _ = env_logger::try_init(); + let source_path = project_path("multi_partition_game"); + + let (dir, live_session) = start_session(&source_path); + tree_step("initial", &live_session, &source_path); + + let added_path = dir.path().join("a/added"); + fs::create_dir_all(&added_path) + .expect("Couldn't create directory"); + thread::sleep(Duration::from_millis(250)); + + tree_step("with_dir", &live_session, &source_path); + + let moved_path = dir.path().join("b/added"); + fs::rename(&added_path, &moved_path) + .expect("Couldn't rename directory"); + thread::sleep(Duration::from_millis(250)); + + tree_step("with_moved_dir", &live_session, &source_path); +} + +/// Find the path to the given test project relative to the manifest. +fn project_path(name: &str) -> PathBuf { + let mut path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../test-projects"); + path.push(name); + path +} + +/// Starts a new LiveSession for the project located at the given file path. +fn start_session(source_path: &Path) -> (TempDir, LiveSession) { + let dir = tempdir() + .expect("Couldn't create temporary directory"); + + copy_recursive(&source_path, dir.path()) + .expect("Couldn't copy project to temporary directory"); + + let project = Arc::new(Project::load_fuzzy(dir.path()) + .expect("Couldn't load project from temp directory")); + + let live_session = LiveSession::new(Arc::clone(&project)) + .expect("Couldn't start live session"); + + (dir, live_session) +} + +/// Marks a 'step' in the test, which will snapshot the session's current +/// RbxTree object and compare it against the saved snapshot if it exists. +fn tree_step(step: &str, live_session: &LiveSession, source_path: &Path) { + let rbx_session = live_session.rbx_session.lock().unwrap(); + let tree = rbx_session.get_tree(); + + match read_tree_by_name(source_path, step) { + Some(existing) => match trees_equal(&existing, tree) { + Ok(_) => {} + Err(e) => { + error!("Trees at step '{}' were not equal.\n{}", step, e); + panic!("Tree mismatch at step '{}'", step); + } + } + None => { + write_tree_by_name(source_path, step, tree); + } + } +} fn run_snapshot_test(path: &Path) { println!("Running snapshot from project: {}", path.display()); @@ -62,67 +143,4 @@ fn run_snapshot_test(path: &Path) { Some(expected_snapshot) => assert_eq!(snapshot, expected_snapshot), None => write_expected_snapshot(path, &snapshot), } -} - -/// Snapshots contain absolute paths, which simplifies much of Rojo. -/// -/// For saving snapshots to the disk, we should strip off the project folder -/// path to make them machine-independent. This doesn't work for paths that fall -/// outside of the project folder, but that's okay here. -/// -/// We also need to sort children, since Rojo tends to enumerate the filesystem -/// in an unpredictable order. -fn anonymize_snapshot(project_folder_path: &Path, snapshot: &mut RbxSnapshotInstance) { - match snapshot.metadata.source_path.as_mut() { - Some(path) => *path = anonymize_path(project_folder_path, path), - None => {}, - } - - match snapshot.metadata.project_definition.as_mut() { - Some((_, project_node)) => anonymize_project_node(project_folder_path, project_node), - None => {}, - } - - snapshot.children.sort_by(|a, b| a.partial_cmp(b).unwrap()); - - for child in snapshot.children.iter_mut() { - anonymize_snapshot(project_folder_path, child); - } -} - -fn anonymize_project_node(project_folder_path: &Path, project_node: &mut ProjectNode) { - match project_node.path.as_mut() { - Some(path) => *path = anonymize_path(project_folder_path, path), - None => {}, - } - - for child_node in project_node.children.values_mut() { - anonymize_project_node(project_folder_path, child_node); - } -} - -fn anonymize_path(project_folder_path: &Path, path: &Path) -> PathBuf { - if path.is_absolute() { - path.strip_prefix(project_folder_path) - .expect("Could not anonymize absolute path") - .to_path_buf() - } else { - path.to_path_buf() - } -} - -fn read_expected_snapshot(path: &Path) -> Option>> { - let contents = fs::read(path.join(SNAPSHOT_EXPECTED_NAME)).ok()?; - let snapshot: Option> = serde_json::from_slice(&contents) - .expect("Could not deserialize snapshot"); - - Some(snapshot) -} - -fn write_expected_snapshot(path: &Path, snapshot: &Option) { - let mut file = File::create(path.join(SNAPSHOT_EXPECTED_NAME)) - .expect("Could not open file to write snapshot"); - - serde_json::to_writer_pretty(&mut file, snapshot) - .expect("Could not serialize snapshot to file"); } \ No newline at end of file diff --git a/server/tests/test_util/mod.rs b/server/tests/test_util/mod.rs index ec3c51fd..a604a62b 100644 --- a/server/tests/test_util/mod.rs +++ b/server/tests/test_util/mod.rs @@ -2,30 +2,10 @@ use std::fs::{create_dir, copy}; use std::path::Path; use std::io; -use rouille::Request; - use walkdir::WalkDir; -use librojo::web::Server; - -pub trait HttpTestUtil { - fn get_string(&self, url: &str) -> String; -} - -impl HttpTestUtil for Server { - fn get_string(&self, url: &str) -> String { - let info_request = Request::fake_http("GET", url, vec![], vec![]); - let response = self.handle_request(&info_request); - - assert_eq!(response.status_code, 200); - - let (mut reader, _) = response.data.into_reader_and_size(); - let mut body = String::new(); - reader.read_to_string(&mut body).unwrap(); - - body - } -} +pub mod snapshot; +pub mod tree; pub fn copy_recursive(from: &Path, to: &Path) -> io::Result<()> { for entry in WalkDir::new(from) { @@ -51,4 +31,4 @@ pub fn copy_recursive(from: &Path, to: &Path) -> io::Result<()> { } Ok(()) -} +} \ No newline at end of file diff --git a/server/tests/test_util/snapshot.rs b/server/tests/test_util/snapshot.rs new file mode 100644 index 00000000..534805b5 --- /dev/null +++ b/server/tests/test_util/snapshot.rs @@ -0,0 +1,98 @@ +use std::{ + fs::{self, File}, + path::{Path, PathBuf}, +}; + +use rbx_dom_weak::RbxTree; + +use librojo::{ + project::ProjectNode, + snapshot_reconciler::RbxSnapshotInstance, +}; + +const SNAPSHOT_EXPECTED_NAME: &str = "expected-snapshot.json"; + +/// Snapshots contain absolute paths, which simplifies much of Rojo. +/// +/// For saving snapshots to the disk, we should strip off the project folder +/// path to make them machine-independent. This doesn't work for paths that fall +/// outside of the project folder, but that's okay here. +/// +/// We also need to sort children, since Rojo tends to enumerate the filesystem +/// in an unpredictable order. +pub fn anonymize_snapshot(project_folder_path: &Path, snapshot: &mut RbxSnapshotInstance) { + match snapshot.metadata.source_path.as_mut() { + Some(path) => *path = anonymize_path(project_folder_path, path), + None => {}, + } + + match snapshot.metadata.project_definition.as_mut() { + Some((_, project_node)) => anonymize_project_node(project_folder_path, project_node), + None => {}, + } + + snapshot.children.sort_by(|a, b| a.partial_cmp(b).unwrap()); + + for child in snapshot.children.iter_mut() { + anonymize_snapshot(project_folder_path, child); + } +} + +pub fn anonymize_project_node(project_folder_path: &Path, project_node: &mut ProjectNode) { + match project_node.path.as_mut() { + Some(path) => *path = anonymize_path(project_folder_path, path), + None => {}, + } + + for child_node in project_node.children.values_mut() { + anonymize_project_node(project_folder_path, child_node); + } +} + +pub fn anonymize_path(project_folder_path: &Path, path: &Path) -> PathBuf { + if path.is_absolute() { + path.strip_prefix(project_folder_path) + .expect("Could not anonymize absolute path") + .to_path_buf() + } else { + path.to_path_buf() + } +} + +pub fn read_tree_by_name(path: &Path, identifier: &str) -> Option { + let mut file_path = path.join(identifier); + file_path.set_extension("tree.json"); + + let contents = fs::read(&file_path).ok()?; + let tree: RbxTree = serde_json::from_slice(&contents) + .expect("Could not deserialize tree"); + + Some(tree) +} + +pub fn write_tree_by_name(path: &Path, identifier: &str, tree: &RbxTree) { + let mut file_path = path.join(identifier); + file_path.set_extension("tree.json"); + + let mut file = File::create(file_path) + .expect("Could not open file to write tree"); + + serde_json::to_writer_pretty(&mut file, tree) + .expect("Could not serialize tree to file"); +} + +pub fn read_expected_snapshot(path: &Path) -> Option>> { + let contents = fs::read(path.join(SNAPSHOT_EXPECTED_NAME)).ok()?; + let snapshot: Option> = serde_json::from_slice(&contents) + .expect("Could not deserialize snapshot"); + + Some(snapshot) +} + +pub fn write_expected_snapshot(path: &Path, snapshot: &Option) { + let mut file = File::create(path.join(SNAPSHOT_EXPECTED_NAME)) + .expect("Could not open file to write snapshot"); + + serde_json::to_writer_pretty(&mut file, snapshot) + .expect("Could not serialize snapshot to file"); +} \ No newline at end of file diff --git a/server/tests/test_util/tree.rs b/server/tests/test_util/tree.rs new file mode 100644 index 00000000..57ac4f12 --- /dev/null +++ b/server/tests/test_util/tree.rs @@ -0,0 +1,181 @@ +//! Defines a mechanism to compare two RbxTree objects and generate a useful +//! diff if they aren't the same. These methods ignore IDs, which are randomly +//! generated whenever a tree is constructed anyways. This makes matching up +//! pairs of instances that should be the same pretty difficult. +//! +//! It relies on a couple different ideas: +//! - Instances with the same name and class name are matched as the same +//! instance. See basic_equal for this logic +//! - A path of period-delimited names (like Roblox's GetFullName) should be +//! enough to debug most issues. If it isn't, we can do something fun like +//! generate GraphViz graphs. + +use std::{ + borrow::Cow, + collections::HashSet, + fmt, +}; + +use rbx_dom_weak::{RbxId, RbxTree}; + +#[derive(Debug)] +pub struct TreeMismatch { + pub path: Cow<'static, str>, + pub detail: Cow<'static, str>, +} + +impl TreeMismatch { + pub fn new<'a, A: Into>, B: Into>>(path: A, detail: B) -> TreeMismatch { + TreeMismatch { + path: Cow::Owned(path.into().into_owned()), + detail: Cow::Owned(detail.into().into_owned()), + } + } + + pub fn add_parent(mut self, name: &str) -> TreeMismatch { + self.path.to_mut().insert(0, '.'); + self.path.to_mut().insert_str(0, name); + + TreeMismatch { + path: self.path, + detail: self.detail, + } + } +} + +impl fmt::Display for TreeMismatch { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + writeln!(formatter, "Tree mismatch at path {}", self.path)?; + writeln!(formatter, "{}", self.detail) + } +} + +pub fn trees_equal(left_tree: &RbxTree, right_tree: &RbxTree) -> Result<(), TreeMismatch> { + let left_id = left_tree.get_root_id(); + let right_id = right_tree.get_root_id(); + + basic_equal(left_tree, left_id, right_tree, right_id)?; + properties_equal(left_tree, left_id, right_tree, right_id)?; + + children_equal(left_tree, left_id, right_tree, right_id) + .map_err(|e| { + let left_instance = left_tree.get_instance(left_id) + .expect("ID did not exist in left tree"); + + e.add_parent(&left_instance.name) + }) +} + +fn basic_equal(left_tree: &RbxTree, left_id: RbxId, right_tree: &RbxTree, right_id: RbxId) -> Result<(), TreeMismatch> { + let left_instance = left_tree.get_instance(left_id) + .expect("ID did not exist in left tree"); + + let right_instance = right_tree.get_instance(right_id) + .expect("ID did not exist in right tree"); + + if left_instance.name != right_instance.name { + return Err(TreeMismatch::new(&left_instance.name, "Name did not match")); + } + + if left_instance.class_name != right_instance.class_name { + return Err(TreeMismatch::new(&left_instance.name, "Class name did not match")); + } + + Ok(()) +} + +fn properties_equal(left_tree: &RbxTree, left_id: RbxId, right_tree: &RbxTree, right_id: RbxId) -> Result<(), TreeMismatch> { + let left_instance = left_tree.get_instance(left_id) + .expect("ID did not exist in left tree"); + + let right_instance = right_tree.get_instance(right_id) + .expect("ID did not exist in right tree"); + + let mut visited = HashSet::new(); + + for (key, left_value) in &left_instance.properties { + visited.insert(key); + + let right_value = right_instance.properties.get(key); + + if Some(left_value) != right_value { + let message = format!( + "Property {}:\n\tLeft: {:?}\n\tRight: {:?}", + key, + Some(left_value), + right_value, + ); + + return Err(TreeMismatch::new(&left_instance.name, message)); + } + } + + for (key, right_value) in &right_instance.properties { + if visited.contains(key) { + continue; + } + + let left_value = left_instance.properties.get(key); + + if left_value != Some(right_value) { + let message = format!( + "Property {} was {:?} on left and {:?} on right", + key, + left_value, + Some(right_value), + ); + + return Err(TreeMismatch::new(&left_instance.name, message)); + } + } + + Ok(()) +} + +fn children_equal(left_tree: &RbxTree, left_id: RbxId, right_tree: &RbxTree, right_id: RbxId) -> Result<(), TreeMismatch> { + let left_instance = left_tree.get_instance(left_id) + .expect("ID did not exist in left tree"); + + let right_instance = right_tree.get_instance(right_id) + .expect("ID did not exist in right tree"); + + let left_children = left_instance.get_children_ids(); + let right_children = right_instance.get_children_ids(); + + if left_children.len() != right_children.len() { + return Err(TreeMismatch::new(&left_instance.name, "Instances had different numbers of children")); + } + + let mut right_visited = HashSet::new(); + + for left_child_id in left_children { + let mut found_match = false; + + for right_child_id in right_children { + if right_visited.contains(right_child_id) { + continue; + } + + if basic_equal(left_tree, *left_child_id, right_tree, *right_child_id).is_err() { + continue; + } + + properties_equal(left_tree, *left_child_id, right_tree, *right_child_id) + .map_err(|e| e.add_parent(&left_instance.name))?; + + children_equal(left_tree, *left_child_id, right_tree, *right_child_id) + .map_err(|e| e.add_parent(&left_instance.name))?; + + found_match = true; + right_visited.insert(*right_child_id); + } + + if !found_match { + let left_child = left_tree.get_instance(*left_child_id).unwrap(); + + return Err(TreeMismatch::new(&left_child.name, "Had no equal in right tree")); + } + } + + Ok(()) +} \ No newline at end of file diff --git a/test-projects/multi_partition_game/a/foo.txt b/test-projects/multi_partition_game/a/foo.txt new file mode 100644 index 00000000..20b6566e --- /dev/null +++ b/test-projects/multi_partition_game/a/foo.txt @@ -0,0 +1 @@ +Hello world, from a/foo.txt \ No newline at end of file diff --git a/test-projects/multi_partition_game/a/main.lua b/test-projects/multi_partition_game/a/main.lua new file mode 100644 index 00000000..23bf7c82 --- /dev/null +++ b/test-projects/multi_partition_game/a/main.lua @@ -0,0 +1 @@ +-- hello, from a/main.lua \ No newline at end of file diff --git a/test-projects/multi_partition_game/b/something.lua b/test-projects/multi_partition_game/b/something.lua new file mode 100644 index 00000000..415e8ebc --- /dev/null +++ b/test-projects/multi_partition_game/b/something.lua @@ -0,0 +1 @@ +-- b/something.lua \ No newline at end of file diff --git a/test-projects/multi_partition_game/default.project.json b/test-projects/multi_partition_game/default.project.json new file mode 100644 index 00000000..9862db3f --- /dev/null +++ b/test-projects/multi_partition_game/default.project.json @@ -0,0 +1,21 @@ +{ + "name": "multi_partition_game", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "$className": "ReplicatedStorage", + "Ack": { + "$path": "a" + }, + "Bar": { + "$path": "b" + } + }, + "HttpService": { + "$className": "HttpService", + "$properties": { + "HttpEnabled": true + } + } + } +} \ No newline at end of file diff --git a/test-projects/multi_partition_game/expected-snapshot.json b/test-projects/multi_partition_game/expected-snapshot.json new file mode 100644 index 00000000..49e76960 --- /dev/null +++ b/test-projects/multi_partition_game/expected-snapshot.json @@ -0,0 +1,212 @@ +{ + "name": "multi_partition_game", + "class_name": "DataModel", + "properties": {}, + "children": [ + { + "name": "HttpService", + "class_name": "HttpService", + "properties": { + "HttpEnabled": { + "Type": "Bool", + "Value": true + } + }, + "children": [], + "metadata": { + "ignore_unknown_instances": true, + "source_path": null, + "project_definition": [ + "HttpService", + { + "class_name": "HttpService", + "children": {}, + "properties": { + "HttpEnabled": { + "Type": "Bool", + "Value": true + } + }, + "ignore_unknown_instances": null, + "path": null + } + ] + } + }, + { + "name": "ReplicatedStorage", + "class_name": "ReplicatedStorage", + "properties": {}, + "children": [ + { + "name": "Ack", + "class_name": "Folder", + "properties": {}, + "children": [ + { + "name": "foo", + "class_name": "StringValue", + "properties": { + "Value": { + "Type": "String", + "Value": "Hello world, from a/foo.txt" + } + }, + "children": [], + "metadata": { + "ignore_unknown_instances": false, + "source_path": "a/foo.txt", + "project_definition": null + } + }, + { + "name": "main", + "class_name": "ModuleScript", + "properties": { + "Source": { + "Type": "String", + "Value": "-- hello, from a/main.lua" + } + }, + "children": [], + "metadata": { + "ignore_unknown_instances": false, + "source_path": "a/main.lua", + "project_definition": null + } + } + ], + "metadata": { + "ignore_unknown_instances": false, + "source_path": "a", + "project_definition": [ + "Ack", + { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "a" + } + ] + } + }, + { + "name": "Bar", + "class_name": "Folder", + "properties": {}, + "children": [ + { + "name": "something", + "class_name": "ModuleScript", + "properties": { + "Source": { + "Type": "String", + "Value": "-- b/something.lua" + } + }, + "children": [], + "metadata": { + "ignore_unknown_instances": false, + "source_path": "b/something.lua", + "project_definition": null + } + } + ], + "metadata": { + "ignore_unknown_instances": false, + "source_path": "b", + "project_definition": [ + "Bar", + { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "b" + } + ] + } + } + ], + "metadata": { + "ignore_unknown_instances": true, + "source_path": null, + "project_definition": [ + "ReplicatedStorage", + { + "class_name": "ReplicatedStorage", + "children": { + "Bar": { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "b" + }, + "Ack": { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "a" + } + }, + "properties": {}, + "ignore_unknown_instances": null, + "path": null + } + ] + } + } + ], + "metadata": { + "ignore_unknown_instances": true, + "source_path": null, + "project_definition": [ + "multi_partition_game", + { + "class_name": "DataModel", + "children": { + "ReplicatedStorage": { + "class_name": "ReplicatedStorage", + "children": { + "Bar": { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "b" + }, + "Ack": { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "a" + } + }, + "properties": {}, + "ignore_unknown_instances": null, + "path": null + }, + "HttpService": { + "class_name": "HttpService", + "children": {}, + "properties": { + "HttpEnabled": { + "Type": "Bool", + "Value": true + } + }, + "ignore_unknown_instances": null, + "path": null + } + }, + "properties": {}, + "ignore_unknown_instances": null, + "path": null + } + ] + } +} \ No newline at end of file diff --git a/test-projects/multi_partition_game/initial.tree.json b/test-projects/multi_partition_game/initial.tree.json new file mode 100644 index 00000000..0be618f3 --- /dev/null +++ b/test-projects/multi_partition_game/initial.tree.json @@ -0,0 +1,100 @@ +{ + "instances": { + "a44d135b-d022-4893-8079-d921a7ba7077": { + "Name": "foo", + "ClassName": "StringValue", + "Properties": { + "Value": { + "Type": "String", + "Value": "Hello world, from a/foo.txt" + } + }, + "Id": "a44d135b-d022-4893-8079-d921a7ba7077", + "Children": [], + "Parent": "c3319126-251e-4743-b0b8-3d58c355883e" + }, + "046a1cf5-e720-4647-9161-10998594ca65": { + "Name": "HttpService", + "ClassName": "HttpService", + "Properties": { + "HttpEnabled": { + "Type": "Bool", + "Value": true + } + }, + "Id": "046a1cf5-e720-4647-9161-10998594ca65", + "Children": [], + "Parent": "2d857fa5-51e8-4c97-9974-b7e1ec795bba" + }, + "15f73586-bb1b-49c7-b543-c4d05b9f8a60": { + "Name": "ReplicatedStorage", + "ClassName": "ReplicatedStorage", + "Properties": {}, + "Id": "15f73586-bb1b-49c7-b543-c4d05b9f8a60", + "Children": [ + "bb3d7bbd-7c90-4838-95e3-693be4daf727", + "c3319126-251e-4743-b0b8-3d58c355883e" + ], + "Parent": "2d857fa5-51e8-4c97-9974-b7e1ec795bba" + }, + "c3319126-251e-4743-b0b8-3d58c355883e": { + "Name": "Ack", + "ClassName": "Folder", + "Properties": {}, + "Id": "c3319126-251e-4743-b0b8-3d58c355883e", + "Children": [ + "a44d135b-d022-4893-8079-d921a7ba7077", + "031507bd-e81e-481f-a7af-401566afab0f" + ], + "Parent": "15f73586-bb1b-49c7-b543-c4d05b9f8a60" + }, + "4162d6bb-732b-4da4-a917-38d267ff9c42": { + "Name": "something", + "ClassName": "ModuleScript", + "Properties": { + "Source": { + "Type": "String", + "Value": "-- b/something.lua" + } + }, + "Id": "4162d6bb-732b-4da4-a917-38d267ff9c42", + "Children": [], + "Parent": "bb3d7bbd-7c90-4838-95e3-693be4daf727" + }, + "bb3d7bbd-7c90-4838-95e3-693be4daf727": { + "Name": "Bar", + "ClassName": "Folder", + "Properties": {}, + "Id": "bb3d7bbd-7c90-4838-95e3-693be4daf727", + "Children": [ + "4162d6bb-732b-4da4-a917-38d267ff9c42" + ], + "Parent": "15f73586-bb1b-49c7-b543-c4d05b9f8a60" + }, + "2d857fa5-51e8-4c97-9974-b7e1ec795bba": { + "Name": "multi_partition_game", + "ClassName": "DataModel", + "Properties": {}, + "Id": "2d857fa5-51e8-4c97-9974-b7e1ec795bba", + "Children": [ + "046a1cf5-e720-4647-9161-10998594ca65", + "15f73586-bb1b-49c7-b543-c4d05b9f8a60" + ], + "Parent": null + }, + "031507bd-e81e-481f-a7af-401566afab0f": { + "Name": "main", + "ClassName": "ModuleScript", + "Properties": { + "Source": { + "Type": "String", + "Value": "-- hello, from a/main.lua" + } + }, + "Id": "031507bd-e81e-481f-a7af-401566afab0f", + "Children": [], + "Parent": "c3319126-251e-4743-b0b8-3d58c355883e" + } + }, + "root_id": "2d857fa5-51e8-4c97-9974-b7e1ec795bba" +} \ No newline at end of file diff --git a/test-projects/multi_partition_game/with_dir.tree.json b/test-projects/multi_partition_game/with_dir.tree.json new file mode 100644 index 00000000..ac752fc1 --- /dev/null +++ b/test-projects/multi_partition_game/with_dir.tree.json @@ -0,0 +1,109 @@ +{ + "instances": { + "50e71545-7a19-4457-acc7-1e97ae9ece18": { + "Name": "something", + "ClassName": "ModuleScript", + "Properties": { + "Source": { + "Type": "String", + "Value": "-- b/something.lua" + } + }, + "Id": "50e71545-7a19-4457-acc7-1e97ae9ece18", + "Children": [], + "Parent": "9a7e28d7-135d-4b8f-89dd-85bf88416d98" + }, + "9b0799cc-fc16-49ce-8ee9-6feeb52ed6da": { + "Name": "added", + "ClassName": "Folder", + "Properties": {}, + "Id": "9b0799cc-fc16-49ce-8ee9-6feeb52ed6da", + "Children": [], + "Parent": "6d0894f6-0365-4363-839e-66c953b42aa8" + }, + "5bdc78c3-a1a9-47b8-9660-abb01364aa70": { + "Name": "ReplicatedStorage", + "ClassName": "ReplicatedStorage", + "Properties": {}, + "Id": "5bdc78c3-a1a9-47b8-9660-abb01364aa70", + "Children": [ + "9a7e28d7-135d-4b8f-89dd-85bf88416d98", + "6d0894f6-0365-4363-839e-66c953b42aa8" + ], + "Parent": "57609a96-3c79-4456-9630-4d157a5e1ee7" + }, + "675477f2-d101-4d62-bb12-aacb1d1e05b5": { + "Name": "main", + "ClassName": "ModuleScript", + "Properties": { + "Source": { + "Type": "String", + "Value": "-- hello, from a/main.lua" + } + }, + "Id": "675477f2-d101-4d62-bb12-aacb1d1e05b5", + "Children": [], + "Parent": "6d0894f6-0365-4363-839e-66c953b42aa8" + }, + "6d0894f6-0365-4363-839e-66c953b42aa8": { + "Name": "Ack", + "ClassName": "Folder", + "Properties": {}, + "Id": "6d0894f6-0365-4363-839e-66c953b42aa8", + "Children": [ + "4ee48ea8-0f0e-4fcf-ade2-196eda2fa05c", + "675477f2-d101-4d62-bb12-aacb1d1e05b5", + "9b0799cc-fc16-49ce-8ee9-6feeb52ed6da" + ], + "Parent": "5bdc78c3-a1a9-47b8-9660-abb01364aa70" + }, + "c2c90183-dfaa-4ee5-ac31-9d78ae32d3fb": { + "Name": "HttpService", + "ClassName": "HttpService", + "Properties": { + "HttpEnabled": { + "Type": "Bool", + "Value": true + } + }, + "Id": "c2c90183-dfaa-4ee5-ac31-9d78ae32d3fb", + "Children": [], + "Parent": "57609a96-3c79-4456-9630-4d157a5e1ee7" + }, + "57609a96-3c79-4456-9630-4d157a5e1ee7": { + "Name": "multi_partition_game", + "ClassName": "DataModel", + "Properties": {}, + "Id": "57609a96-3c79-4456-9630-4d157a5e1ee7", + "Children": [ + "5bdc78c3-a1a9-47b8-9660-abb01364aa70", + "c2c90183-dfaa-4ee5-ac31-9d78ae32d3fb" + ], + "Parent": null + }, + "4ee48ea8-0f0e-4fcf-ade2-196eda2fa05c": { + "Name": "foo", + "ClassName": "StringValue", + "Properties": { + "Value": { + "Type": "String", + "Value": "Hello world, from a/foo.txt" + } + }, + "Id": "4ee48ea8-0f0e-4fcf-ade2-196eda2fa05c", + "Children": [], + "Parent": "6d0894f6-0365-4363-839e-66c953b42aa8" + }, + "9a7e28d7-135d-4b8f-89dd-85bf88416d98": { + "Name": "Bar", + "ClassName": "Folder", + "Properties": {}, + "Id": "9a7e28d7-135d-4b8f-89dd-85bf88416d98", + "Children": [ + "50e71545-7a19-4457-acc7-1e97ae9ece18" + ], + "Parent": "5bdc78c3-a1a9-47b8-9660-abb01364aa70" + } + }, + "root_id": "57609a96-3c79-4456-9630-4d157a5e1ee7" +} \ No newline at end of file diff --git a/test-projects/multi_partition_game/with_moved_dir.tree.json b/test-projects/multi_partition_game/with_moved_dir.tree.json new file mode 100644 index 00000000..1a24307c --- /dev/null +++ b/test-projects/multi_partition_game/with_moved_dir.tree.json @@ -0,0 +1,109 @@ +{ + "instances": { + "833f7f8b-7ec3-4f6a-9476-63af9612510a": { + "Name": "foo", + "ClassName": "StringValue", + "Properties": { + "Value": { + "Type": "String", + "Value": "Hello world, from a/foo.txt" + } + }, + "Id": "833f7f8b-7ec3-4f6a-9476-63af9612510a", + "Children": [], + "Parent": "ea14bb8b-cc04-4b0c-9aae-02f1447cbda7" + }, + "381278fa-4eb9-4110-b64c-3bbdb76a0bd5": { + "Name": "something", + "ClassName": "ModuleScript", + "Properties": { + "Source": { + "Type": "String", + "Value": "-- b/something.lua" + } + }, + "Id": "381278fa-4eb9-4110-b64c-3bbdb76a0bd5", + "Children": [], + "Parent": "74b23f01-582f-4c6b-9f6c-e5c17c41c466" + }, + "74b23f01-582f-4c6b-9f6c-e5c17c41c466": { + "Name": "Bar", + "ClassName": "Folder", + "Properties": {}, + "Id": "74b23f01-582f-4c6b-9f6c-e5c17c41c466", + "Children": [ + "381278fa-4eb9-4110-b64c-3bbdb76a0bd5", + "ce5ccafe-1165-4a3a-afb2-8da27cc123e2" + ], + "Parent": "10f2a2c4-b3e4-478d-84d5-a74a71ee8235" + }, + "caf1ebc4-af44-4cad-935d-b44a8ca57d32": { + "Name": "multi_partition_game", + "ClassName": "DataModel", + "Properties": {}, + "Id": "caf1ebc4-af44-4cad-935d-b44a8ca57d32", + "Children": [ + "10f2a2c4-b3e4-478d-84d5-a74a71ee8235", + "b411bf23-4662-4799-a8eb-6afc1bd8888e" + ], + "Parent": null + }, + "10f2a2c4-b3e4-478d-84d5-a74a71ee8235": { + "Name": "ReplicatedStorage", + "ClassName": "ReplicatedStorage", + "Properties": {}, + "Id": "10f2a2c4-b3e4-478d-84d5-a74a71ee8235", + "Children": [ + "74b23f01-582f-4c6b-9f6c-e5c17c41c466", + "ea14bb8b-cc04-4b0c-9aae-02f1447cbda7" + ], + "Parent": "caf1ebc4-af44-4cad-935d-b44a8ca57d32" + }, + "0de0ab3d-e74a-473b-808a-076febe9d808": { + "Name": "main", + "ClassName": "ModuleScript", + "Properties": { + "Source": { + "Type": "String", + "Value": "-- hello, from a/main.lua" + } + }, + "Id": "0de0ab3d-e74a-473b-808a-076febe9d808", + "Children": [], + "Parent": "ea14bb8b-cc04-4b0c-9aae-02f1447cbda7" + }, + "b411bf23-4662-4799-a8eb-6afc1bd8888e": { + "Name": "HttpService", + "ClassName": "HttpService", + "Properties": { + "HttpEnabled": { + "Type": "Bool", + "Value": true + } + }, + "Id": "b411bf23-4662-4799-a8eb-6afc1bd8888e", + "Children": [], + "Parent": "caf1ebc4-af44-4cad-935d-b44a8ca57d32" + }, + "ea14bb8b-cc04-4b0c-9aae-02f1447cbda7": { + "Name": "Ack", + "ClassName": "Folder", + "Properties": {}, + "Id": "ea14bb8b-cc04-4b0c-9aae-02f1447cbda7", + "Children": [ + "833f7f8b-7ec3-4f6a-9476-63af9612510a", + "0de0ab3d-e74a-473b-808a-076febe9d808" + ], + "Parent": "10f2a2c4-b3e4-478d-84d5-a74a71ee8235" + }, + "ce5ccafe-1165-4a3a-afb2-8da27cc123e2": { + "Name": "added", + "ClassName": "Folder", + "Properties": {}, + "Id": "ce5ccafe-1165-4a3a-afb2-8da27cc123e2", + "Children": [], + "Parent": "74b23f01-582f-4c6b-9f6c-e5c17c41c466" + } + }, + "root_id": "caf1ebc4-af44-4cad-935d-b44a8ca57d32" +} \ No newline at end of file