New snapshot tests

This commit is contained in:
Lucien Greathouse 2019-03-12 15:58:22 -07:00
parent ad93631ef8
commit 684a33c418
12 changed files with 921 additions and 90 deletions

View file

@ -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 [<snapshot_ $name>]() {
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<Option<RbxSnapshotInstance<'static>>> {
let contents = fs::read(path.join(SNAPSHOT_EXPECTED_NAME)).ok()?;
let snapshot: Option<RbxSnapshotInstance<'static>> = serde_json::from_slice(&contents)
.expect("Could not deserialize snapshot");
Some(snapshot)
}
fn write_expected_snapshot(path: &Path, snapshot: &Option<RbxSnapshotInstance>) {
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");
}

View file

@ -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(())
}
}

View file

@ -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<RbxTree> {
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<Option<RbxSnapshotInstance<'static>>> {
let contents = fs::read(path.join(SNAPSHOT_EXPECTED_NAME)).ok()?;
let snapshot: Option<RbxSnapshotInstance<'static>> = serde_json::from_slice(&contents)
.expect("Could not deserialize snapshot");
Some(snapshot)
}
pub fn write_expected_snapshot(path: &Path, snapshot: &Option<RbxSnapshotInstance>) {
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");
}

View file

@ -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<Cow<'a, str>>, B: Into<Cow<'a, str>>>(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(())
}

View file

@ -0,0 +1 @@
Hello world, from a/foo.txt

View file

@ -0,0 +1 @@
-- hello, from a/main.lua

View file

@ -0,0 +1 @@
-- b/something.lua

View file

@ -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
}
}
}
}

View file

@ -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
}
]
}
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}