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:
Lucien Greathouse
2019-02-07 14:55:01 -08:00
committed by GitHub
parent 38e3c198f2
commit ecb9b5e28f
37 changed files with 999 additions and 396 deletions

51
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
View 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");
}

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

View File

@@ -0,0 +1,9 @@
{
"name": "nested-partitions",
"tree": {
"$path": "outer",
"inner": {
"$path": "inner"
}
}
}

View 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"
}
]
}
}

View File

@@ -0,0 +1 @@
-- inner/hello.lua

View File

@@ -0,0 +1 @@
-- outer/world.lua

View File

@@ -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"
1 Key Context Example Source es-es de
2 ClickableGroup:BuilderGui:TextLabel You got 22 hearts! You got {1} hearts!
3 Team "Red" wins! Team "{1}" wins! ¡Gana el equipo "{1}"! ¡Gana el equipo "{1}"!
4 Frame:TextLabel {1} killed {2}, with a {3} {1} mató a {2} con una escopeta {1} mató a {2} con una escopeta

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

View 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"
}
]
}
}

View File

@@ -0,0 +1 @@
-- ReplicatedStorage/hello.lua

View File

@@ -0,0 +1,11 @@
{
"name": "transmute-partition",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"$path": "ReplicatedStorage"
}
}
}

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