forked from rojo-rbx/rojo
Support nested partitions and partitions directly targeting services (#122)
* Do the nested partition thing * Tidy up touched code * Add nested partition test project, not fully functional * Clean up variable names, move path_metadata mutation strictly into snapshot_reconciler * Remove path_metadata, snapshotting is now pure * Factor out snapshot metadata storage to fix a missing case * Pull instance_name out of per_path_metadata, closer to what we need * Refactor to make metadata make more sense, part one * All appears to be well * Cull 'metadata_per_path' in favor of 'instances_per_path' * Remove SnapshotContext * InstanceMetadata -> PublicInstanceMetadata in web module * Build in snapshot testing system for testing... snapshots? * Remove pretty_assertions to see if it fixes a snapshot comparison bug * Reintroduce pretty assertions, it's not the cause of inequality * Fix snapshot tests with custom relative path serializer
This commit is contained in:
committed by
GitHub
parent
38e3c198f2
commit
ecb9b5e28f
51
Cargo.lock
generated
51
Cargo.lock
generated
@@ -330,6 +330,11 @@ dependencies = [
|
|||||||
"gzip-header 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"gzip-header 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "difference"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dtoa"
|
name = "dtoa"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -932,6 +937,26 @@ dependencies = [
|
|||||||
"winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paste"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"paste-impl 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"proc-macro-hack 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paste-impl"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-hack 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@@ -977,6 +1002,25 @@ name = "pkg-config"
|
|||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pretty_assertions"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-hack"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "0.4.26"
|
version = "0.4.26"
|
||||||
@@ -1258,6 +1302,8 @@ dependencies = [
|
|||||||
"log 0.4.6 (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)",
|
"maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"notify 4.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
"notify 4.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"paste 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"pretty_assertions 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rand 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rand 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rbx_binary 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rbx_binary 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rbx_tree 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rbx_tree 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@@ -1927,6 +1973,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
"checksum csv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "9fd1c44c58078cfbeaf11fbb3eac9ae5534c23004ed770cc4bfb48e658ae4f04"
|
"checksum csv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "9fd1c44c58078cfbeaf11fbb3eac9ae5534c23004ed770cc4bfb48e658ae4f04"
|
||||||
"checksum csv-core 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fa5cdef62f37e6ffe7d1f07a381bc0db32b7a3ff1cac0de56cb0d81e71f53d65"
|
"checksum csv-core 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fa5cdef62f37e6ffe7d1f07a381bc0db32b7a3ff1cac0de56cb0d81e71f53d65"
|
||||||
"checksum deflate 0.7.19 (registry+https://github.com/rust-lang/crates.io-index)" = "8a6abb26e16e8d419b5c78662aa9f82857c2386a073da266840e474d5055ec86"
|
"checksum deflate 0.7.19 (registry+https://github.com/rust-lang/crates.io-index)" = "8a6abb26e16e8d419b5c78662aa9f82857c2386a073da266840e474d5055ec86"
|
||||||
|
"checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
|
||||||
"checksum dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6d301140eb411af13d3115f9a562c85cc6b541ade9dfa314132244aaee7489dd"
|
"checksum dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6d301140eb411af13d3115f9a562c85cc6b541ade9dfa314132244aaee7489dd"
|
||||||
"checksum encoding_rs 0.8.14 (registry+https://github.com/rust-lang/crates.io-index)" = "a69d152eaa438a291636c1971b0a370212165ca8a75759eb66818c5ce9b538f7"
|
"checksum encoding_rs 0.8.14 (registry+https://github.com/rust-lang/crates.io-index)" = "a69d152eaa438a291636c1971b0a370212165ca8a75759eb66818c5ce9b538f7"
|
||||||
"checksum env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "afb070faf94c85d17d50ca44f6ad076bce18ae92f0037d350947240a36e9d42e"
|
"checksum env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "afb070faf94c85d17d50ca44f6ad076bce18ae92f0037d350947240a36e9d42e"
|
||||||
@@ -1993,12 +2040,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
"checksum owning_ref 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "49a4b8ea2179e6a2e27411d3bca09ca6dd630821cf6894c6c7c8467a8ee7ef13"
|
"checksum owning_ref 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "49a4b8ea2179e6a2e27411d3bca09ca6dd630821cf6894c6c7c8467a8ee7ef13"
|
||||||
"checksum parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ab41b4aed082705d1056416ae4468b6ea99d52599ecf3169b00088d43113e337"
|
"checksum parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ab41b4aed082705d1056416ae4468b6ea99d52599ecf3169b00088d43113e337"
|
||||||
"checksum parking_lot_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9"
|
"checksum parking_lot_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9"
|
||||||
|
"checksum paste 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f50392d1265092fbee9273414cc40eb6d47d307bd66222c477bb8450c8504f9d"
|
||||||
|
"checksum paste-impl 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a3cd512fe3a55e8933b2dcad913e365639db86d512e4004c3084b86864d9467a"
|
||||||
"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
|
"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
|
||||||
"checksum phf 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18"
|
"checksum phf 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18"
|
||||||
"checksum phf_codegen 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e"
|
"checksum phf_codegen 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e"
|
||||||
"checksum phf_generator 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662"
|
"checksum phf_generator 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662"
|
||||||
"checksum phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0"
|
"checksum phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0"
|
||||||
"checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c"
|
"checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c"
|
||||||
|
"checksum pretty_assertions 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a029430f0d744bc3d15dd474d591bed2402b645d024583082b9f63bb936dac6"
|
||||||
|
"checksum proc-macro-hack 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3e90aa19cd73dedc2d0e1e8407473f073d735fef0ab521438de6da8ee449ab66"
|
||||||
"checksum proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)" = "38fddd23d98b2144d197c0eca5705632d4fe2667d14a6be5df8934f8d74f1978"
|
"checksum proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)" = "38fddd23d98b2144d197c0eca5705632d4fe2667d14a6be5df8934f8d74f1978"
|
||||||
"checksum pulldown-cmark 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "eef52fac62d0ea7b9b4dc7da092aa64ea7ec3d90af6679422d3d7e0e14b6ee15"
|
"checksum pulldown-cmark 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "eef52fac62d0ea7b9b4dc7da092aa64ea7ec3d90af6679422d3d7e0e14b6ee15"
|
||||||
"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0"
|
"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0"
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ log = "0.4"
|
|||||||
maplit = "1.0.1"
|
maplit = "1.0.1"
|
||||||
notify = "4.0"
|
notify = "4.0"
|
||||||
rand = "0.4"
|
rand = "0.4"
|
||||||
|
rbx_binary = "0.2.0"
|
||||||
|
rbx_tree = "0.2.0"
|
||||||
|
rbx_xml = "0.2.0"
|
||||||
regex = "1.0"
|
regex = "1.0"
|
||||||
reqwest = "0.9.5"
|
reqwest = "0.9.5"
|
||||||
rouille = "2.1"
|
rouille = "2.1"
|
||||||
@@ -35,11 +38,10 @@ serde = "1.0"
|
|||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
uuid = { version = "0.7", features = ["v4", "serde"] }
|
uuid = { version = "0.7", features = ["v4", "serde"] }
|
||||||
rbx_tree = "0.2.0"
|
|
||||||
rbx_xml = "0.2.0"
|
|
||||||
rbx_binary = "0.2.0"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.0"
|
tempfile = "3.0"
|
||||||
walkdir = "2.1"
|
walkdir = "2.1"
|
||||||
lazy_static = "1.2"
|
lazy_static = "1.2"
|
||||||
|
pretty_assertions = "0.5.1"
|
||||||
|
paste = "0.1"
|
||||||
@@ -44,13 +44,14 @@ impl FsWatcher {
|
|||||||
let imfs = imfs.lock().unwrap();
|
let imfs = imfs.lock().unwrap();
|
||||||
|
|
||||||
for root_path in imfs.get_roots() {
|
for root_path in imfs.get_roots() {
|
||||||
|
trace!("Watching path {}", root_path.display());
|
||||||
watcher.watch(root_path, RecursiveMode::Recursive)
|
watcher.watch(root_path, RecursiveMode::Recursive)
|
||||||
.expect("Could not watch directory");
|
.expect("Could not watch directory");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let imfs = Arc::clone(&imfs);
|
let imfs = Arc::clone(&imfs);
|
||||||
let rbx_session = rbx_session.as_ref().map(Arc::clone);
|
let rbx_session = rbx_session.as_ref().map(Arc::clone);
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
|
|||||||
@@ -35,16 +35,13 @@ impl fmt::Display for FsError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_sync_points(imfs: &mut Imfs, project_node: &ProjectNode) -> Result<(), FsError> {
|
fn add_sync_points(imfs: &mut Imfs, node: &ProjectNode) -> Result<(), FsError> {
|
||||||
match project_node {
|
if let Some(path) = &node.path {
|
||||||
ProjectNode::Instance(node) => {
|
imfs.add_root(path)?;
|
||||||
for child in node.children.values() {
|
}
|
||||||
add_sync_points(imfs, child)?;
|
|
||||||
}
|
for child in node.children.values() {
|
||||||
},
|
add_sync_points(imfs, child)?;
|
||||||
ProjectNode::SyncPoint(node) => {
|
|
||||||
imfs.add_root(&node.path)?;
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ pub mod impl_from;
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod fs_watcher;
|
pub mod fs_watcher;
|
||||||
pub mod imfs;
|
pub mod imfs;
|
||||||
|
pub mod live_session;
|
||||||
pub mod message_queue;
|
pub mod message_queue;
|
||||||
pub mod path_map;
|
pub mod path_map;
|
||||||
|
pub mod path_serializer;
|
||||||
pub mod project;
|
pub mod project;
|
||||||
pub mod rbx_session;
|
pub mod rbx_session;
|
||||||
pub mod rbx_snapshot;
|
pub mod rbx_snapshot;
|
||||||
pub mod live_session;
|
|
||||||
pub mod session_id;
|
pub mod session_id;
|
||||||
pub mod snapshot_reconciler;
|
pub mod snapshot_reconciler;
|
||||||
pub mod visualize;
|
pub mod visualize;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::hash_map,
|
|
||||||
path::{self, Path, PathBuf},
|
path::{self, Path, PathBuf},
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
};
|
};
|
||||||
@@ -36,12 +35,6 @@ impl<T> PathMap<T> {
|
|||||||
self.nodes.get_mut(path).map(|v| &mut v.value)
|
self.nodes.get_mut(path).map(|v| &mut v.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn entry<'a>(&'a mut self, path: PathBuf) -> Entry<'a, T> {
|
|
||||||
Entry {
|
|
||||||
internal: self.nodes.entry(path),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert(&mut self, path: PathBuf, value: T) {
|
pub fn insert(&mut self, path: PathBuf, value: T) {
|
||||||
if let Some(parent_path) = path.parent() {
|
if let Some(parent_path) = path.parent() {
|
||||||
if let Some(parent) = self.nodes.get_mut(parent_path) {
|
if let Some(parent) = self.nodes.get_mut(parent_path) {
|
||||||
@@ -116,28 +109,4 @@ impl<T> PathMap<T> {
|
|||||||
|
|
||||||
current_path
|
current_path
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Entry<'a, T> {
|
|
||||||
internal: hash_map::Entry<'a, PathBuf, PathMapNode<T>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T> Entry<'a, T> {
|
|
||||||
pub fn or_insert(self, value: T) -> &'a mut T {
|
|
||||||
&mut self.internal.or_insert(PathMapNode {
|
|
||||||
value,
|
|
||||||
children: HashSet::new(),
|
|
||||||
}).value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T> Entry<'a, T>
|
|
||||||
where T: Default
|
|
||||||
{
|
|
||||||
pub fn or_default(self) -> &'a mut T {
|
|
||||||
&mut self.internal.or_insert(PathMapNode {
|
|
||||||
value: Default::default(),
|
|
||||||
children: HashSet::new(),
|
|
||||||
}).value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
69
server/src/path_serializer.rs
Normal file
69
server/src/path_serializer.rs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
//! path_serializer is used in cases where we need to serialize relative Path
|
||||||
|
//! and PathBuf objects in a way that's cross-platform.
|
||||||
|
//!
|
||||||
|
//! This is used for the snapshot testing system to make sure that snapshots
|
||||||
|
//! that reference local paths that are generated on Windows don't fail when run
|
||||||
|
//! in systems that use a different directory separator.
|
||||||
|
//!
|
||||||
|
//! To use, annotate your PathBuf or Option<PathBuf> field with the correct
|
||||||
|
//! serializer function:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! # use std::path::PathBuf;
|
||||||
|
//! # use serde_derive::{Serialize, Deserialize};
|
||||||
|
//!
|
||||||
|
//! #[derive(Serialize, Deserialize)]
|
||||||
|
//! struct Mine {
|
||||||
|
//! name: String,
|
||||||
|
//!
|
||||||
|
//! // Use 'crate' instead of librojo if writing code inside Rojo
|
||||||
|
//! #[serde(serialize_with = "librojo::path_serializer::serialize")]
|
||||||
|
//! source_path: PathBuf,
|
||||||
|
//!
|
||||||
|
//! #[serde(serialize_with = "librojo::path_serializer::serialize_option")]
|
||||||
|
//! maybe_path: Option<PathBuf>,
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! **The methods in this module can only handle relative paths, since absolute
|
||||||
|
//! paths are never portable.**
|
||||||
|
|
||||||
|
use std::path::{Component, Path};
|
||||||
|
|
||||||
|
use serde::Serializer;
|
||||||
|
|
||||||
|
pub fn serialize_option<S, T>(maybe_path: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where S: Serializer,
|
||||||
|
T: AsRef<Path>,
|
||||||
|
{
|
||||||
|
match maybe_path {
|
||||||
|
Some(path) => serialize(path, serializer),
|
||||||
|
None => serializer.serialize_none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize<S, T>(path: T, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where S: Serializer,
|
||||||
|
T: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
assert!(path.is_relative(), "path_serializer can only handle relative paths");
|
||||||
|
|
||||||
|
let mut output = String::new();
|
||||||
|
|
||||||
|
for component in path.components() {
|
||||||
|
if !output.is_empty() {
|
||||||
|
output.push('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
match component {
|
||||||
|
Component::CurDir => output.push('.'),
|
||||||
|
Component::ParentDir => output.push_str(".."),
|
||||||
|
Component::Normal(piece) => output.push_str(piece.to_str().unwrap()),
|
||||||
|
_ => panic!("path_serializer cannot handle absolute path components"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serializer.serialize_str(&output)
|
||||||
|
}
|
||||||
@@ -15,16 +15,6 @@ use serde_derive::{Serialize, Deserialize};
|
|||||||
pub static PROJECT_FILENAME: &'static str = "default.project.json";
|
pub static PROJECT_FILENAME: &'static str = "default.project.json";
|
||||||
pub static COMPAT_PROJECT_FILENAME: &'static str = "roblox-project.json";
|
pub static COMPAT_PROJECT_FILENAME: &'static str = "roblox-project.json";
|
||||||
|
|
||||||
// Methods used for Serde's default value system, which doesn't support using
|
|
||||||
// value literals directly, only functions that return values.
|
|
||||||
const fn yeah() -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn is_true(value: &bool) -> bool {
|
|
||||||
*value
|
|
||||||
}
|
|
||||||
|
|
||||||
/// SourceProject is the format that users author projects on-disk. Since we
|
/// SourceProject is the format that users author projects on-disk. Since we
|
||||||
/// want to do things like transforming paths to be absolute before handing them
|
/// want to do things like transforming paths to be absolute before handing them
|
||||||
/// off to the rest of Rojo, we use this intermediate struct.
|
/// off to the rest of Rojo, we use this intermediate struct.
|
||||||
@@ -60,59 +50,47 @@ impl SourceProject {
|
|||||||
/// slightly different on-disk than how we want to handle them in the rest of
|
/// slightly different on-disk than how we want to handle them in the rest of
|
||||||
/// Rojo.
|
/// Rojo.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(untagged)]
|
struct SourceProjectNode {
|
||||||
enum SourceProjectNode {
|
#[serde(rename = "$className", skip_serializing_if = "Option::is_none")]
|
||||||
Instance {
|
class_name: Option<String>,
|
||||||
#[serde(rename = "$className")]
|
|
||||||
class_name: String,
|
|
||||||
|
|
||||||
#[serde(rename = "$properties", default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
|
#[serde(rename = "$properties", default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
|
||||||
properties: HashMap<String, RbxValue>,
|
properties: HashMap<String, RbxValue>,
|
||||||
|
|
||||||
#[serde(rename = "$ignoreUnknownInstances", default = "yeah", skip_serializing_if = "is_true")]
|
#[serde(rename = "$ignoreUnknownInstances", skip_serializing_if = "Option::is_none")]
|
||||||
ignore_unknown_instances: bool,
|
ignore_unknown_instances: Option<bool>,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(rename = "$path", skip_serializing_if = "Option::is_none")]
|
||||||
children: HashMap<String, SourceProjectNode>,
|
path: Option<String>,
|
||||||
},
|
|
||||||
SyncPoint {
|
#[serde(flatten)]
|
||||||
#[serde(rename = "$path")]
|
children: HashMap<String, SourceProjectNode>,
|
||||||
path: String,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SourceProjectNode {
|
impl SourceProjectNode {
|
||||||
/// Consumes the SourceProjectNode and turns it into a ProjectNode.
|
/// Consumes the SourceProjectNode and turns it into a ProjectNode.
|
||||||
pub fn into_project_node(self, project_file_location: &Path) -> ProjectNode {
|
pub fn into_project_node(mut self, project_file_location: &Path) -> ProjectNode {
|
||||||
match self {
|
let children = self.children.drain()
|
||||||
SourceProjectNode::Instance { class_name, mut children, properties, ignore_unknown_instances } => {
|
.map(|(key, value)| (key, value.into_project_node(project_file_location)))
|
||||||
let mut new_children = HashMap::new();
|
.collect();
|
||||||
|
|
||||||
for (node_name, node) in children.drain() {
|
// Make sure that paths are absolute, transforming them by adding the
|
||||||
new_children.insert(node_name, node.into_project_node(project_file_location));
|
// project folder if they're not already absolute.
|
||||||
}
|
let path = self.path.as_ref().map(|source_path| {
|
||||||
|
if Path::new(source_path).is_absolute() {
|
||||||
|
PathBuf::from(source_path)
|
||||||
|
} else {
|
||||||
|
let project_folder_location = project_file_location.parent().unwrap();
|
||||||
|
project_folder_location.join(source_path)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ProjectNode::Instance(InstanceProjectNode {
|
ProjectNode {
|
||||||
class_name,
|
class_name: self.class_name,
|
||||||
children: new_children,
|
properties: self.properties,
|
||||||
properties,
|
ignore_unknown_instances: self.ignore_unknown_instances,
|
||||||
metadata: InstanceProjectNodeMetadata {
|
path,
|
||||||
ignore_unknown_instances,
|
children,
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
SourceProjectNode::SyncPoint { path: source_path } => {
|
|
||||||
let path = if Path::new(&source_path).is_absolute() {
|
|
||||||
PathBuf::from(source_path)
|
|
||||||
} else {
|
|
||||||
let project_folder_location = project_file_location.parent().unwrap();
|
|
||||||
project_folder_location.join(source_path)
|
|
||||||
};
|
|
||||||
|
|
||||||
ProjectNode::SyncPoint(SyncPointProjectNode {
|
|
||||||
path,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,75 +155,49 @@ pub enum ProjectSaveError {
|
|||||||
IoError(#[fail(cause)] io::Error),
|
IoError(#[fail(cause)] io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
pub struct ProjectNode {
|
||||||
pub struct InstanceProjectNodeMetadata {
|
pub class_name: Option<String>,
|
||||||
pub ignore_unknown_instances: bool,
|
pub children: HashMap<String, ProjectNode>,
|
||||||
}
|
pub properties: HashMap<String, RbxValue>,
|
||||||
|
pub ignore_unknown_instances: Option<bool>,
|
||||||
|
|
||||||
impl Default for InstanceProjectNodeMetadata {
|
#[serde(serialize_with = "crate::path_serializer::serialize_option")]
|
||||||
fn default() -> InstanceProjectNodeMetadata {
|
pub path: Option<PathBuf>,
|
||||||
InstanceProjectNodeMetadata {
|
|
||||||
ignore_unknown_instances: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum ProjectNode {
|
|
||||||
Instance(InstanceProjectNode),
|
|
||||||
SyncPoint(SyncPointProjectNode),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProjectNode {
|
impl ProjectNode {
|
||||||
fn to_source_node(&self, project_file_location: &Path) -> SourceProjectNode {
|
fn to_source_node(&self, project_file_location: &Path) -> SourceProjectNode {
|
||||||
match self {
|
let children = self.children.iter()
|
||||||
ProjectNode::Instance(node) => {
|
.map(|(key, value)| (key.clone(), value.to_source_node(project_file_location)))
|
||||||
let mut children = HashMap::new();
|
.collect();
|
||||||
|
|
||||||
for (key, child) in &node.children {
|
// If paths are relative to the project file, transform them to look
|
||||||
children.insert(key.clone(), child.to_source_node(project_file_location));
|
// Unixy and write relative paths instead.
|
||||||
}
|
//
|
||||||
|
// This isn't perfect, since it means that paths like .. will stay as
|
||||||
|
// absolute paths and make projects non-portable. Fixing this probably
|
||||||
|
// means keeping the paths relative in the project format and making
|
||||||
|
// everywhere else in Rojo do the resolution locally.
|
||||||
|
let path = self.path.as_ref().map(|path| {
|
||||||
|
let project_folder_location = project_file_location.parent().unwrap();
|
||||||
|
|
||||||
SourceProjectNode::Instance {
|
match path.strip_prefix(project_folder_location) {
|
||||||
class_name: node.class_name.clone(),
|
Ok(stripped) => stripped.to_str().unwrap().replace("\\", "/"),
|
||||||
children,
|
Err(_) => format!("{}", path.display()),
|
||||||
properties: node.properties.clone(),
|
}
|
||||||
ignore_unknown_instances: node.metadata.ignore_unknown_instances,
|
});
|
||||||
}
|
|
||||||
},
|
|
||||||
ProjectNode::SyncPoint(sync_node) => {
|
|
||||||
let project_folder_location = project_file_location.parent().unwrap();
|
|
||||||
|
|
||||||
let friendly_path = match sync_node.path.strip_prefix(project_folder_location) {
|
SourceProjectNode {
|
||||||
Ok(stripped) => stripped.to_str().unwrap().replace("\\", "/"),
|
class_name: self.class_name.clone(),
|
||||||
Err(_) => format!("{}", sync_node.path.display()),
|
properties: self.properties.clone(),
|
||||||
};
|
ignore_unknown_instances: self.ignore_unknown_instances,
|
||||||
|
children,
|
||||||
SourceProjectNode::SyncPoint {
|
path,
|
||||||
path: friendly_path,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct InstanceProjectNode {
|
|
||||||
pub class_name: String,
|
|
||||||
pub children: HashMap<String, ProjectNode>,
|
|
||||||
pub properties: HashMap<String, RbxValue>,
|
|
||||||
pub metadata: InstanceProjectNodeMetadata,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct SyncPointProjectNode {
|
|
||||||
pub path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -265,33 +217,31 @@ impl Project {
|
|||||||
project_fuzzy_path.file_name().unwrap().to_str().unwrap()
|
project_fuzzy_path.file_name().unwrap().to_str().unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
let tree = ProjectNode::Instance(InstanceProjectNode {
|
let tree = ProjectNode {
|
||||||
class_name: "DataModel".to_string(),
|
class_name: Some(String::from("DataModel")),
|
||||||
children: hashmap! {
|
children: hashmap! {
|
||||||
String::from("ReplicatedStorage") => ProjectNode::Instance(InstanceProjectNode {
|
String::from("ReplicatedStorage") => ProjectNode {
|
||||||
class_name: String::from("ReplicatedStorage"),
|
class_name: Some(String::from("ReplicatedStorage")),
|
||||||
children: hashmap! {
|
children: hashmap! {
|
||||||
String::from("Source") => ProjectNode::SyncPoint(SyncPointProjectNode {
|
String::from("Source") => ProjectNode {
|
||||||
path: project_folder_path.join("src"),
|
path: Some(project_folder_path.join("src")),
|
||||||
}),
|
..Default::default()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
properties: HashMap::new(),
|
..Default::default()
|
||||||
metadata: Default::default(),
|
},
|
||||||
}),
|
String::from("HttpService") => ProjectNode {
|
||||||
String::from("HttpService") => ProjectNode::Instance(InstanceProjectNode {
|
class_name: Some(String::from("HttpService")),
|
||||||
class_name: String::from("HttpService"),
|
|
||||||
children: HashMap::new(),
|
|
||||||
properties: hashmap! {
|
properties: hashmap! {
|
||||||
String::from("HttpEnabled") => RbxValue::Bool {
|
String::from("HttpEnabled") => RbxValue::Bool {
|
||||||
value: true,
|
value: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
metadata: Default::default(),
|
..Default::default()
|
||||||
}),
|
},
|
||||||
},
|
},
|
||||||
properties: HashMap::new(),
|
..Default::default()
|
||||||
metadata: Default::default(),
|
};
|
||||||
});
|
|
||||||
|
|
||||||
let project = Project {
|
let project = Project {
|
||||||
name: project_name.to_string(),
|
name: project_name.to_string(),
|
||||||
@@ -316,9 +266,10 @@ impl Project {
|
|||||||
project_fuzzy_path.file_name().unwrap().to_str().unwrap()
|
project_fuzzy_path.file_name().unwrap().to_str().unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
let tree = ProjectNode::SyncPoint(SyncPointProjectNode {
|
let tree = ProjectNode {
|
||||||
path: project_folder_path.join("src"),
|
path: Some(project_folder_path.join("src")),
|
||||||
});
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
let project = Project {
|
let project = Project {
|
||||||
name: project_name.to_string(),
|
name: project_name.to_string(),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
collections::HashMap,
|
collections::{HashSet, HashMap},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
str,
|
str,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
@@ -11,11 +11,11 @@ use log::{info, trace};
|
|||||||
use rbx_tree::{RbxTree, RbxId};
|
use rbx_tree::{RbxTree, RbxId};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
project::Project,
|
project::{Project, ProjectNode},
|
||||||
message_queue::MessageQueue,
|
message_queue::MessageQueue,
|
||||||
imfs::{Imfs, ImfsItem},
|
imfs::{Imfs, ImfsItem},
|
||||||
path_map::PathMap,
|
path_map::PathMap,
|
||||||
rbx_snapshot::{SnapshotContext, snapshot_project_tree, snapshot_imfs_path},
|
rbx_snapshot::{snapshot_project_tree, snapshot_project_node, snapshot_imfs_path},
|
||||||
snapshot_reconciler::{InstanceChanges, reify_root, reconcile_subtree},
|
snapshot_reconciler::{InstanceChanges, reify_root, reconcile_subtree},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,24 +23,28 @@ const INIT_SCRIPT: &str = "init.lua";
|
|||||||
const INIT_SERVER_SCRIPT: &str = "init.server.lua";
|
const INIT_SERVER_SCRIPT: &str = "init.server.lua";
|
||||||
const INIT_CLIENT_SCRIPT: &str = "init.client.lua";
|
const INIT_CLIENT_SCRIPT: &str = "init.client.lua";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
/// `source_path` or `project_definition` or both must both be Some.
|
||||||
pub struct MetadataPerPath {
|
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||||
pub instance_id: Option<RbxId>,
|
|
||||||
pub instance_name: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
||||||
pub struct MetadataPerInstance {
|
pub struct MetadataPerInstance {
|
||||||
pub source_path: Option<PathBuf>,
|
|
||||||
pub ignore_unknown_instances: bool,
|
pub ignore_unknown_instances: bool,
|
||||||
|
|
||||||
|
/// The path on the filesystem that the instance was read from the
|
||||||
|
/// filesystem if it came from the filesystem.
|
||||||
|
#[serde(serialize_with = "crate::path_serializer::serialize_option")]
|
||||||
|
pub source_path: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Information about the instance that came from the project that defined
|
||||||
|
/// it, if that's where it was defined.
|
||||||
|
///
|
||||||
|
/// A key-value pair where the key should be the name of the instance and
|
||||||
|
/// the value is the ProjectNode from the instance's project.
|
||||||
|
pub project_definition: Option<(String, ProjectNode)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RbxSession {
|
pub struct RbxSession {
|
||||||
tree: RbxTree,
|
tree: RbxTree,
|
||||||
|
|
||||||
// TODO(#105): Change metadata_per_path to PathMap<Vec<MetadataPerPath>> for
|
instances_per_path: PathMap<HashSet<RbxId>>,
|
||||||
// path aliasing.
|
|
||||||
metadata_per_path: PathMap<MetadataPerPath>,
|
|
||||||
metadata_per_instance: HashMap<RbxId, MetadataPerInstance>,
|
metadata_per_instance: HashMap<RbxId, MetadataPerInstance>,
|
||||||
message_queue: Arc<MessageQueue<InstanceChanges>>,
|
message_queue: Arc<MessageQueue<InstanceChanges>>,
|
||||||
imfs: Arc<Mutex<Imfs>>,
|
imfs: Arc<Mutex<Imfs>>,
|
||||||
@@ -52,17 +56,17 @@ impl RbxSession {
|
|||||||
imfs: Arc<Mutex<Imfs>>,
|
imfs: Arc<Mutex<Imfs>>,
|
||||||
message_queue: Arc<MessageQueue<InstanceChanges>>,
|
message_queue: Arc<MessageQueue<InstanceChanges>>,
|
||||||
) -> RbxSession {
|
) -> RbxSession {
|
||||||
let mut metadata_per_path = PathMap::new();
|
let mut instances_per_path = PathMap::new();
|
||||||
let mut metadata_per_instance = HashMap::new();
|
let mut metadata_per_instance = HashMap::new();
|
||||||
|
|
||||||
let tree = {
|
let tree = {
|
||||||
let temp_imfs = imfs.lock().unwrap();
|
let temp_imfs = imfs.lock().unwrap();
|
||||||
reify_initial_tree(&project, &temp_imfs, &mut metadata_per_path, &mut metadata_per_instance)
|
reify_initial_tree(&project, &temp_imfs, &mut instances_per_path, &mut metadata_per_instance)
|
||||||
};
|
};
|
||||||
|
|
||||||
RbxSession {
|
RbxSession {
|
||||||
tree,
|
tree,
|
||||||
metadata_per_path,
|
instances_per_path,
|
||||||
metadata_per_instance,
|
metadata_per_instance,
|
||||||
message_queue,
|
message_queue,
|
||||||
imfs,
|
imfs,
|
||||||
@@ -80,7 +84,7 @@ impl RbxSession {
|
|||||||
.expect("Path was outside in-memory filesystem roots");
|
.expect("Path was outside in-memory filesystem roots");
|
||||||
|
|
||||||
// Find the closest instance in the tree that currently exists
|
// Find the closest instance in the tree that currently exists
|
||||||
let mut path_to_snapshot = self.metadata_per_path.descend(root_path, path);
|
let mut path_to_snapshot = self.instances_per_path.descend(root_path, path);
|
||||||
|
|
||||||
// If this is a file that might affect its parent if modified, we
|
// If this is a file that might affect its parent if modified, we
|
||||||
// should snapshot its parent instead.
|
// should snapshot its parent instead.
|
||||||
@@ -93,42 +97,44 @@ impl RbxSession {
|
|||||||
|
|
||||||
trace!("Snapshotting path {}", path_to_snapshot.display());
|
trace!("Snapshotting path {}", path_to_snapshot.display());
|
||||||
|
|
||||||
let path_metadata = self.metadata_per_path.get(&path_to_snapshot).unwrap();
|
let instances_at_path = self.instances_per_path.get(&path_to_snapshot)
|
||||||
|
.expect("Metadata did not exist for path")
|
||||||
|
.clone();
|
||||||
|
|
||||||
trace!("Metadata for path: {:?}", path_metadata);
|
for instance_id in &instances_at_path {
|
||||||
|
let instance_metadata = self.metadata_per_instance.get(&instance_id)
|
||||||
|
.expect("Metadata for instance ID did not exist");
|
||||||
|
|
||||||
let instance_id = path_metadata.instance_id
|
let maybe_snapshot = match &instance_metadata.project_definition {
|
||||||
.expect("Instance did not exist in tree");
|
Some((instance_name, project_node)) => {
|
||||||
|
snapshot_project_node(&imfs, &project_node, Cow::Owned(instance_name.clone()))
|
||||||
|
.unwrap_or_else(|_| panic!("Could not generate instance snapshot for path {}", path_to_snapshot.display()))
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
snapshot_imfs_path(&imfs, &path_to_snapshot, None)
|
||||||
|
.unwrap_or_else(|_| panic!("Could not generate instance snapshot for path {}", path_to_snapshot.display()))
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// If this instance is a sync point, pull its name out of our
|
let snapshot = match maybe_snapshot {
|
||||||
// per-path metadata store.
|
Some(snapshot) => snapshot,
|
||||||
let instance_name = path_metadata.instance_name.as_ref()
|
None => {
|
||||||
.map(|value| Cow::Owned(value.to_owned()));
|
trace!("Path resulted in no snapshot being generated.");
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
let mut context = SnapshotContext {
|
trace!("Snapshot: {:#?}", snapshot);
|
||||||
metadata_per_path: &mut self.metadata_per_path,
|
|
||||||
};
|
|
||||||
let maybe_snapshot = snapshot_imfs_path(&imfs, &mut context, &path_to_snapshot, instance_name)
|
|
||||||
.unwrap_or_else(|_| panic!("Could not generate instance snapshot for path {}", path_to_snapshot.display()));
|
|
||||||
|
|
||||||
let snapshot = match maybe_snapshot {
|
reconcile_subtree(
|
||||||
Some(snapshot) => snapshot,
|
&mut self.tree,
|
||||||
None => {
|
*instance_id,
|
||||||
trace!("Path resulted in no snapshot being generated.");
|
&snapshot,
|
||||||
return;
|
&mut self.instances_per_path,
|
||||||
},
|
&mut self.metadata_per_instance,
|
||||||
};
|
&mut changes,
|
||||||
|
);
|
||||||
trace!("Snapshot: {:#?}", snapshot);
|
}
|
||||||
|
|
||||||
reconcile_subtree(
|
|
||||||
&mut self.tree,
|
|
||||||
instance_id,
|
|
||||||
&snapshot,
|
|
||||||
&mut self.metadata_per_path,
|
|
||||||
&mut self.metadata_per_instance,
|
|
||||||
&mut changes,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if changes.is_empty() {
|
if changes.is_empty() {
|
||||||
@@ -170,13 +176,13 @@ impl RbxSession {
|
|||||||
|
|
||||||
pub fn path_removed(&mut self, path: &Path) {
|
pub fn path_removed(&mut self, path: &Path) {
|
||||||
info!("Path removed: {}", path.display());
|
info!("Path removed: {}", path.display());
|
||||||
self.metadata_per_path.remove(path);
|
self.instances_per_path.remove(path);
|
||||||
self.path_created_or_updated(path);
|
self.path_created_or_updated(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path_renamed(&mut self, from_path: &Path, to_path: &Path) {
|
pub fn path_renamed(&mut self, from_path: &Path, to_path: &Path) {
|
||||||
info!("Path renamed from {} to {}", from_path.display(), to_path.display());
|
info!("Path renamed from {} to {}", from_path.display(), to_path.display());
|
||||||
self.metadata_per_path.remove(from_path);
|
self.instances_per_path.remove(from_path);
|
||||||
self.path_created_or_updated(from_path);
|
self.path_created_or_updated(from_path);
|
||||||
self.path_created_or_updated(to_path);
|
self.path_created_or_updated(to_path);
|
||||||
}
|
}
|
||||||
@@ -188,33 +194,26 @@ impl RbxSession {
|
|||||||
pub fn get_instance_metadata(&self, id: RbxId) -> Option<&MetadataPerInstance> {
|
pub fn get_instance_metadata(&self, id: RbxId) -> Option<&MetadataPerInstance> {
|
||||||
self.metadata_per_instance.get(&id)
|
self.metadata_per_instance.get(&id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn debug_get_metadata_per_path(&self) -> &PathMap<MetadataPerPath> {
|
|
||||||
&self.metadata_per_path
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn construct_oneoff_tree(project: &Project, imfs: &Imfs) -> RbxTree {
|
pub fn construct_oneoff_tree(project: &Project, imfs: &Imfs) -> RbxTree {
|
||||||
let mut metadata_per_path = PathMap::new();
|
let mut instances_per_path = PathMap::new();
|
||||||
let mut metadata_per_instance = HashMap::new();
|
let mut metadata_per_instance = HashMap::new();
|
||||||
reify_initial_tree(project, imfs, &mut metadata_per_path, &mut metadata_per_instance)
|
reify_initial_tree(project, imfs, &mut instances_per_path, &mut metadata_per_instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reify_initial_tree(
|
fn reify_initial_tree(
|
||||||
project: &Project,
|
project: &Project,
|
||||||
imfs: &Imfs,
|
imfs: &Imfs,
|
||||||
metadata_per_path: &mut PathMap<MetadataPerPath>,
|
instances_per_path: &mut PathMap<HashSet<RbxId>>,
|
||||||
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
|
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
|
||||||
) -> RbxTree {
|
) -> RbxTree {
|
||||||
let mut context = SnapshotContext {
|
let snapshot = snapshot_project_tree(imfs, project)
|
||||||
metadata_per_path,
|
|
||||||
};
|
|
||||||
let snapshot = snapshot_project_tree(imfs, &mut context, project)
|
|
||||||
.expect("Could not snapshot project tree")
|
.expect("Could not snapshot project tree")
|
||||||
.expect("Project did not produce any instances");
|
.expect("Project did not produce any instances");
|
||||||
|
|
||||||
let mut changes = InstanceChanges::default();
|
let mut changes = InstanceChanges::default();
|
||||||
let tree = reify_root(&snapshot, metadata_per_path, metadata_per_instance, &mut changes);
|
let tree = reify_root(&snapshot, instances_per_path, metadata_per_instance, &mut changes);
|
||||||
|
|
||||||
tree
|
tree
|
||||||
}
|
}
|
||||||
@@ -22,16 +22,13 @@ use crate::{
|
|||||||
project::{
|
project::{
|
||||||
Project,
|
Project,
|
||||||
ProjectNode,
|
ProjectNode,
|
||||||
InstanceProjectNode,
|
|
||||||
SyncPointProjectNode,
|
|
||||||
},
|
},
|
||||||
snapshot_reconciler::{
|
snapshot_reconciler::{
|
||||||
RbxSnapshotInstance,
|
RbxSnapshotInstance,
|
||||||
snapshot_from_tree,
|
snapshot_from_tree,
|
||||||
},
|
},
|
||||||
path_map::PathMap,
|
// TODO: Move MetadataPerInstance into this module?
|
||||||
// TODO: Move MetadataPerPath into this module?
|
rbx_session::MetadataPerInstance,
|
||||||
rbx_session::{MetadataPerPath, MetadataPerInstance},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const INIT_MODULE_NAME: &str = "init.lua";
|
const INIT_MODULE_NAME: &str = "init.lua";
|
||||||
@@ -40,10 +37,6 @@ const INIT_CLIENT_NAME: &str = "init.client.lua";
|
|||||||
|
|
||||||
pub type SnapshotResult<'a> = Result<Option<RbxSnapshotInstance<'a>>, SnapshotError>;
|
pub type SnapshotResult<'a> = Result<Option<RbxSnapshotInstance<'a>>, SnapshotError>;
|
||||||
|
|
||||||
pub struct SnapshotContext<'meta> {
|
|
||||||
pub metadata_per_path: &'meta mut PathMap<MetadataPerPath>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
#[derive(Debug, Fail)]
|
||||||
pub enum SnapshotError {
|
pub enum SnapshotError {
|
||||||
DidNotExist(PathBuf),
|
DidNotExist(PathBuf),
|
||||||
@@ -55,6 +48,7 @@ pub enum SnapshotError {
|
|||||||
},
|
},
|
||||||
|
|
||||||
JsonModelDecodeError {
|
JsonModelDecodeError {
|
||||||
|
#[fail(cause)]
|
||||||
inner: serde_json::Error,
|
inner: serde_json::Error,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
},
|
},
|
||||||
@@ -68,6 +62,12 @@ pub enum SnapshotError {
|
|||||||
inner: rbx_binary::DecodeError,
|
inner: rbx_binary::DecodeError,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ProjectNodeUnusable,
|
||||||
|
|
||||||
|
ProjectNodeInvalidTransmute {
|
||||||
|
partition_path: PathBuf,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for SnapshotError {
|
impl fmt::Display for SnapshotError {
|
||||||
@@ -78,7 +78,7 @@ impl fmt::Display for SnapshotError {
|
|||||||
write!(output, "Invalid UTF-8: {} in path {}", inner, path.display())
|
write!(output, "Invalid UTF-8: {} in path {}", inner, path.display())
|
||||||
},
|
},
|
||||||
SnapshotError::JsonModelDecodeError { inner, path } => {
|
SnapshotError::JsonModelDecodeError { inner, path } => {
|
||||||
write!(output, "Malformed .model.json model: {:?} in path {}", inner, path.display())
|
write!(output, "Malformed .model.json model: {} in path {}", inner, path.display())
|
||||||
},
|
},
|
||||||
SnapshotError::XmlModelDecodeError { inner, path } => {
|
SnapshotError::XmlModelDecodeError { inner, path } => {
|
||||||
write!(output, "Malformed rbxmx model: {:?} in path {}", inner, path.display())
|
write!(output, "Malformed rbxmx model: {:?} in path {}", inner, path.display())
|
||||||
@@ -86,107 +86,131 @@ impl fmt::Display for SnapshotError {
|
|||||||
SnapshotError::BinaryModelDecodeError { inner, path } => {
|
SnapshotError::BinaryModelDecodeError { inner, path } => {
|
||||||
write!(output, "Malformed rbxm model: {:?} in path {}", inner, path.display())
|
write!(output, "Malformed rbxm model: {:?} in path {}", inner, path.display())
|
||||||
},
|
},
|
||||||
|
SnapshotError::ProjectNodeUnusable => {
|
||||||
|
write!(output, "Rojo project nodes must specify either $path or $className.")
|
||||||
|
},
|
||||||
|
SnapshotError::ProjectNodeInvalidTransmute { partition_path } => {
|
||||||
|
writeln!(output, "Rojo project nodes that specify both $path and $className require that the")?;
|
||||||
|
writeln!(output, "instance produced by the files pointed to by $path has a ClassName of")?;
|
||||||
|
writeln!(output, "Folder.")?;
|
||||||
|
writeln!(output, "")?;
|
||||||
|
writeln!(output, "Partition target ($path): {}", partition_path.display())
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn snapshot_project_tree<'source>(
|
pub fn snapshot_project_tree<'source>(
|
||||||
imfs: &'source Imfs,
|
imfs: &'source Imfs,
|
||||||
context: &mut SnapshotContext,
|
|
||||||
project: &'source Project,
|
project: &'source Project,
|
||||||
) -> SnapshotResult<'source> {
|
) -> SnapshotResult<'source> {
|
||||||
snapshot_project_node(imfs, context, &project.tree, Cow::Borrowed(&project.name))
|
snapshot_project_node(imfs, &project.tree, Cow::Borrowed(&project.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn snapshot_project_node<'source>(
|
pub fn snapshot_project_node<'source>(
|
||||||
imfs: &'source Imfs,
|
imfs: &'source Imfs,
|
||||||
context: &mut SnapshotContext,
|
node: &ProjectNode,
|
||||||
node: &'source ProjectNode,
|
|
||||||
instance_name: Cow<'source, str>,
|
instance_name: Cow<'source, str>,
|
||||||
) -> SnapshotResult<'source> {
|
) -> SnapshotResult<'source> {
|
||||||
match node {
|
let maybe_snapshot = match &node.path {
|
||||||
ProjectNode::Instance(instance_node) => snapshot_instance_node(imfs, context, instance_node, instance_name),
|
Some(path) => snapshot_imfs_path(imfs, &path, Some(instance_name.clone()))?,
|
||||||
ProjectNode::SyncPoint(sync_node) => snapshot_sync_point_node(imfs, context, sync_node, instance_name),
|
None => match &node.class_name {
|
||||||
}
|
Some(_class_name) => Some(RbxSnapshotInstance {
|
||||||
}
|
name: instance_name.clone(),
|
||||||
|
|
||||||
fn snapshot_instance_node<'source>(
|
// These properties are replaced later in the function to
|
||||||
imfs: &'source Imfs,
|
// reduce code duplication.
|
||||||
context: &mut SnapshotContext,
|
class_name: Cow::Borrowed("Folder"),
|
||||||
node: &'source InstanceProjectNode,
|
properties: HashMap::new(),
|
||||||
instance_name: Cow<'source, str>,
|
children: Vec::new(),
|
||||||
) -> SnapshotResult<'source> {
|
metadata: MetadataPerInstance {
|
||||||
let mut children = Vec::new();
|
source_path: None,
|
||||||
|
ignore_unknown_instances: true,
|
||||||
for (child_name, child_project_node) in &node.children {
|
project_definition: None,
|
||||||
if let Some(child) = snapshot_project_node(imfs, context, child_project_node, Cow::Borrowed(child_name))? {
|
},
|
||||||
children.push(child);
|
}),
|
||||||
}
|
None => {
|
||||||
}
|
return Err(SnapshotError::ProjectNodeUnusable);
|
||||||
|
},
|
||||||
Ok(Some(RbxSnapshotInstance {
|
|
||||||
class_name: Cow::Borrowed(&node.class_name),
|
|
||||||
name: instance_name,
|
|
||||||
properties: node.properties.clone(),
|
|
||||||
children,
|
|
||||||
metadata: MetadataPerInstance {
|
|
||||||
source_path: None,
|
|
||||||
ignore_unknown_instances: node.metadata.ignore_unknown_instances,
|
|
||||||
},
|
},
|
||||||
}))
|
};
|
||||||
}
|
|
||||||
|
|
||||||
fn snapshot_sync_point_node<'source>(
|
|
||||||
imfs: &'source Imfs,
|
|
||||||
context: &mut SnapshotContext,
|
|
||||||
node: &'source SyncPointProjectNode,
|
|
||||||
instance_name: Cow<'source, str>,
|
|
||||||
) -> SnapshotResult<'source> {
|
|
||||||
let maybe_snapshot = snapshot_imfs_path(imfs, context, &node.path, Some(instance_name))?;
|
|
||||||
|
|
||||||
// If the snapshot resulted in no instances, like if it targets an unknown
|
// If the snapshot resulted in no instances, like if it targets an unknown
|
||||||
// file or an empty model file, we can early-return.
|
// file or an empty model file, we can early-return.
|
||||||
let snapshot = match maybe_snapshot {
|
//
|
||||||
|
// In the future, we might want to issue a warning if the project also
|
||||||
|
// specified fields like class_name, since the user will probably be
|
||||||
|
// confused as to why nothing showed up in the tree.
|
||||||
|
let mut snapshot = match maybe_snapshot {
|
||||||
Some(snapshot) => snapshot,
|
Some(snapshot) => snapshot,
|
||||||
None => return Ok(None),
|
None => {
|
||||||
|
// TODO: Return some other sort of marker here instead? If a node
|
||||||
|
// transitions from None into Some, it's possible that configuration
|
||||||
|
// from the ProjectNode might be lost since there's nowhere to put
|
||||||
|
// it!
|
||||||
|
return Ok(None);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Otherwise, we can log the name of the sync point we just snapshotted.
|
// Applies the class name specified in `class_name` from the project, if it's
|
||||||
let path_meta = context.metadata_per_path.entry(node.path.to_owned()).or_default();
|
// set.
|
||||||
path_meta.instance_name = Some(snapshot.name.clone().into_owned());
|
if let Some(class_name) = &node.class_name {
|
||||||
|
// This can only happen if `path` was specified in the project node and
|
||||||
|
// that path represented a non-Folder instance.
|
||||||
|
if snapshot.class_name != "Folder" {
|
||||||
|
return Err(SnapshotError::ProjectNodeInvalidTransmute {
|
||||||
|
partition_path: node.path.as_ref().unwrap().to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.class_name = Cow::Owned(class_name.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (child_name, child_project_node) in &node.children {
|
||||||
|
if let Some(child) = snapshot_project_node(imfs, child_project_node, Cow::Owned(child_name.clone()))? {
|
||||||
|
snapshot.children.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, value) in &node.properties {
|
||||||
|
snapshot.properties.insert(key.clone(), value.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ignore_unknown_instances) = node.ignore_unknown_instances {
|
||||||
|
snapshot.metadata.ignore_unknown_instances = ignore_unknown_instances;
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.metadata.project_definition = Some((instance_name.into_owned(), node.clone()));
|
||||||
|
|
||||||
Ok(Some(snapshot))
|
Ok(Some(snapshot))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn snapshot_imfs_path<'source>(
|
pub fn snapshot_imfs_path<'source>(
|
||||||
imfs: &'source Imfs,
|
imfs: &'source Imfs,
|
||||||
context: &mut SnapshotContext,
|
|
||||||
path: &Path,
|
path: &Path,
|
||||||
instance_name: Option<Cow<'source, str>>,
|
instance_name: Option<Cow<'source, str>>,
|
||||||
) -> SnapshotResult<'source> {
|
) -> SnapshotResult<'source> {
|
||||||
// If the given path doesn't exist in the in-memory filesystem, we consider
|
// If the given path doesn't exist in the in-memory filesystem, we consider
|
||||||
// that an error.
|
// that an error.
|
||||||
match imfs.get(path) {
|
match imfs.get(path) {
|
||||||
Some(imfs_item) => snapshot_imfs_item(imfs, context, imfs_item, instance_name),
|
Some(imfs_item) => snapshot_imfs_item(imfs, imfs_item, instance_name),
|
||||||
None => return Err(SnapshotError::DidNotExist(path.to_owned())),
|
None => return Err(SnapshotError::DidNotExist(path.to_owned())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn snapshot_imfs_item<'source>(
|
fn snapshot_imfs_item<'source>(
|
||||||
imfs: &'source Imfs,
|
imfs: &'source Imfs,
|
||||||
context: &mut SnapshotContext,
|
|
||||||
item: &'source ImfsItem,
|
item: &'source ImfsItem,
|
||||||
instance_name: Option<Cow<'source, str>>,
|
instance_name: Option<Cow<'source, str>>,
|
||||||
) -> SnapshotResult<'source> {
|
) -> SnapshotResult<'source> {
|
||||||
match item {
|
match item {
|
||||||
ImfsItem::File(file) => snapshot_imfs_file(file, instance_name),
|
ImfsItem::File(file) => snapshot_imfs_file(file, instance_name),
|
||||||
ImfsItem::Directory(directory) => snapshot_imfs_directory(imfs, context, directory, instance_name),
|
ImfsItem::Directory(directory) => snapshot_imfs_directory(imfs, directory, instance_name),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn snapshot_imfs_directory<'source>(
|
fn snapshot_imfs_directory<'source>(
|
||||||
imfs: &'source Imfs,
|
imfs: &'source Imfs,
|
||||||
context: &mut SnapshotContext,
|
|
||||||
directory: &'source ImfsDirectory,
|
directory: &'source ImfsDirectory,
|
||||||
instance_name: Option<Cow<'source, str>>,
|
instance_name: Option<Cow<'source, str>>,
|
||||||
) -> SnapshotResult<'source> {
|
) -> SnapshotResult<'source> {
|
||||||
@@ -202,11 +226,11 @@ fn snapshot_imfs_directory<'source>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let mut snapshot = if directory.children.contains(&init_path) {
|
let mut snapshot = if directory.children.contains(&init_path) {
|
||||||
snapshot_imfs_path(imfs, context, &init_path, Some(snapshot_name))?.unwrap()
|
snapshot_imfs_path(imfs, &init_path, Some(snapshot_name))?.unwrap()
|
||||||
} else if directory.children.contains(&init_server_path) {
|
} else if directory.children.contains(&init_server_path) {
|
||||||
snapshot_imfs_path(imfs, context, &init_server_path, Some(snapshot_name))?.unwrap()
|
snapshot_imfs_path(imfs, &init_server_path, Some(snapshot_name))?.unwrap()
|
||||||
} else if directory.children.contains(&init_client_path) {
|
} else if directory.children.contains(&init_client_path) {
|
||||||
snapshot_imfs_path(imfs, context, &init_client_path, Some(snapshot_name))?.unwrap()
|
snapshot_imfs_path(imfs, &init_client_path, Some(snapshot_name))?.unwrap()
|
||||||
} else {
|
} else {
|
||||||
RbxSnapshotInstance {
|
RbxSnapshotInstance {
|
||||||
class_name: Cow::Borrowed("Folder"),
|
class_name: Cow::Borrowed("Folder"),
|
||||||
@@ -216,6 +240,7 @@ fn snapshot_imfs_directory<'source>(
|
|||||||
metadata: MetadataPerInstance {
|
metadata: MetadataPerInstance {
|
||||||
source_path: None,
|
source_path: None,
|
||||||
ignore_unknown_instances: false,
|
ignore_unknown_instances: false,
|
||||||
|
project_definition: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -234,7 +259,7 @@ fn snapshot_imfs_directory<'source>(
|
|||||||
// them here.
|
// them here.
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(child) = snapshot_imfs_path(imfs, context, child_path, None)? {
|
if let Some(child) = snapshot_imfs_path(imfs, child_path, None)? {
|
||||||
snapshot.children.push(child);
|
snapshot.children.push(child);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -316,6 +341,7 @@ fn snapshot_lua_file<'source>(
|
|||||||
metadata: MetadataPerInstance {
|
metadata: MetadataPerInstance {
|
||||||
source_path: Some(file.path.to_path_buf()),
|
source_path: Some(file.path.to_path_buf()),
|
||||||
ignore_unknown_instances: false,
|
ignore_unknown_instances: false,
|
||||||
|
project_definition: None,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -354,6 +380,7 @@ fn snapshot_txt_file<'source>(
|
|||||||
metadata: MetadataPerInstance {
|
metadata: MetadataPerInstance {
|
||||||
source_path: Some(file.path.to_path_buf()),
|
source_path: Some(file.path.to_path_buf()),
|
||||||
ignore_unknown_instances: false,
|
ignore_unknown_instances: false,
|
||||||
|
project_definition: None,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -387,6 +414,7 @@ fn snapshot_csv_file<'source>(
|
|||||||
metadata: MetadataPerInstance {
|
metadata: MetadataPerInstance {
|
||||||
source_path: Some(file.path.to_path_buf()),
|
source_path: Some(file.path.to_path_buf()),
|
||||||
ignore_unknown_instances: false,
|
ignore_unknown_instances: false,
|
||||||
|
project_definition: None,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use std::{
|
use std::{
|
||||||
str,
|
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
|
cmp::Ordering,
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
fmt,
|
fmt,
|
||||||
|
str,
|
||||||
};
|
};
|
||||||
|
|
||||||
use rbx_tree::{RbxTree, RbxId, RbxInstanceProperties, RbxValue};
|
use rbx_tree::{RbxTree, RbxId, RbxInstanceProperties, RbxValue};
|
||||||
@@ -10,7 +11,7 @@ use serde_derive::{Serialize, Deserialize};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
path_map::PathMap,
|
path_map::PathMap,
|
||||||
rbx_session::{MetadataPerPath, MetadataPerInstance},
|
rbx_session::MetadataPerInstance,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
@@ -55,7 +56,7 @@ impl InstanceChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct RbxSnapshotInstance<'a> {
|
pub struct RbxSnapshotInstance<'a> {
|
||||||
pub name: Cow<'a, str>,
|
pub name: Cow<'a, str>,
|
||||||
pub class_name: Cow<'a, str>,
|
pub class_name: Cow<'a, str>,
|
||||||
@@ -64,6 +65,13 @@ pub struct RbxSnapshotInstance<'a> {
|
|||||||
pub metadata: MetadataPerInstance,
|
pub metadata: MetadataPerInstance,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> PartialOrd for RbxSnapshotInstance<'a> {
|
||||||
|
fn partial_cmp(&self, other: &RbxSnapshotInstance) -> Option<Ordering> {
|
||||||
|
Some(self.name.cmp(&other.name)
|
||||||
|
.then(self.class_name.cmp(&other.class_name)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn snapshot_from_tree(tree: &RbxTree, id: RbxId) -> Option<RbxSnapshotInstance<'static>> {
|
pub fn snapshot_from_tree(tree: &RbxTree, id: RbxId) -> Option<RbxSnapshotInstance<'static>> {
|
||||||
let instance = tree.get_instance(id)?;
|
let instance = tree.get_instance(id)?;
|
||||||
|
|
||||||
@@ -80,31 +88,27 @@ pub fn snapshot_from_tree(tree: &RbxTree, id: RbxId) -> Option<RbxSnapshotInstan
|
|||||||
metadata: MetadataPerInstance {
|
metadata: MetadataPerInstance {
|
||||||
source_path: None,
|
source_path: None,
|
||||||
ignore_unknown_instances: false,
|
ignore_unknown_instances: false,
|
||||||
|
project_definition: None,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reify_root(
|
pub fn reify_root(
|
||||||
snapshot: &RbxSnapshotInstance,
|
snapshot: &RbxSnapshotInstance,
|
||||||
metadata_per_path: &mut PathMap<MetadataPerPath>,
|
instance_per_path: &mut PathMap<HashSet<RbxId>>,
|
||||||
instance_metadata_map: &mut HashMap<RbxId, MetadataPerInstance>,
|
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
|
||||||
changes: &mut InstanceChanges,
|
changes: &mut InstanceChanges,
|
||||||
) -> RbxTree {
|
) -> RbxTree {
|
||||||
let instance = reify_core(snapshot);
|
let instance = reify_core(snapshot);
|
||||||
let mut tree = RbxTree::new(instance);
|
let mut tree = RbxTree::new(instance);
|
||||||
let root_id = tree.get_root_id();
|
let id = tree.get_root_id();
|
||||||
|
|
||||||
if let Some(source_path) = &snapshot.metadata.source_path {
|
reify_metadata(snapshot, id, instance_per_path, metadata_per_instance);
|
||||||
let path_meta = metadata_per_path.entry(source_path.to_owned()).or_default();
|
|
||||||
path_meta.instance_id = Some(root_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
instance_metadata_map.insert(root_id, snapshot.metadata.clone());
|
changes.added.insert(id);
|
||||||
|
|
||||||
changes.added.insert(root_id);
|
|
||||||
|
|
||||||
for child in &snapshot.children {
|
for child in &snapshot.children {
|
||||||
reify_subtree(child, &mut tree, root_id, metadata_per_path, instance_metadata_map, changes);
|
reify_subtree(child, &mut tree, id, instance_per_path, metadata_per_instance, changes);
|
||||||
}
|
}
|
||||||
|
|
||||||
tree
|
tree
|
||||||
@@ -114,47 +118,58 @@ pub fn reify_subtree(
|
|||||||
snapshot: &RbxSnapshotInstance,
|
snapshot: &RbxSnapshotInstance,
|
||||||
tree: &mut RbxTree,
|
tree: &mut RbxTree,
|
||||||
parent_id: RbxId,
|
parent_id: RbxId,
|
||||||
metadata_per_path: &mut PathMap<MetadataPerPath>,
|
instance_per_path: &mut PathMap<HashSet<RbxId>>,
|
||||||
instance_metadata_map: &mut HashMap<RbxId, MetadataPerInstance>,
|
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
|
||||||
changes: &mut InstanceChanges,
|
changes: &mut InstanceChanges,
|
||||||
) {
|
) {
|
||||||
let instance = reify_core(snapshot);
|
let instance = reify_core(snapshot);
|
||||||
let id = tree.insert_instance(instance, parent_id);
|
let id = tree.insert_instance(instance, parent_id);
|
||||||
|
|
||||||
if let Some(source_path) = &snapshot.metadata.source_path {
|
reify_metadata(snapshot, id, instance_per_path, metadata_per_instance);
|
||||||
let path_meta = metadata_per_path.entry(source_path.clone()).or_default();
|
|
||||||
path_meta.instance_id = Some(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
instance_metadata_map.insert(id, snapshot.metadata.clone());
|
|
||||||
|
|
||||||
changes.added.insert(id);
|
changes.added.insert(id);
|
||||||
|
|
||||||
for child in &snapshot.children {
|
for child in &snapshot.children {
|
||||||
reify_subtree(child, tree, id, metadata_per_path, instance_metadata_map, changes);
|
reify_subtree(child, tree, id, instance_per_path, metadata_per_instance, changes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reify_metadata(
|
||||||
|
snapshot: &RbxSnapshotInstance,
|
||||||
|
instance_id: RbxId,
|
||||||
|
instance_per_path: &mut PathMap<HashSet<RbxId>>,
|
||||||
|
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
|
||||||
|
) {
|
||||||
|
if let Some(source_path) = &snapshot.metadata.source_path {
|
||||||
|
let path_metadata = match instance_per_path.get_mut(&source_path) {
|
||||||
|
Some(v) => v,
|
||||||
|
None => {
|
||||||
|
instance_per_path.insert(source_path.clone(), Default::default());
|
||||||
|
instance_per_path.get_mut(&source_path).unwrap()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
path_metadata.insert(instance_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata_per_instance.insert(instance_id, snapshot.metadata.clone());
|
||||||
|
}
|
||||||
|
|
||||||
pub fn reconcile_subtree(
|
pub fn reconcile_subtree(
|
||||||
tree: &mut RbxTree,
|
tree: &mut RbxTree,
|
||||||
id: RbxId,
|
id: RbxId,
|
||||||
snapshot: &RbxSnapshotInstance,
|
snapshot: &RbxSnapshotInstance,
|
||||||
metadata_per_path: &mut PathMap<MetadataPerPath>,
|
instance_per_path: &mut PathMap<HashSet<RbxId>>,
|
||||||
instance_metadata_map: &mut HashMap<RbxId, MetadataPerInstance>,
|
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
|
||||||
changes: &mut InstanceChanges,
|
changes: &mut InstanceChanges,
|
||||||
) {
|
) {
|
||||||
if let Some(source_path) = &snapshot.metadata.source_path {
|
reify_metadata(snapshot, id, instance_per_path, metadata_per_instance);
|
||||||
let path_meta = metadata_per_path.entry(source_path.to_owned()).or_default();
|
|
||||||
path_meta.instance_id = Some(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
instance_metadata_map.insert(id, snapshot.metadata.clone());
|
|
||||||
|
|
||||||
if reconcile_instance_properties(tree.get_instance_mut(id).unwrap(), snapshot) {
|
if reconcile_instance_properties(tree.get_instance_mut(id).unwrap(), snapshot) {
|
||||||
changes.updated.insert(id);
|
changes.updated.insert(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
reconcile_instance_children(tree, id, snapshot, metadata_per_path, instance_metadata_map, changes);
|
reconcile_instance_children(tree, id, snapshot, instance_per_path, metadata_per_instance, changes);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reify_core(snapshot: &RbxSnapshotInstance) -> RbxInstanceProperties {
|
fn reify_core(snapshot: &RbxSnapshotInstance) -> RbxInstanceProperties {
|
||||||
@@ -234,8 +249,8 @@ fn reconcile_instance_children(
|
|||||||
tree: &mut RbxTree,
|
tree: &mut RbxTree,
|
||||||
id: RbxId,
|
id: RbxId,
|
||||||
snapshot: &RbxSnapshotInstance,
|
snapshot: &RbxSnapshotInstance,
|
||||||
metadata_per_path: &mut PathMap<MetadataPerPath>,
|
instance_per_path: &mut PathMap<HashSet<RbxId>>,
|
||||||
instance_metadata_map: &mut HashMap<RbxId, MetadataPerInstance>,
|
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
|
||||||
changes: &mut InstanceChanges,
|
changes: &mut InstanceChanges,
|
||||||
) {
|
) {
|
||||||
let mut visited_snapshot_indices = HashSet::new();
|
let mut visited_snapshot_indices = HashSet::new();
|
||||||
@@ -287,19 +302,19 @@ fn reconcile_instance_children(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for child_snapshot in &children_to_add {
|
for child_snapshot in &children_to_add {
|
||||||
reify_subtree(child_snapshot, tree, id, metadata_per_path, instance_metadata_map, changes);
|
reify_subtree(child_snapshot, tree, id, instance_per_path, metadata_per_instance, changes);
|
||||||
}
|
}
|
||||||
|
|
||||||
for child_id in &children_to_remove {
|
for child_id in &children_to_remove {
|
||||||
if let Some(subtree) = tree.remove_instance(*child_id) {
|
if let Some(subtree) = tree.remove_instance(*child_id) {
|
||||||
for id in subtree.iter_all_ids() {
|
for id in subtree.iter_all_ids() {
|
||||||
instance_metadata_map.remove(&id);
|
metadata_per_instance.remove(&id);
|
||||||
changes.removed.insert(id);
|
changes.removed.insert(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (child_id, child_snapshot) in &children_to_update {
|
for (child_id, child_snapshot) in &children_to_update {
|
||||||
reconcile_subtree(tree, *child_id, child_snapshot, metadata_per_path, instance_metadata_map, changes);
|
reconcile_subtree(tree, *child_id, child_snapshot, instance_per_path, metadata_per_instance, changes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ use rbx_tree::RbxId;
|
|||||||
use crate::{
|
use crate::{
|
||||||
imfs::{Imfs, ImfsItem},
|
imfs::{Imfs, ImfsItem},
|
||||||
rbx_session::RbxSession,
|
rbx_session::RbxSession,
|
||||||
web::InstanceMetadata,
|
web::PublicInstanceMetadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
static GRAPHVIZ_HEADER: &str = r#"
|
static GRAPHVIZ_HEADER: &str = r#"
|
||||||
@@ -74,7 +74,7 @@ fn visualize_rbx_node(session: &RbxSession, id: RbxId, output: &mut fmt::Formatt
|
|||||||
let mut node_label = format!("{}|{}|{}", node.name, node.class_name, id);
|
let mut node_label = format!("{}|{}|{}", node.name, node.class_name, id);
|
||||||
|
|
||||||
if let Some(session_metadata) = session.get_instance_metadata(id) {
|
if let Some(session_metadata) = session.get_instance_metadata(id) {
|
||||||
let metadata = InstanceMetadata::from_session_metadata(session_metadata);
|
let metadata = PublicInstanceMetadata::from_session_metadata(session_metadata);
|
||||||
node_label.push('|');
|
node_label.push('|');
|
||||||
node_label.push_str(&serde_json::to_string(&metadata).unwrap());
|
node_label.push_str(&serde_json::to_string(&metadata).unwrap());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ static HOME_CONTENT: &str = include_str!("../assets/index.html");
|
|||||||
/// Contains the instance metadata relevant to Rojo clients.
|
/// Contains the instance metadata relevant to Rojo clients.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct InstanceMetadata {
|
pub struct PublicInstanceMetadata {
|
||||||
ignore_unknown_instances: bool,
|
ignore_unknown_instances: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InstanceMetadata {
|
impl PublicInstanceMetadata {
|
||||||
pub fn from_session_metadata(meta: &MetadataPerInstance) -> InstanceMetadata {
|
pub fn from_session_metadata(meta: &MetadataPerInstance) -> PublicInstanceMetadata {
|
||||||
InstanceMetadata {
|
PublicInstanceMetadata {
|
||||||
ignore_unknown_instances: meta.ignore_unknown_instances,
|
ignore_unknown_instances: meta.ignore_unknown_instances,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ pub struct InstanceWithMetadata<'a> {
|
|||||||
pub instance: Cow<'a, RbxInstance>,
|
pub instance: Cow<'a, RbxInstance>,
|
||||||
|
|
||||||
#[serde(rename = "Metadata")]
|
#[serde(rename = "Metadata")]
|
||||||
pub metadata: Option<InstanceMetadata>,
|
pub metadata: Option<PublicInstanceMetadata>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@@ -120,9 +120,6 @@ impl Server {
|
|||||||
(GET) (/visualize/imfs) => {
|
(GET) (/visualize/imfs) => {
|
||||||
self.handle_visualize_imfs()
|
self.handle_visualize_imfs()
|
||||||
},
|
},
|
||||||
(GET) (/visualize/path_metadata) => {
|
|
||||||
self.handle_visualize_path_metadata()
|
|
||||||
},
|
|
||||||
_ => Response::empty_404()
|
_ => Response::empty_404()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -209,7 +206,7 @@ impl Server {
|
|||||||
for &requested_id in &requested_ids {
|
for &requested_id in &requested_ids {
|
||||||
if let Some(instance) = tree.get_instance(requested_id) {
|
if let Some(instance) = tree.get_instance(requested_id) {
|
||||||
let metadata = rbx_session.get_instance_metadata(requested_id)
|
let metadata = rbx_session.get_instance_metadata(requested_id)
|
||||||
.map(InstanceMetadata::from_session_metadata);
|
.map(PublicInstanceMetadata::from_session_metadata);
|
||||||
|
|
||||||
instances.insert(instance.get_id(), InstanceWithMetadata {
|
instances.insert(instance.get_id(), InstanceWithMetadata {
|
||||||
instance: Cow::Borrowed(instance),
|
instance: Cow::Borrowed(instance),
|
||||||
@@ -218,7 +215,7 @@ impl Server {
|
|||||||
|
|
||||||
for descendant in tree.descendants(requested_id) {
|
for descendant in tree.descendants(requested_id) {
|
||||||
let descendant_meta = rbx_session.get_instance_metadata(descendant.get_id())
|
let descendant_meta = rbx_session.get_instance_metadata(descendant.get_id())
|
||||||
.map(InstanceMetadata::from_session_metadata);
|
.map(PublicInstanceMetadata::from_session_metadata);
|
||||||
|
|
||||||
instances.insert(descendant.get_id(), InstanceWithMetadata {
|
instances.insert(descendant.get_id(), InstanceWithMetadata {
|
||||||
instance: Cow::Borrowed(descendant),
|
instance: Cow::Borrowed(descendant),
|
||||||
@@ -254,9 +251,4 @@ impl Server {
|
|||||||
None => Response::text(dot_source),
|
None => Response::text(dot_source),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_visualize_path_metadata(&self) -> Response {
|
|
||||||
let rbx_session = self.live_session.rbx_session.lock().unwrap();
|
|
||||||
Response::json(&rbx_session.debug_get_metadata_per_path())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
#[macro_use] extern crate lazy_static;
|
#[macro_use] extern crate lazy_static;
|
||||||
|
|
||||||
extern crate librojo;
|
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use rbx_tree::RbxValue;
|
use rbx_tree::RbxValue;
|
||||||
|
|
||||||
use librojo::{
|
use librojo::{
|
||||||
project::{Project, ProjectNode, InstanceProjectNode, SyncPointProjectNode},
|
project::{Project, ProjectNode},
|
||||||
};
|
};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
@@ -44,54 +43,52 @@ fn empty_fuzzy_folder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn single_sync_point() {
|
fn single_partition_game() {
|
||||||
let project_file_location = TEST_PROJECTS_ROOT.join("single-sync-point/default.project.json");
|
let project_location = TEST_PROJECTS_ROOT.join("single_partition_game");
|
||||||
let project = Project::load_exact(&project_file_location).unwrap();
|
let project = Project::load_fuzzy(&project_location).unwrap();
|
||||||
|
|
||||||
let expected_project = {
|
let expected_project = {
|
||||||
let foo = ProjectNode::SyncPoint(SyncPointProjectNode {
|
let foo = ProjectNode {
|
||||||
path: project_file_location.parent().unwrap().join("lib"),
|
path: Some(project_location.join("lib")),
|
||||||
});
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
let mut replicated_storage_children = HashMap::new();
|
let mut replicated_storage_children = HashMap::new();
|
||||||
replicated_storage_children.insert("Foo".to_string(), foo);
|
replicated_storage_children.insert("Foo".to_string(), foo);
|
||||||
|
|
||||||
let replicated_storage = ProjectNode::Instance(InstanceProjectNode {
|
let replicated_storage = ProjectNode {
|
||||||
class_name: "ReplicatedStorage".to_string(),
|
class_name: Some(String::from("ReplicatedStorage")),
|
||||||
children: replicated_storage_children,
|
children: replicated_storage_children,
|
||||||
properties: HashMap::new(),
|
..Default::default()
|
||||||
metadata: Default::default(),
|
};
|
||||||
});
|
|
||||||
|
|
||||||
let mut http_service_properties = HashMap::new();
|
let mut http_service_properties = HashMap::new();
|
||||||
http_service_properties.insert("HttpEnabled".to_string(), RbxValue::Bool {
|
http_service_properties.insert("HttpEnabled".to_string(), RbxValue::Bool {
|
||||||
value: true,
|
value: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
let http_service = ProjectNode::Instance(InstanceProjectNode {
|
let http_service = ProjectNode {
|
||||||
class_name: "HttpService".to_string(),
|
class_name: Some(String::from("HttpService")),
|
||||||
children: HashMap::new(),
|
|
||||||
properties: http_service_properties,
|
properties: http_service_properties,
|
||||||
metadata: Default::default(),
|
..Default::default()
|
||||||
});
|
};
|
||||||
|
|
||||||
let mut root_children = HashMap::new();
|
let mut root_children = HashMap::new();
|
||||||
root_children.insert("ReplicatedStorage".to_string(), replicated_storage);
|
root_children.insert("ReplicatedStorage".to_string(), replicated_storage);
|
||||||
root_children.insert("HttpService".to_string(), http_service);
|
root_children.insert("HttpService".to_string(), http_service);
|
||||||
|
|
||||||
let root_node = ProjectNode::Instance(InstanceProjectNode {
|
let root_node = ProjectNode {
|
||||||
class_name: "DataModel".to_string(),
|
class_name: Some(String::from("DataModel")),
|
||||||
children: root_children,
|
children: root_children,
|
||||||
properties: HashMap::new(),
|
..Default::default()
|
||||||
metadata: Default::default(),
|
};
|
||||||
});
|
|
||||||
|
|
||||||
Project {
|
Project {
|
||||||
name: "single-sync-point".to_string(),
|
name: "single-sync-point".to_string(),
|
||||||
tree: root_node,
|
tree: root_node,
|
||||||
serve_port: None,
|
serve_port: None,
|
||||||
serve_place_ids: None,
|
serve_place_ids: None,
|
||||||
file_location: project_file_location.clone(),
|
file_location: project_location.join("default.project.json"),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,9 +96,17 @@ fn single_sync_point() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_model() {
|
fn single_partition_model() {
|
||||||
let project_file_location = TEST_PROJECTS_ROOT.join("test-model/default.project.json");
|
let project_file_location = TEST_PROJECTS_ROOT.join("single_partition_model");
|
||||||
let project = Project::load_exact(&project_file_location).unwrap();
|
let project = Project::load_fuzzy(&project_file_location).unwrap();
|
||||||
|
|
||||||
assert_eq!(project.name, "test-model");
|
assert_eq!(project.name, "test-model");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn composing_models() {
|
||||||
|
let project_file_location = TEST_PROJECTS_ROOT.join("composing_models");
|
||||||
|
let project = Project::load_fuzzy(&project_file_location).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(project.name, "composing-models");
|
||||||
}
|
}
|
||||||
124
server/tests/snapshots.rs
Normal file
124
server/tests/snapshots.rs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
use std::{
|
||||||
|
fs::{self, File},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
use librojo::{
|
||||||
|
imfs::Imfs,
|
||||||
|
project::{Project, ProjectNode},
|
||||||
|
rbx_snapshot::snapshot_project_tree,
|
||||||
|
snapshot_reconciler::{RbxSnapshotInstance},
|
||||||
|
};
|
||||||
|
|
||||||
|
macro_rules! generate_snapshot_tests {
|
||||||
|
($($name: ident),*) => {
|
||||||
|
$(
|
||||||
|
paste::item! {
|
||||||
|
#[test]
|
||||||
|
fn [<snapshot_ $name>]() {
|
||||||
|
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 mut snapshot = snapshot_project_tree(&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<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");
|
||||||
|
}
|
||||||
20
test-projects/empty/expected-snapshot.json
Normal file
20
test-projects/empty/expected-snapshot.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "empty",
|
||||||
|
"class_name": "DataModel",
|
||||||
|
"properties": {},
|
||||||
|
"children": [],
|
||||||
|
"metadata": {
|
||||||
|
"ignore_unknown_instances": true,
|
||||||
|
"source_path": null,
|
||||||
|
"project_definition": [
|
||||||
|
"empty",
|
||||||
|
{
|
||||||
|
"class_name": "DataModel",
|
||||||
|
"children": {},
|
||||||
|
"properties": {},
|
||||||
|
"ignore_unknown_instances": null,
|
||||||
|
"path": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
9
test-projects/nested_partitions/default.project.json
Normal file
9
test-projects/nested_partitions/default.project.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "nested-partitions",
|
||||||
|
"tree": {
|
||||||
|
"$path": "outer",
|
||||||
|
"inner": {
|
||||||
|
"$path": "inner"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
82
test-projects/nested_partitions/expected-snapshot.json
Normal file
82
test-projects/nested_partitions/expected-snapshot.json
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"name": "nested-partitions",
|
||||||
|
"class_name": "Folder",
|
||||||
|
"properties": {},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "inner",
|
||||||
|
"class_name": "Folder",
|
||||||
|
"properties": {},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "hello",
|
||||||
|
"class_name": "ModuleScript",
|
||||||
|
"properties": {
|
||||||
|
"Source": {
|
||||||
|
"Type": "String",
|
||||||
|
"Value": "-- inner/hello.lua"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"children": [],
|
||||||
|
"metadata": {
|
||||||
|
"ignore_unknown_instances": false,
|
||||||
|
"source_path": "inner/hello.lua",
|
||||||
|
"project_definition": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"ignore_unknown_instances": false,
|
||||||
|
"source_path": "inner",
|
||||||
|
"project_definition": [
|
||||||
|
"inner",
|
||||||
|
{
|
||||||
|
"class_name": null,
|
||||||
|
"children": {},
|
||||||
|
"properties": {},
|
||||||
|
"ignore_unknown_instances": null,
|
||||||
|
"path": "inner"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "world",
|
||||||
|
"class_name": "ModuleScript",
|
||||||
|
"properties": {
|
||||||
|
"Source": {
|
||||||
|
"Type": "String",
|
||||||
|
"Value": "-- outer/world.lua"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"children": [],
|
||||||
|
"metadata": {
|
||||||
|
"ignore_unknown_instances": false,
|
||||||
|
"source_path": "outer/world.lua",
|
||||||
|
"project_definition": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"ignore_unknown_instances": false,
|
||||||
|
"source_path": "outer",
|
||||||
|
"project_definition": [
|
||||||
|
"nested-partitions",
|
||||||
|
{
|
||||||
|
"class_name": null,
|
||||||
|
"children": {
|
||||||
|
"inner": {
|
||||||
|
"class_name": null,
|
||||||
|
"children": {},
|
||||||
|
"properties": {},
|
||||||
|
"ignore_unknown_instances": null,
|
||||||
|
"path": "inner"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"ignore_unknown_instances": null,
|
||||||
|
"path": "outer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
1
test-projects/nested_partitions/inner/hello.lua
Normal file
1
test-projects/nested_partitions/inner/hello.lua
Normal file
@@ -0,0 +1 @@
|
|||||||
|
-- inner/hello.lua
|
||||||
1
test-projects/nested_partitions/outer/world.lua
Normal file
1
test-projects/nested_partitions/outer/world.lua
Normal file
@@ -0,0 +1 @@
|
|||||||
|
-- outer/world.lua
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
Key,Context,Example,Source,es-es,de
|
|
||||||
,ClickableGroup:BuilderGui:TextLabel,You got 22 hearts!,You got {1} hearts!,,
|
|
||||||
,,"Team ""Red"" wins!","Team ""{1}"" wins!","¡Gana el equipo ""{1}""!","¡Gana el equipo ""{1}""!"
|
|
||||||
,Frame:TextLabel,,"{1} killed {2}, with a {3}","{1} mató a {2} con
|
|
||||||
una escopeta","{1} mató a {2} con
|
|
||||||
una escopeta"
|
|
||||||
|
161
test-projects/single_partition_game/expected-snapshot.json
Normal file
161
test-projects/single_partition_game/expected-snapshot.json
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
{
|
||||||
|
"name": "single-sync-point",
|
||||||
|
"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": "Foo",
|
||||||
|
"class_name": "Folder",
|
||||||
|
"properties": {},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "foo",
|
||||||
|
"class_name": "StringValue",
|
||||||
|
"properties": {
|
||||||
|
"Value": {
|
||||||
|
"Type": "String",
|
||||||
|
"Value": "Hello world, from foo.txt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"children": [],
|
||||||
|
"metadata": {
|
||||||
|
"ignore_unknown_instances": false,
|
||||||
|
"source_path": "lib/foo.txt",
|
||||||
|
"project_definition": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "main",
|
||||||
|
"class_name": "ModuleScript",
|
||||||
|
"properties": {
|
||||||
|
"Source": {
|
||||||
|
"Type": "String",
|
||||||
|
"Value": "-- hello, from main"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"children": [],
|
||||||
|
"metadata": {
|
||||||
|
"ignore_unknown_instances": false,
|
||||||
|
"source_path": "lib/main.lua",
|
||||||
|
"project_definition": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"ignore_unknown_instances": false,
|
||||||
|
"source_path": "lib",
|
||||||
|
"project_definition": [
|
||||||
|
"Foo",
|
||||||
|
{
|
||||||
|
"class_name": null,
|
||||||
|
"children": {},
|
||||||
|
"properties": {},
|
||||||
|
"ignore_unknown_instances": null,
|
||||||
|
"path": "lib"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"ignore_unknown_instances": true,
|
||||||
|
"source_path": null,
|
||||||
|
"project_definition": [
|
||||||
|
"ReplicatedStorage",
|
||||||
|
{
|
||||||
|
"class_name": "ReplicatedStorage",
|
||||||
|
"children": {
|
||||||
|
"Foo": {
|
||||||
|
"class_name": null,
|
||||||
|
"children": {},
|
||||||
|
"properties": {},
|
||||||
|
"ignore_unknown_instances": null,
|
||||||
|
"path": "lib"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"ignore_unknown_instances": null,
|
||||||
|
"path": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"ignore_unknown_instances": true,
|
||||||
|
"source_path": null,
|
||||||
|
"project_definition": [
|
||||||
|
"single-sync-point",
|
||||||
|
{
|
||||||
|
"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": {
|
||||||
|
"Foo": {
|
||||||
|
"class_name": null,
|
||||||
|
"children": {},
|
||||||
|
"properties": {},
|
||||||
|
"ignore_unknown_instances": null,
|
||||||
|
"path": "lib"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"ignore_unknown_instances": null,
|
||||||
|
"path": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"ignore_unknown_instances": null,
|
||||||
|
"path": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
53
test-projects/single_partition_model/expected-snapshot.json
Normal file
53
test-projects/single_partition_model/expected-snapshot.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "test-model",
|
||||||
|
"class_name": "Folder",
|
||||||
|
"properties": {},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "main",
|
||||||
|
"class_name": "Script",
|
||||||
|
"properties": {
|
||||||
|
"Source": {
|
||||||
|
"Type": "String",
|
||||||
|
"Value": "local other = require(script.Parent.other)\n\nprint(other)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"children": [],
|
||||||
|
"metadata": {
|
||||||
|
"ignore_unknown_instances": false,
|
||||||
|
"source_path": "src/main.server.lua",
|
||||||
|
"project_definition": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "other",
|
||||||
|
"class_name": "ModuleScript",
|
||||||
|
"properties": {
|
||||||
|
"Source": {
|
||||||
|
"Type": "String",
|
||||||
|
"Value": "return \"Hello, world!\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"children": [],
|
||||||
|
"metadata": {
|
||||||
|
"ignore_unknown_instances": false,
|
||||||
|
"source_path": "src/other.lua",
|
||||||
|
"project_definition": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"ignore_unknown_instances": false,
|
||||||
|
"source_path": "src",
|
||||||
|
"project_definition": [
|
||||||
|
"test-model",
|
||||||
|
{
|
||||||
|
"class_name": null,
|
||||||
|
"children": {},
|
||||||
|
"properties": {},
|
||||||
|
"ignore_unknown_instances": null,
|
||||||
|
"path": "src"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
-- ReplicatedStorage/hello.lua
|
||||||
11
test-projects/transmute_partition/default.project.json
Normal file
11
test-projects/transmute_partition/default.project.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "transmute-partition",
|
||||||
|
"tree": {
|
||||||
|
"$className": "DataModel",
|
||||||
|
|
||||||
|
"ReplicatedStorage": {
|
||||||
|
"$className": "ReplicatedStorage",
|
||||||
|
"$path": "ReplicatedStorage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
test-projects/transmute_partition/expected-snapshot.json
Normal file
66
test-projects/transmute_partition/expected-snapshot.json
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"name": "transmute-partition",
|
||||||
|
"class_name": "DataModel",
|
||||||
|
"properties": {},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "ReplicatedStorage",
|
||||||
|
"class_name": "ReplicatedStorage",
|
||||||
|
"properties": {},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "hello",
|
||||||
|
"class_name": "ModuleScript",
|
||||||
|
"properties": {
|
||||||
|
"Source": {
|
||||||
|
"Type": "String",
|
||||||
|
"Value": "-- ReplicatedStorage/hello.lua"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"children": [],
|
||||||
|
"metadata": {
|
||||||
|
"ignore_unknown_instances": false,
|
||||||
|
"source_path": "ReplicatedStorage/hello.lua",
|
||||||
|
"project_definition": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"ignore_unknown_instances": false,
|
||||||
|
"source_path": "ReplicatedStorage",
|
||||||
|
"project_definition": [
|
||||||
|
"ReplicatedStorage",
|
||||||
|
{
|
||||||
|
"class_name": "ReplicatedStorage",
|
||||||
|
"children": {},
|
||||||
|
"properties": {},
|
||||||
|
"ignore_unknown_instances": null,
|
||||||
|
"path": "ReplicatedStorage"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"ignore_unknown_instances": true,
|
||||||
|
"source_path": null,
|
||||||
|
"project_definition": [
|
||||||
|
"transmute-partition",
|
||||||
|
{
|
||||||
|
"class_name": "DataModel",
|
||||||
|
"children": {
|
||||||
|
"ReplicatedStorage": {
|
||||||
|
"class_name": "ReplicatedStorage",
|
||||||
|
"children": {},
|
||||||
|
"properties": {},
|
||||||
|
"ignore_unknown_instances": null,
|
||||||
|
"path": "ReplicatedStorage"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"ignore_unknown_instances": null,
|
||||||
|
"path": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user