From ec0a1f1ce4c3749f638bb3ab2df41a0bbeb97ee9 Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Thu, 14 Mar 2019 14:20:03 -0700 Subject: [PATCH] New snapshot tests (#134) * Changes project-related structures to use `BTreeMap` instead of `HashMap` for children to aid determiniusm * Changes imfs-related structures to have total ordering and use `BTreeSet` instead of `HashSet` * Upgrades dependencies to `bx_dom_weak`1.2.0 and rbx_xml 0.5.0 to aid in more determinism stuff * Re-exposes the `RbxSession`'s root project via `root_project()` * Implements `Default` for a couple things * Tweaks visualization code to support visualizing trees not attached to an `RbxSession` * Adds an ID-invariant comparison method for `rbx_tree` relying on previous determinism changes * Adds a (disabled) test to start finding issues in the reconciler with regards to communicativity of snapshot application * Adds a snapshot testing system that operates on `RbxTree` and associated metadata, which are committed in this change --- .gitignore | 3 +- Cargo.lock | 18 +- server/Cargo.toml | 4 +- server/src/imfs.rs | 43 ++- server/src/live_session.rs | 4 + server/src/path_map.rs | 6 + server/src/project.rs | 18 +- server/src/rbx_session.rs | 4 + server/src/snapshot_reconciler.rs | 2 +- server/src/visualize.rs | 55 ++- server/tests/imfs.rs | 8 +- server/tests/read_projects.rs | 6 +- server/tests/snapshot_reconciler.rs | 115 ++++++ server/tests/snapshot_snapshots.rs | 68 ++++ server/tests/snapshots.rs | 128 ------- server/tests/test_util/mod.rs | 28 +- server/tests/test_util/snapshot.rs | 79 ++++ server/tests/test_util/tree.rs | 351 ++++++++++++++++++ server/tests/tree_snapshots.rs | 68 ++++ test-projects/multi_partition_game/a/foo.txt | 1 + test-projects/multi_partition_game/a/main.lua | 1 + .../multi_partition_game/b/something.lua | 1 + .../multi_partition_game/default.project.json | 21 ++ .../expected-snapshot.json | 212 +++++++++++ .../multi_partition_game/initial.tree.json | 242 ++++++++++++ .../multi_partition_game/with_dir.tree.json | 256 +++++++++++++ .../with_moved_dir.tree.json | 256 +++++++++++++ 27 files changed, 1793 insertions(+), 205 deletions(-) create mode 100644 server/tests/snapshot_reconciler.rs create mode 100644 server/tests/snapshot_snapshots.rs delete mode 100644 server/tests/snapshots.rs create mode 100644 server/tests/test_util/snapshot.rs create mode 100644 server/tests/test_util/tree.rs create mode 100644 server/tests/tree_snapshots.rs create mode 100644 test-projects/multi_partition_game/a/foo.txt create mode 100644 test-projects/multi_partition_game/a/main.lua create mode 100644 test-projects/multi_partition_game/b/something.lua create mode 100644 test-projects/multi_partition_game/default.project.json create mode 100644 test-projects/multi_partition_game/expected-snapshot.json create mode 100644 test-projects/multi_partition_game/initial.tree.json create mode 100644 test-projects/multi_partition_game/with_dir.tree.json create mode 100644 test-projects/multi_partition_game/with_moved_dir.tree.json diff --git a/.gitignore b/.gitignore index d77fcf54..216fc1c5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /target /scratch-project **/*.rs.bk -/generate-docs.run \ No newline at end of file +/generate-docs.run +/server/failed-snapshots/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index bff54a52..189076f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1034,12 +1034,12 @@ dependencies = [ "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "lz4 1.23.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rbx_dom_weak 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rbx_dom_weak 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "rbx_dom_weak" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1055,12 +1055,12 @@ dependencies = [ "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rbx_dom_weak 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rbx_dom_weak 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "rbx_xml" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1068,7 +1068,7 @@ dependencies = [ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rbx_dom_weak 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rbx_dom_weak 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "xml-rs 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1200,9 +1200,9 @@ dependencies = [ "paste 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "rbx_binary 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rbx_dom_weak 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rbx_dom_weak 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "rbx_reflection 2.0.374 (registry+https://github.com/rust-lang/crates.io-index)", - "rbx_xml 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rbx_xml 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", "ritz 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1874,9 +1874,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" "checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" "checksum rbx_binary 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b147f236284747ac1b4643476265dd36b402877d97adb7cbd0fafc1d247de0a5" -"checksum rbx_dom_weak 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c44f210860e85fa3ea733a3a03e8585f9a47a1f6fcd218f3d577fadd5de089bc" +"checksum rbx_dom_weak 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e31ec3223caeb1a57254a8a10b33614a2d457517be01ef93694f83d995833b04" "checksum rbx_reflection 2.0.374 (registry+https://github.com/rust-lang/crates.io-index)" = "8a826ff869b33b54db727f9776f1dc7a8a779791f5a46ddd4941b8334bf909fe" -"checksum rbx_xml 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de8408fec1a0b4b73fda1bfcd70757f34a38ef2af84618b57da69d422ea13841" +"checksum rbx_xml 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a240c155684b744c4985f283702b61f6ab0a2d4479694051d875844632c9f454" "checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" "checksum redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)" = "423e376fffca3dfa06c9e9790a9ccd282fafb3cc6e6397d01dbf64f9bacc6b85" "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" diff --git a/server/Cargo.toml b/server/Cargo.toml index 155e04ff..47b4dccf 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -30,8 +30,8 @@ log = "0.4" maplit = "1.0.1" notify = "4.0" rbx_binary = "0.4.0" -rbx_dom_weak = "1.0.0" -rbx_xml = "0.4.0" +rbx_dom_weak = "1.2.0" +rbx_xml = "0.5.0" rbx_reflection = "2.0.374" regex = "1.0" reqwest = "0.9.5" diff --git a/server/src/imfs.rs b/server/src/imfs.rs index c144ae5e..0672ddb1 100644 --- a/server/src/imfs.rs +++ b/server/src/imfs.rs @@ -1,9 +1,10 @@ use std::{ - collections::{HashMap, HashSet}, - path::{self, Path, PathBuf}, + cmp::Ordering, + collections::{HashMap, HashSet, BTreeSet}, fmt, fs, io, + path::{self, Path, PathBuf}, }; use failure::Fail; @@ -237,7 +238,7 @@ impl Imfs { } else if metadata.is_dir() { let item = ImfsItem::Directory(ImfsDirectory { path: path.to_path_buf(), - children: HashSet::new(), + children: BTreeSet::new(), }); self.items.insert(path.to_path_buf(), item); @@ -285,19 +286,43 @@ impl Imfs { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ImfsFile { pub path: PathBuf, pub contents: Vec, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ImfsDirectory { - pub path: PathBuf, - pub children: HashSet, +impl PartialOrd for ImfsFile { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +impl Ord for ImfsFile { + fn cmp(&self, other: &Self) -> Ordering { + self.path.cmp(&other.path) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ImfsDirectory { + pub path: PathBuf, + pub children: BTreeSet, +} + +impl PartialOrd for ImfsDirectory { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ImfsDirectory { + fn cmp(&self, other: &Self) -> Ordering { + self.path.cmp(&other.path) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum ImfsItem { File(ImfsFile), Directory(ImfsDirectory), diff --git a/server/src/live_session.rs b/server/src/live_session.rs index 1b9db569..b90b6e87 100644 --- a/server/src/live_session.rs +++ b/server/src/live_session.rs @@ -85,6 +85,10 @@ impl LiveSession { Ok(()) } + pub fn root_project(&self) -> &Project { + &self.project + } + pub fn session_id(&self) -> SessionId { self.session_id } diff --git a/server/src/path_map.rs b/server/src/path_map.rs index d944541e..8df2d83a 100644 --- a/server/src/path_map.rs +++ b/server/src/path_map.rs @@ -20,6 +20,12 @@ pub struct PathMap { nodes: HashMap>, } +impl Default for PathMap { + fn default() -> Self { + Self::new() + } +} + impl PathMap { pub fn new() -> PathMap { PathMap { diff --git a/server/src/project.rs b/server/src/project.rs index 878591c2..24d945e4 100644 --- a/server/src/project.rs +++ b/server/src/project.rs @@ -1,5 +1,5 @@ use std::{ - collections::{HashMap, HashSet}, + collections::{HashMap, HashSet, BTreeMap}, fmt, fs::{self, File}, io, @@ -128,7 +128,7 @@ fn serialize_unresolved_map(value: &HashMap, seri /// Similar to SourceProject, the structure of nodes in the project tree is /// slightly different on-disk than how we want to handle them in the rest of /// Rojo. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] struct SourceProjectNode { #[serde(rename = "$className", skip_serializing_if = "Option::is_none")] class_name: Option, @@ -148,14 +148,14 @@ struct SourceProjectNode { path: Option, #[serde(flatten)] - children: HashMap, + children: BTreeMap, } impl SourceProjectNode { /// Consumes the SourceProjectNode and turns it into a ProjectNode. - pub fn into_project_node(mut self, project_file_location: &Path) -> ProjectNode { - let children = self.children.drain() - .map(|(key, value)| (key, value.into_project_node(project_file_location))) + pub fn into_project_node(self, project_file_location: &Path) -> ProjectNode { + let children = self.children.iter() + .map(|(key, value)| (key.clone(), value.clone().into_project_node(project_file_location))) .collect(); // Make sure that paths are absolute, transforming them by adding the @@ -264,7 +264,7 @@ pub enum ProjectSaveError { #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] pub struct ProjectNode { pub class_name: Option, - pub children: HashMap, + pub children: BTreeMap, pub properties: HashMap, pub ignore_unknown_instances: Option, @@ -486,6 +486,10 @@ impl Project { } } + pub fn folder_location(&self) -> &Path { + self.file_location.parent().unwrap() + } + fn to_source_project(&self) -> SourceProject { let plugins = self.plugins .iter() diff --git a/server/src/rbx_session.rs b/server/src/rbx_session.rs index 00940dca..7310736a 100644 --- a/server/src/rbx_session.rs +++ b/server/src/rbx_session.rs @@ -251,6 +251,10 @@ impl RbxSession { &self.tree } + pub fn get_all_instance_metadata(&self) -> &HashMap { + &self.metadata_per_instance + } + pub fn get_instance_metadata(&self, id: RbxId) -> Option<&MetadataPerInstance> { self.metadata_per_instance.get(&id) } diff --git a/server/src/snapshot_reconciler.rs b/server/src/snapshot_reconciler.rs index 42024ecb..2b52ca52 100644 --- a/server/src/snapshot_reconciler.rs +++ b/server/src/snapshot_reconciler.rs @@ -64,7 +64,7 @@ impl InstanceChanges { /// A lightweight, hierarchical representation of an instance that can be /// applied to the tree. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct RbxSnapshotInstance<'a> { pub name: Cow<'a, str>, pub class_name: Cow<'a, str>, diff --git a/server/src/visualize.rs b/server/src/visualize.rs index 32ab0786..d0b36604 100644 --- a/server/src/visualize.rs +++ b/server/src/visualize.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashMap, fmt, io::Write, path::Path, @@ -6,12 +7,13 @@ use std::{ }; use log::warn; -use rbx_dom_weak::RbxId; +use rbx_dom_weak::{RbxTree, RbxId}; use crate::{ imfs::{Imfs, ImfsItem}, rbx_session::RbxSession, web::api::PublicInstanceMetadata, + rbx_session::MetadataPerInstance, }; static GRAPHVIZ_HEADER: &str = r#" @@ -53,42 +55,59 @@ pub fn graphviz_to_svg(source: &str) -> Option { Some(String::from_utf8(output.stdout).expect("Failed to parse stdout as UTF-8")) } +pub struct VisualizeRbxTree<'a, 'b> { + pub tree: &'a RbxTree, + pub metadata: &'b HashMap, +} + +impl<'a, 'b> fmt::Display for VisualizeRbxTree<'a, 'b> { + fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result { + writeln!(output, "{}", GRAPHVIZ_HEADER)?; + + visualize_instance(&self.tree, self.tree.get_root_id(), &self.metadata, output)?; + + writeln!(output, "}}") + } +} + /// A Display wrapper struct to visualize an RbxSession as SVG. pub struct VisualizeRbxSession<'a>(pub &'a RbxSession); impl<'a> fmt::Display for VisualizeRbxSession<'a> { fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result { - writeln!(output, "{}", GRAPHVIZ_HEADER)?; - - visualize_rbx_node(self.0, self.0.get_tree().get_root_id(), output)?; - - writeln!(output, "}}")?; - - Ok(()) + writeln!(output, "{}", VisualizeRbxTree { + tree: self.0.get_tree(), + metadata: self.0.get_all_instance_metadata(), + }) } } -fn visualize_rbx_node(session: &RbxSession, id: RbxId, output: &mut fmt::Formatter) -> fmt::Result { - let node = session.get_tree().get_instance(id).unwrap(); +fn visualize_instance( + tree: &RbxTree, + id: RbxId, + metadata: &HashMap, + output: &mut fmt::Formatter, +) -> fmt::Result { + let instance = tree.get_instance(id).unwrap(); - let mut node_label = format!("{}|{}|{}", node.name, node.class_name, id); + let mut instance_label = format!("{}|{}|{}", instance.name, instance.class_name, id); - if let Some(session_metadata) = session.get_instance_metadata(id) { + if let Some(session_metadata) = metadata.get(&id) { let metadata = PublicInstanceMetadata::from_session_metadata(session_metadata); - node_label.push('|'); - node_label.push_str(&serde_json::to_string(&metadata).unwrap()); + instance_label.push('|'); + instance_label.push_str(&serde_json::to_string(&metadata).unwrap()); } - node_label = node_label + instance_label = instance_label .replace("\"", """) .replace("{", "\\{") .replace("}", "\\}"); - writeln!(output, " \"{}\" [label=\"{}\"]", id, node_label)?; + writeln!(output, " \"{}\" [label=\"{}\"]", id, instance_label)?; - for &child_id in node.get_children_ids() { + for &child_id in instance.get_children_ids() { writeln!(output, " \"{}\" -> \"{}\"", id, child_id)?; - visualize_rbx_node(session, child_id, output)?; + visualize_instance(tree, child_id, metadata, output)?; } Ok(()) diff --git a/server/tests/imfs.rs b/server/tests/imfs.rs index 617ce06a..6dc3f49d 100644 --- a/server/tests/imfs.rs +++ b/server/tests/imfs.rs @@ -1,5 +1,5 @@ use std::{ - collections::{HashMap, HashSet}, + collections::{HashMap, HashSet, BTreeSet}, fs, path::PathBuf, }; @@ -80,7 +80,7 @@ fn base_tree() -> Result<(TempDir, Imfs, ExpectedImfs, TestResources), Error> { expected_roots.insert(root.path().to_path_buf()); let root_item = { - let mut children = HashSet::new(); + let mut children = BTreeSet::new(); children.insert(foo_path.clone()); children.insert(bar_path.clone()); @@ -91,7 +91,7 @@ fn base_tree() -> Result<(TempDir, Imfs, ExpectedImfs, TestResources), Error> { }; let foo_item = { - let mut children = HashSet::new(); + let mut children = BTreeSet::new(); children.insert(baz_path.clone()); ImfsItem::Directory(ImfsDirectory { @@ -199,7 +199,7 @@ fn adding_folder() -> Result<(), Error> { } let folder_item = { - let mut children = HashSet::new(); + let mut children = BTreeSet::new(); children.insert(file1_path.clone()); children.insert(file2_path.clone()); diff --git a/server/tests/read_projects.rs b/server/tests/read_projects.rs index 63195acf..38cb5f89 100644 --- a/server/tests/read_projects.rs +++ b/server/tests/read_projects.rs @@ -1,7 +1,7 @@ #[macro_use] extern crate lazy_static; use std::{ - collections::HashMap, + collections::{HashMap, BTreeMap}, path::{Path, PathBuf}, }; @@ -53,7 +53,7 @@ fn single_partition_game() { ..Default::default() }; - let mut replicated_storage_children = HashMap::new(); + let mut replicated_storage_children = BTreeMap::new(); replicated_storage_children.insert("Foo".to_string(), foo); let replicated_storage = ProjectNode { @@ -73,7 +73,7 @@ fn single_partition_game() { ..Default::default() }; - let mut root_children = HashMap::new(); + let mut root_children = BTreeMap::new(); root_children.insert("ReplicatedStorage".to_string(), replicated_storage); root_children.insert("HttpService".to_string(), http_service); diff --git a/server/tests/snapshot_reconciler.rs b/server/tests/snapshot_reconciler.rs new file mode 100644 index 00000000..4888e917 --- /dev/null +++ b/server/tests/snapshot_reconciler.rs @@ -0,0 +1,115 @@ +mod test_util; + +use std::collections::HashMap; + +use pretty_assertions::assert_eq; +use rbx_dom_weak::{RbxTree, RbxInstanceProperties}; + +use librojo::{ + snapshot_reconciler::{RbxSnapshotInstance, reconcile_subtree}, +}; + +use test_util::tree::trees_equal; + +// TODO: Snapshot application isn't communicative right now with the current +// snapshot reconciler. In practice this mostly isn't a problem, but presents +// a problem trying to rely on determinism to make snapshot tests. +// #[test] +fn patch_communicativity() { + let base_tree = RbxTree::new(RbxInstanceProperties { + name: "DataModel".into(), + class_name: "DataModel".into(), + properties: HashMap::new(), + }); + + let patch_a = RbxSnapshotInstance { + name: "DataModel".into(), + class_name: "DataModel".into(), + children: vec![ + RbxSnapshotInstance { + name: "Child-A".into(), + class_name: "Folder".into(), + ..Default::default() + }, + ], + ..Default::default() + }; + + let patch_b = RbxSnapshotInstance { + name: "DataModel".into(), + class_name: "DataModel".into(), + children: vec![ + RbxSnapshotInstance { + name: "Child-B".into(), + class_name: "Folder".into(), + ..Default::default() + }, + ], + ..Default::default() + }; + + let patch_combined = RbxSnapshotInstance { + name: "DataModel".into(), + class_name: "DataModel".into(), + children: vec![ + RbxSnapshotInstance { + name: "Child-A".into(), + class_name: "Folder".into(), + ..Default::default() + }, + RbxSnapshotInstance { + name: "Child-B".into(), + class_name: "Folder".into(), + ..Default::default() + }, + ], + ..Default::default() + }; + + let root_id = base_tree.get_root_id(); + + let mut tree_a = base_tree.clone(); + + reconcile_subtree( + &mut tree_a, + root_id, + &patch_a, + &mut Default::default(), + &mut Default::default(), + &mut Default::default(), + ); + + reconcile_subtree( + &mut tree_a, + root_id, + &patch_combined, + &mut Default::default(), + &mut Default::default(), + &mut Default::default(), + ); + + let mut tree_b = base_tree.clone(); + + reconcile_subtree( + &mut tree_b, + root_id, + &patch_b, + &mut Default::default(), + &mut Default::default(), + &mut Default::default(), + ); + + reconcile_subtree( + &mut tree_b, + root_id, + &patch_combined, + &mut Default::default(), + &mut Default::default(), + &mut Default::default(), + ); + + match trees_equal(&tree_a, &tree_b) { + Ok(_) => {} + Err(e) => panic!("{}", e), + } +} \ No newline at end of file diff --git a/server/tests/snapshot_snapshots.rs b/server/tests/snapshot_snapshots.rs new file mode 100644 index 00000000..d697f50f --- /dev/null +++ b/server/tests/snapshot_snapshots.rs @@ -0,0 +1,68 @@ +mod test_util; + +use std::path::Path; + +use pretty_assertions::assert_eq; + +use librojo::{ + imfs::Imfs, + project::Project, + rbx_snapshot::{SnapshotContext, snapshot_project_tree}, +}; + +use crate::test_util::{ + snapshot::*, +}; + +macro_rules! generate_snapshot_tests { + ($($name: ident),*) => { + $( + 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); + } + } + )* + }; +} + +generate_snapshot_tests!( + empty, + multi_partition_game, + nested_partitions, + single_partition_game, + single_partition_model, + transmute_partition +); + +fn run_snapshot_test(path: &Path) { + println!("Running snapshot from project: {}", path.display()); + + let project = Project::load_fuzzy(path) + .expect("Couldn't load project file for snapshot test"); + + let mut imfs = Imfs::new(); + imfs.add_roots_from_project(&project) + .expect("Could not add IMFS roots to snapshot project"); + + let context = SnapshotContext { + plugin_context: None, + }; + + let mut snapshot = snapshot_project_tree(&context, &imfs, &project) + .expect("Could not generate snapshot for snapshot test"); + + if let Some(snapshot) = snapshot.as_mut() { + anonymize_snapshot(path, snapshot); + } + + match read_expected_snapshot(path) { + Some(expected_snapshot) => assert_eq!(snapshot, expected_snapshot), + None => write_expected_snapshot(path, &snapshot), + } +} \ No newline at end of file diff --git a/server/tests/snapshots.rs b/server/tests/snapshots.rs deleted file mode 100644 index 1707495d..00000000 --- a/server/tests/snapshots.rs +++ /dev/null @@ -1,128 +0,0 @@ -use std::{ - fs::{self, File}, - path::{Path, PathBuf}, -}; - -use pretty_assertions::assert_eq; - -use librojo::{ - imfs::Imfs, - project::{Project, ProjectNode}, - rbx_snapshot::{SnapshotContext, snapshot_project_tree}, - snapshot_reconciler::{RbxSnapshotInstance}, -}; - -macro_rules! generate_snapshot_tests { - ($($name: ident),*) => { - $( - paste::item! { - #[test] - fn []() { - 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); - } - } - )* - }; -} - -generate_snapshot_tests!( - empty, - nested_partitions, - single_partition_game, - single_partition_model, - transmute_partition -); - -const SNAPSHOT_EXPECTED_NAME: &str = "expected-snapshot.json"; - -fn run_snapshot_test(path: &Path) { - println!("Running snapshot from project: {}", path.display()); - - let project = Project::load_fuzzy(path) - .expect("Couldn't load project file for snapshot test"); - - let mut imfs = Imfs::new(); - imfs.add_roots_from_project(&project) - .expect("Could not add IMFS roots to snapshot project"); - - let context = SnapshotContext { - plugin_context: None, - }; - - let mut snapshot = snapshot_project_tree(&context, &imfs, &project) - .expect("Could not generate snapshot for snapshot test"); - - if let Some(snapshot) = snapshot.as_mut() { - anonymize_snapshot(path, snapshot); - } - - match read_expected_snapshot(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..5988865d 100644 --- a/server/tests/test_util/mod.rs +++ b/server/tests/test_util/mod.rs @@ -1,31 +1,13 @@ +#![allow(dead_code)] + 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 +33,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..43088684 --- /dev/null +++ b/server/tests/test_util/snapshot.rs @@ -0,0 +1,79 @@ +use std::{ + fs::{self, File}, + path::{Path, PathBuf}, +}; + +use librojo::{ + project::ProjectNode, + snapshot_reconciler::RbxSnapshotInstance, + rbx_session::MetadataPerInstance, +}; + +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) { + anonymize_metadata(project_folder_path, &mut snapshot.metadata); + + 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_metadata(project_folder_path: &Path, metadata: &mut MetadataPerInstance) { + match metadata.source_path.as_mut() { + Some(path) => *path = anonymize_path(project_folder_path, path), + None => {}, + } + + match metadata.project_definition.as_mut() { + Some((_, project_node)) => anonymize_project_node(project_folder_path, project_node), + None => {}, + } +} + +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_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..4f2525de --- /dev/null +++ b/server/tests/test_util/tree.rs @@ -0,0 +1,351 @@ +//! 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 potentially 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::{HashMap, HashSet}, + fmt, + fs::{self, File}, + hash::Hash, + path::{Path, PathBuf}, +}; + +use log::error; +use serde_derive::{Serialize, Deserialize}; +use rbx_dom_weak::{RbxId, RbxTree}; + +use librojo::{ + rbx_session::MetadataPerInstance, + live_session::LiveSession, + visualize::{VisualizeRbxTree, graphviz_to_svg}, +}; + +use super::snapshot::anonymize_metadata; + +/// 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. +pub 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(); + + let project_folder = live_session.root_project().folder_location(); + let metadata = rbx_session.get_all_instance_metadata() + .iter() + .map(|(key, meta)| { + let mut meta = meta.clone(); + anonymize_metadata(project_folder, &mut meta); + (*key, meta) + }) + .collect(); + + let tree_with_metadata = TreeWithMetadata { + tree: Cow::Borrowed(&tree), + metadata: Cow::Owned(metadata), + }; + + match read_tree_by_name(source_path, step) { + Some(expected) => match trees_and_metadata_equal(&expected, &tree_with_metadata) { + Ok(_) => {} + Err(e) => { + error!("Trees at step '{}' were not equal.\n{}", step, e); + + let expected_gv = format!("{}", VisualizeRbxTree { + tree: &expected.tree, + metadata: &expected.metadata, + }); + + let actual_gv = format!("{}", VisualizeRbxTree { + tree: &tree_with_metadata.tree, + metadata: &tree_with_metadata.metadata, + }); + + let output_dir = PathBuf::from("failed-snapshots"); + fs::create_dir_all(&output_dir) + .expect("Could not create failed-snapshots directory"); + + let expected_basename = format!("{}-{}-expected", live_session.root_project().name, step); + let actual_basename = format!("{}-{}-actual", live_session.root_project().name, step); + + let mut expected_out = output_dir.join(expected_basename); + let mut actual_out = output_dir.join(actual_basename); + + match (graphviz_to_svg(&expected_gv), graphviz_to_svg(&actual_gv)) { + (Some(expected_svg), Some(actual_svg)) => { + expected_out.set_extension("svg"); + actual_out.set_extension("svg"); + + fs::write(&expected_out, expected_svg) + .expect("Couldn't write expected SVG"); + + fs::write(&actual_out, actual_svg) + .expect("Couldn't write actual SVG"); + } + _ => { + expected_out.set_extension("gv"); + actual_out.set_extension("gv"); + + fs::write(&expected_out, expected_gv) + .expect("Couldn't write expected GV"); + + fs::write(&actual_out, actual_gv) + .expect("Couldn't write actual GV"); + } + } + + error!("Output at {} and {}", expected_out.display(), actual_out.display()); + + panic!("Tree mismatch at step '{}'", step); + } + } + None => { + write_tree_by_name(source_path, step, &tree_with_metadata); + } + } +} + +fn new_cow_map() -> Cow<'static, HashMap> { + Cow::Owned(HashMap::new()) +} + +#[derive(Debug, Serialize, Deserialize)] +struct TreeWithMetadata<'a> { + #[serde(flatten)] + pub tree: Cow<'a, RbxTree>, + + #[serde(default = "new_cow_map")] + pub metadata: Cow<'a, HashMap>, +} + +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: TreeWithMetadata = serde_json::from_slice(&contents) + .expect("Could not deserialize tree"); + + Some(tree) +} + +fn write_tree_by_name(path: &Path, identifier: &str, tree: &TreeWithMetadata) { + 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"); +} + +#[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()), + } + } + + 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 = TreeWithMetadata { + tree: Cow::Borrowed(left_tree), + metadata: Cow::Owned(HashMap::new()), + }; + + let right = TreeWithMetadata { + tree: Cow::Borrowed(right_tree), + metadata: Cow::Owned(HashMap::new()), + }; + + trees_and_metadata_equal(&left, &right) +} + +fn trees_and_metadata_equal( + left_tree: &TreeWithMetadata, + right_tree: &TreeWithMetadata, +) -> Result<(), TreeMismatch> { + let left_id = left_tree.tree.get_root_id(); + let right_id = right_tree.tree.get_root_id(); + + instances_equal(left_tree, left_id, right_tree, right_id) +} + +fn instances_equal( + left_tree: &TreeWithMetadata, + left_id: RbxId, + right_tree: &TreeWithMetadata, + right_id: RbxId, +) -> Result<(), TreeMismatch> { + 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)?; + metadata_equal(left_tree, left_id, right_tree, right_id) +} + +fn basic_equal( + left_tree: &TreeWithMetadata, + left_id: RbxId, + right_tree: &TreeWithMetadata, + right_id: RbxId, +) -> Result<(), TreeMismatch> { + let left_instance = left_tree.tree.get_instance(left_id) + .expect("ID did not exist in left tree"); + + let right_instance = right_tree.tree.get_instance(right_id) + .expect("ID did not exist in right tree"); + + if left_instance.name != right_instance.name { + let message = format!("Name did not match ('{}' vs '{}')", left_instance.name, right_instance.name); + + return Err(TreeMismatch::new(&left_instance.name, message)); + } + + if left_instance.class_name != right_instance.class_name { + let message = format!("Class name did not match ('{}' vs '{}')", left_instance.class_name, right_instance.class_name); + + return Err(TreeMismatch::new(&left_instance.name, message)); + } + + Ok(()) +} + +fn properties_equal( + left_tree: &TreeWithMetadata, + left_id: RbxId, + right_tree: &TreeWithMetadata, + right_id: RbxId, +) -> Result<(), TreeMismatch> { + let left_instance = left_tree.tree.get_instance(left_id) + .expect("ID did not exist in left tree"); + + let right_instance = right_tree.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 {}:\n\tLeft: {:?}\n\tRight: {:?}", + key, + left_value, + Some(right_value), + ); + + return Err(TreeMismatch::new(&left_instance.name, message)); + } + } + + Ok(()) +} + +fn children_equal( + left_tree: &TreeWithMetadata, + left_id: RbxId, + right_tree: &TreeWithMetadata, + right_id: RbxId, +) -> Result<(), TreeMismatch> { + let left_instance = left_tree.tree.get_instance(left_id) + .expect("ID did not exist in left tree"); + + let right_instance = right_tree.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")); + } + + for (left_child_id, right_child_id) in left_children.iter().zip(right_children) { + instances_equal(left_tree, *left_child_id, right_tree, *right_child_id) + .map_err(|e| e.add_parent(&left_instance.name))?; + } + + Ok(()) +} + +fn metadata_equal( + left_tree: &TreeWithMetadata, + left_id: RbxId, + right_tree: &TreeWithMetadata, + right_id: RbxId, +) -> Result<(), TreeMismatch> { + let left_meta = left_tree.metadata.get(&left_id); + let right_meta = right_tree.metadata.get(&right_id); + + if left_meta != right_meta { + let left_instance = left_tree.tree.get_instance(left_id) + .expect("Left instance didn't exist in tree"); + + let message = format!( + "Metadata mismatch:\n\tLeft: {:?}\n\tRight: {:?}", + left_meta, + right_meta, + ); + + return Err(TreeMismatch::new(&left_instance.name, message)); + } + + Ok(()) +} \ No newline at end of file diff --git a/server/tests/tree_snapshots.rs b/server/tests/tree_snapshots.rs new file mode 100644 index 00000000..f5b991fa --- /dev/null +++ b/server/tests/tree_snapshots.rs @@ -0,0 +1,68 @@ +mod test_util; + +use std::{ + fs, + path::{Path, PathBuf}, + sync::Arc, + thread, + time::Duration, +}; + +use tempfile::{tempdir, TempDir}; + +use librojo::{ + live_session::LiveSession, + project::Project, +}; + +use crate::test_util::{ + copy_recursive, + tree::tree_step, +}; + +#[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) +} \ 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..b6a2208c --- /dev/null +++ b/test-projects/multi_partition_game/initial.tree.json @@ -0,0 +1,242 @@ +{ + "instances": { + "8d44bb30-db3c-4366-a6c5-633bd1441885": { + "Name": "main", + "ClassName": "ModuleScript", + "Properties": { + "Source": { + "Type": "String", + "Value": "-- hello, from a/main.lua" + } + }, + "Id": "8d44bb30-db3c-4366-a6c5-633bd1441885", + "Children": [], + "Parent": "1aafa29b-bdca-40a0-a677-7ead327b84ce" + }, + "b1c9928c-bf11-427f-90eb-b672c811d859": { + "Name": "Bar", + "ClassName": "Folder", + "Properties": {}, + "Id": "b1c9928c-bf11-427f-90eb-b672c811d859", + "Children": [ + "8d690a2a-e987-4c86-b9ac-18e6d3a98503" + ], + "Parent": "9f141826-14c2-492b-b360-2558712f0c08" + }, + "645ba594-4482-441f-9f41-5bb9c444405b": { + "Name": "multi_partition_game", + "ClassName": "DataModel", + "Properties": {}, + "Id": "645ba594-4482-441f-9f41-5bb9c444405b", + "Children": [ + "b1298bcc-e370-44a6-9ef4-fbefa290124c", + "9f141826-14c2-492b-b360-2558712f0c08" + ], + "Parent": null + }, + "9f141826-14c2-492b-b360-2558712f0c08": { + "Name": "ReplicatedStorage", + "ClassName": "ReplicatedStorage", + "Properties": {}, + "Id": "9f141826-14c2-492b-b360-2558712f0c08", + "Children": [ + "1aafa29b-bdca-40a0-a677-7ead327b84ce", + "b1c9928c-bf11-427f-90eb-b672c811d859" + ], + "Parent": "645ba594-4482-441f-9f41-5bb9c444405b" + }, + "8d690a2a-e987-4c86-b9ac-18e6d3a98503": { + "Name": "something", + "ClassName": "ModuleScript", + "Properties": { + "Source": { + "Type": "String", + "Value": "-- b/something.lua" + } + }, + "Id": "8d690a2a-e987-4c86-b9ac-18e6d3a98503", + "Children": [], + "Parent": "b1c9928c-bf11-427f-90eb-b672c811d859" + }, + "b1298bcc-e370-44a6-9ef4-fbefa290124c": { + "Name": "HttpService", + "ClassName": "HttpService", + "Properties": { + "HttpEnabled": { + "Type": "Bool", + "Value": true + } + }, + "Id": "b1298bcc-e370-44a6-9ef4-fbefa290124c", + "Children": [], + "Parent": "645ba594-4482-441f-9f41-5bb9c444405b" + }, + "54f2f276-964f-4c60-87d8-5fb2209c97c9": { + "Name": "foo", + "ClassName": "StringValue", + "Properties": { + "Value": { + "Type": "String", + "Value": "Hello world, from a/foo.txt" + } + }, + "Id": "54f2f276-964f-4c60-87d8-5fb2209c97c9", + "Children": [], + "Parent": "1aafa29b-bdca-40a0-a677-7ead327b84ce" + }, + "1aafa29b-bdca-40a0-a677-7ead327b84ce": { + "Name": "Ack", + "ClassName": "Folder", + "Properties": {}, + "Id": "1aafa29b-bdca-40a0-a677-7ead327b84ce", + "Children": [ + "54f2f276-964f-4c60-87d8-5fb2209c97c9", + "8d44bb30-db3c-4366-a6c5-633bd1441885" + ], + "Parent": "9f141826-14c2-492b-b360-2558712f0c08" + } + }, + "root_id": "645ba594-4482-441f-9f41-5bb9c444405b", + "metadata": { + "645ba594-4482-441f-9f41-5bb9c444405b": { + "ignore_unknown_instances": true, + "source_path": null, + "project_definition": [ + "multi_partition_game", + { + "class_name": "DataModel", + "children": { + "HttpService": { + "class_name": "HttpService", + "children": {}, + "properties": { + "HttpEnabled": { + "Type": "Bool", + "Value": true + } + }, + "ignore_unknown_instances": null, + "path": null + }, + "ReplicatedStorage": { + "class_name": "ReplicatedStorage", + "children": { + "Ack": { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "a" + }, + "Bar": { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "b" + } + }, + "properties": {}, + "ignore_unknown_instances": null, + "path": null + } + }, + "properties": {}, + "ignore_unknown_instances": null, + "path": null + } + ] + }, + "1aafa29b-bdca-40a0-a677-7ead327b84ce": { + "ignore_unknown_instances": false, + "source_path": "a", + "project_definition": [ + "Ack", + { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "a" + } + ] + }, + "b1c9928c-bf11-427f-90eb-b672c811d859": { + "ignore_unknown_instances": false, + "source_path": "b", + "project_definition": [ + "Bar", + { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "b" + } + ] + }, + "54f2f276-964f-4c60-87d8-5fb2209c97c9": { + "ignore_unknown_instances": false, + "source_path": "a/foo.txt", + "project_definition": null + }, + "9f141826-14c2-492b-b360-2558712f0c08": { + "ignore_unknown_instances": true, + "source_path": null, + "project_definition": [ + "ReplicatedStorage", + { + "class_name": "ReplicatedStorage", + "children": { + "Ack": { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "a" + }, + "Bar": { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "b" + } + }, + "properties": {}, + "ignore_unknown_instances": null, + "path": null + } + ] + }, + "8d44bb30-db3c-4366-a6c5-633bd1441885": { + "ignore_unknown_instances": false, + "source_path": "a/main.lua", + "project_definition": null + }, + "b1298bcc-e370-44a6-9ef4-fbefa290124c": { + "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 + } + ] + }, + "8d690a2a-e987-4c86-b9ac-18e6d3a98503": { + "ignore_unknown_instances": false, + "source_path": "b/something.lua", + "project_definition": null + } + } +} \ 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..5237c507 --- /dev/null +++ b/test-projects/multi_partition_game/with_dir.tree.json @@ -0,0 +1,256 @@ +{ + "instances": { + "8d44bb30-db3c-4366-a6c5-633bd1441885": { + "Name": "main", + "ClassName": "ModuleScript", + "Properties": { + "Source": { + "Type": "String", + "Value": "-- hello, from a/main.lua" + } + }, + "Id": "8d44bb30-db3c-4366-a6c5-633bd1441885", + "Children": [], + "Parent": "1aafa29b-bdca-40a0-a677-7ead327b84ce" + }, + "b1c9928c-bf11-427f-90eb-b672c811d859": { + "Name": "Bar", + "ClassName": "Folder", + "Properties": {}, + "Id": "b1c9928c-bf11-427f-90eb-b672c811d859", + "Children": [ + "8d690a2a-e987-4c86-b9ac-18e6d3a98503" + ], + "Parent": "9f141826-14c2-492b-b360-2558712f0c08" + }, + "645ba594-4482-441f-9f41-5bb9c444405b": { + "Name": "multi_partition_game", + "ClassName": "DataModel", + "Properties": {}, + "Id": "645ba594-4482-441f-9f41-5bb9c444405b", + "Children": [ + "b1298bcc-e370-44a6-9ef4-fbefa290124c", + "9f141826-14c2-492b-b360-2558712f0c08" + ], + "Parent": null + }, + "9f141826-14c2-492b-b360-2558712f0c08": { + "Name": "ReplicatedStorage", + "ClassName": "ReplicatedStorage", + "Properties": {}, + "Id": "9f141826-14c2-492b-b360-2558712f0c08", + "Children": [ + "1aafa29b-bdca-40a0-a677-7ead327b84ce", + "b1c9928c-bf11-427f-90eb-b672c811d859" + ], + "Parent": "645ba594-4482-441f-9f41-5bb9c444405b" + }, + "8d690a2a-e987-4c86-b9ac-18e6d3a98503": { + "Name": "something", + "ClassName": "ModuleScript", + "Properties": { + "Source": { + "Type": "String", + "Value": "-- b/something.lua" + } + }, + "Id": "8d690a2a-e987-4c86-b9ac-18e6d3a98503", + "Children": [], + "Parent": "b1c9928c-bf11-427f-90eb-b672c811d859" + }, + "b1298bcc-e370-44a6-9ef4-fbefa290124c": { + "Name": "HttpService", + "ClassName": "HttpService", + "Properties": { + "HttpEnabled": { + "Type": "Bool", + "Value": true + } + }, + "Id": "b1298bcc-e370-44a6-9ef4-fbefa290124c", + "Children": [], + "Parent": "645ba594-4482-441f-9f41-5bb9c444405b" + }, + "46353305-8818-48fe-94fd-80cf0c5d974c": { + "Name": "added", + "ClassName": "Folder", + "Properties": {}, + "Id": "46353305-8818-48fe-94fd-80cf0c5d974c", + "Children": [], + "Parent": "1aafa29b-bdca-40a0-a677-7ead327b84ce" + }, + "54f2f276-964f-4c60-87d8-5fb2209c97c9": { + "Name": "foo", + "ClassName": "StringValue", + "Properties": { + "Value": { + "Type": "String", + "Value": "Hello world, from a/foo.txt" + } + }, + "Id": "54f2f276-964f-4c60-87d8-5fb2209c97c9", + "Children": [], + "Parent": "1aafa29b-bdca-40a0-a677-7ead327b84ce" + }, + "1aafa29b-bdca-40a0-a677-7ead327b84ce": { + "Name": "Ack", + "ClassName": "Folder", + "Properties": {}, + "Id": "1aafa29b-bdca-40a0-a677-7ead327b84ce", + "Children": [ + "54f2f276-964f-4c60-87d8-5fb2209c97c9", + "8d44bb30-db3c-4366-a6c5-633bd1441885", + "46353305-8818-48fe-94fd-80cf0c5d974c" + ], + "Parent": "9f141826-14c2-492b-b360-2558712f0c08" + } + }, + "root_id": "645ba594-4482-441f-9f41-5bb9c444405b", + "metadata": { + "b1c9928c-bf11-427f-90eb-b672c811d859": { + "ignore_unknown_instances": false, + "source_path": "b", + "project_definition": [ + "Bar", + { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "b" + } + ] + }, + "54f2f276-964f-4c60-87d8-5fb2209c97c9": { + "ignore_unknown_instances": false, + "source_path": "a/foo.txt", + "project_definition": null + }, + "8d44bb30-db3c-4366-a6c5-633bd1441885": { + "ignore_unknown_instances": false, + "source_path": "a/main.lua", + "project_definition": null + }, + "9f141826-14c2-492b-b360-2558712f0c08": { + "ignore_unknown_instances": true, + "source_path": null, + "project_definition": [ + "ReplicatedStorage", + { + "class_name": "ReplicatedStorage", + "children": { + "Ack": { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "a" + }, + "Bar": { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "b" + } + }, + "properties": {}, + "ignore_unknown_instances": null, + "path": null + } + ] + }, + "b1298bcc-e370-44a6-9ef4-fbefa290124c": { + "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 + } + ] + }, + "46353305-8818-48fe-94fd-80cf0c5d974c": { + "ignore_unknown_instances": false, + "source_path": "a/added", + "project_definition": null + }, + "1aafa29b-bdca-40a0-a677-7ead327b84ce": { + "ignore_unknown_instances": false, + "source_path": "a", + "project_definition": [ + "Ack", + { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "a" + } + ] + }, + "8d690a2a-e987-4c86-b9ac-18e6d3a98503": { + "ignore_unknown_instances": false, + "source_path": "b/something.lua", + "project_definition": null + }, + "645ba594-4482-441f-9f41-5bb9c444405b": { + "ignore_unknown_instances": true, + "source_path": null, + "project_definition": [ + "multi_partition_game", + { + "class_name": "DataModel", + "children": { + "HttpService": { + "class_name": "HttpService", + "children": {}, + "properties": { + "HttpEnabled": { + "Type": "Bool", + "Value": true + } + }, + "ignore_unknown_instances": null, + "path": null + }, + "ReplicatedStorage": { + "class_name": "ReplicatedStorage", + "children": { + "Ack": { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "a" + }, + "Bar": { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "b" + } + }, + "properties": {}, + "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/with_moved_dir.tree.json b/test-projects/multi_partition_game/with_moved_dir.tree.json new file mode 100644 index 00000000..4da8884a --- /dev/null +++ b/test-projects/multi_partition_game/with_moved_dir.tree.json @@ -0,0 +1,256 @@ +{ + "instances": { + "8d44bb30-db3c-4366-a6c5-633bd1441885": { + "Name": "main", + "ClassName": "ModuleScript", + "Properties": { + "Source": { + "Type": "String", + "Value": "-- hello, from a/main.lua" + } + }, + "Id": "8d44bb30-db3c-4366-a6c5-633bd1441885", + "Children": [], + "Parent": "1aafa29b-bdca-40a0-a677-7ead327b84ce" + }, + "b1c9928c-bf11-427f-90eb-b672c811d859": { + "Name": "Bar", + "ClassName": "Folder", + "Properties": {}, + "Id": "b1c9928c-bf11-427f-90eb-b672c811d859", + "Children": [ + "8d690a2a-e987-4c86-b9ac-18e6d3a98503", + "a8566e76-0495-45a3-a713-1c59ab39453b" + ], + "Parent": "9f141826-14c2-492b-b360-2558712f0c08" + }, + "645ba594-4482-441f-9f41-5bb9c444405b": { + "Name": "multi_partition_game", + "ClassName": "DataModel", + "Properties": {}, + "Id": "645ba594-4482-441f-9f41-5bb9c444405b", + "Children": [ + "b1298bcc-e370-44a6-9ef4-fbefa290124c", + "9f141826-14c2-492b-b360-2558712f0c08" + ], + "Parent": null + }, + "9f141826-14c2-492b-b360-2558712f0c08": { + "Name": "ReplicatedStorage", + "ClassName": "ReplicatedStorage", + "Properties": {}, + "Id": "9f141826-14c2-492b-b360-2558712f0c08", + "Children": [ + "1aafa29b-bdca-40a0-a677-7ead327b84ce", + "b1c9928c-bf11-427f-90eb-b672c811d859" + ], + "Parent": "645ba594-4482-441f-9f41-5bb9c444405b" + }, + "8d690a2a-e987-4c86-b9ac-18e6d3a98503": { + "Name": "something", + "ClassName": "ModuleScript", + "Properties": { + "Source": { + "Type": "String", + "Value": "-- b/something.lua" + } + }, + "Id": "8d690a2a-e987-4c86-b9ac-18e6d3a98503", + "Children": [], + "Parent": "b1c9928c-bf11-427f-90eb-b672c811d859" + }, + "b1298bcc-e370-44a6-9ef4-fbefa290124c": { + "Name": "HttpService", + "ClassName": "HttpService", + "Properties": { + "HttpEnabled": { + "Type": "Bool", + "Value": true + } + }, + "Id": "b1298bcc-e370-44a6-9ef4-fbefa290124c", + "Children": [], + "Parent": "645ba594-4482-441f-9f41-5bb9c444405b" + }, + "54f2f276-964f-4c60-87d8-5fb2209c97c9": { + "Name": "foo", + "ClassName": "StringValue", + "Properties": { + "Value": { + "Type": "String", + "Value": "Hello world, from a/foo.txt" + } + }, + "Id": "54f2f276-964f-4c60-87d8-5fb2209c97c9", + "Children": [], + "Parent": "1aafa29b-bdca-40a0-a677-7ead327b84ce" + }, + "a8566e76-0495-45a3-a713-1c59ab39453b": { + "Name": "added", + "ClassName": "Folder", + "Properties": {}, + "Id": "a8566e76-0495-45a3-a713-1c59ab39453b", + "Children": [], + "Parent": "b1c9928c-bf11-427f-90eb-b672c811d859" + }, + "1aafa29b-bdca-40a0-a677-7ead327b84ce": { + "Name": "Ack", + "ClassName": "Folder", + "Properties": {}, + "Id": "1aafa29b-bdca-40a0-a677-7ead327b84ce", + "Children": [ + "54f2f276-964f-4c60-87d8-5fb2209c97c9", + "8d44bb30-db3c-4366-a6c5-633bd1441885" + ], + "Parent": "9f141826-14c2-492b-b360-2558712f0c08" + } + }, + "root_id": "645ba594-4482-441f-9f41-5bb9c444405b", + "metadata": { + "645ba594-4482-441f-9f41-5bb9c444405b": { + "ignore_unknown_instances": true, + "source_path": null, + "project_definition": [ + "multi_partition_game", + { + "class_name": "DataModel", + "children": { + "HttpService": { + "class_name": "HttpService", + "children": {}, + "properties": { + "HttpEnabled": { + "Type": "Bool", + "Value": true + } + }, + "ignore_unknown_instances": null, + "path": null + }, + "ReplicatedStorage": { + "class_name": "ReplicatedStorage", + "children": { + "Ack": { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "a" + }, + "Bar": { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "b" + } + }, + "properties": {}, + "ignore_unknown_instances": null, + "path": null + } + }, + "properties": {}, + "ignore_unknown_instances": null, + "path": null + } + ] + }, + "1aafa29b-bdca-40a0-a677-7ead327b84ce": { + "ignore_unknown_instances": false, + "source_path": "a", + "project_definition": [ + "Ack", + { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "a" + } + ] + }, + "a8566e76-0495-45a3-a713-1c59ab39453b": { + "ignore_unknown_instances": false, + "source_path": "b/added", + "project_definition": null + }, + "8d690a2a-e987-4c86-b9ac-18e6d3a98503": { + "ignore_unknown_instances": false, + "source_path": "b/something.lua", + "project_definition": null + }, + "8d44bb30-db3c-4366-a6c5-633bd1441885": { + "ignore_unknown_instances": false, + "source_path": "a/main.lua", + "project_definition": null + }, + "9f141826-14c2-492b-b360-2558712f0c08": { + "ignore_unknown_instances": true, + "source_path": null, + "project_definition": [ + "ReplicatedStorage", + { + "class_name": "ReplicatedStorage", + "children": { + "Ack": { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "a" + }, + "Bar": { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "b" + } + }, + "properties": {}, + "ignore_unknown_instances": null, + "path": null + } + ] + }, + "b1c9928c-bf11-427f-90eb-b672c811d859": { + "ignore_unknown_instances": false, + "source_path": "b", + "project_definition": [ + "Bar", + { + "class_name": null, + "children": {}, + "properties": {}, + "ignore_unknown_instances": null, + "path": "b" + } + ] + }, + "54f2f276-964f-4c60-87d8-5fb2209c97c9": { + "ignore_unknown_instances": false, + "source_path": "a/foo.txt", + "project_definition": null + }, + "b1298bcc-e370-44a6-9ef4-fbefa290124c": { + "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 + } + ] + } + } +} \ No newline at end of file