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)",
]
[[package]]
name = "difference"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "dtoa"
version = "0.4.3"
@@ -932,6 +937,26 @@ dependencies = [
"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]]
name = "percent-encoding"
version = "1.0.1"
@@ -977,6 +1002,25 @@ name = "pkg-config"
version = "0.3.14"
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]]
name = "proc-macro2"
version = "0.4.26"
@@ -1258,6 +1302,8 @@ dependencies = [
"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)",
"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)",
"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)",
@@ -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-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 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 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"
@@ -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 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 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 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_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 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 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"

View File

@@ -28,6 +28,9 @@ log = "0.4"
maplit = "1.0.1"
notify = "4.0"
rand = "0.4"
rbx_binary = "0.2.0"
rbx_tree = "0.2.0"
rbx_xml = "0.2.0"
regex = "1.0"
reqwest = "0.9.5"
rouille = "2.1"
@@ -35,11 +38,10 @@ serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
uuid = { version = "0.7", features = ["v4", "serde"] }
rbx_tree = "0.2.0"
rbx_xml = "0.2.0"
rbx_binary = "0.2.0"
[dev-dependencies]
tempfile = "3.0"
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();
for root_path in imfs.get_roots() {
trace!("Watching path {}", root_path.display());
watcher.watch(root_path, RecursiveMode::Recursive)
.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);
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> {
match project_node {
ProjectNode::Instance(node) => {
for child in node.children.values() {
add_sync_points(imfs, child)?;
}
},
ProjectNode::SyncPoint(node) => {
imfs.add_root(&node.path)?;
},
fn add_sync_points(imfs: &mut Imfs, node: &ProjectNode) -> Result<(), FsError> {
if let Some(path) = &node.path {
imfs.add_root(path)?;
}
for child in node.children.values() {
add_sync_points(imfs, child)?;
}
Ok(())

View File

@@ -6,12 +6,13 @@ pub mod impl_from;
pub mod commands;
pub mod fs_watcher;
pub mod imfs;
pub mod live_session;
pub mod message_queue;
pub mod path_map;
pub mod path_serializer;
pub mod project;
pub mod rbx_session;
pub mod rbx_snapshot;
pub mod live_session;
pub mod session_id;
pub mod snapshot_reconciler;
pub mod visualize;

View File

@@ -1,5 +1,4 @@
use std::{
collections::hash_map,
path::{self, Path, PathBuf},
collections::{HashMap, HashSet},
};
@@ -36,12 +35,6 @@ impl<T> PathMap<T> {
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) {
if let Some(parent_path) = path.parent() {
if let Some(parent) = self.nodes.get_mut(parent_path) {
@@ -116,28 +109,4 @@ impl<T> PathMap<T> {
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 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
/// want to do things like transforming paths to be absolute before handing them
/// 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
/// Rojo.
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
enum SourceProjectNode {
Instance {
#[serde(rename = "$className")]
class_name: String,
struct SourceProjectNode {
#[serde(rename = "$className", skip_serializing_if = "Option::is_none")]
class_name: Option<String>,
#[serde(rename = "$properties", default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
properties: HashMap<String, RbxValue>,
#[serde(rename = "$properties", default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
properties: HashMap<String, RbxValue>,
#[serde(rename = "$ignoreUnknownInstances", default = "yeah", skip_serializing_if = "is_true")]
ignore_unknown_instances: bool,
#[serde(rename = "$ignoreUnknownInstances", skip_serializing_if = "Option::is_none")]
ignore_unknown_instances: Option<bool>,
#[serde(flatten)]
children: HashMap<String, SourceProjectNode>,
},
SyncPoint {
#[serde(rename = "$path")]
path: String,
}
#[serde(rename = "$path", skip_serializing_if = "Option::is_none")]
path: Option<String>,
#[serde(flatten)]
children: HashMap<String, SourceProjectNode>,
}
impl SourceProjectNode {
/// Consumes the SourceProjectNode and turns it into a ProjectNode.
pub fn into_project_node(self, project_file_location: &Path) -> ProjectNode {
match self {
SourceProjectNode::Instance { class_name, mut children, properties, ignore_unknown_instances } => {
let mut new_children = HashMap::new();
pub fn into_project_node(mut self, project_file_location: &Path) -> ProjectNode {
let children = self.children.drain()
.map(|(key, value)| (key, value.into_project_node(project_file_location)))
.collect();
for (node_name, node) in children.drain() {
new_children.insert(node_name, node.into_project_node(project_file_location));
}
// Make sure that paths are absolute, transforming them by adding the
// 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 {
class_name,
children: new_children,
properties,
metadata: InstanceProjectNodeMetadata {
ignore_unknown_instances,
},
})
},
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,
})
},
ProjectNode {
class_name: self.class_name,
properties: self.properties,
ignore_unknown_instances: self.ignore_unknown_instances,
path,
children,
}
}
}
@@ -177,75 +155,49 @@ pub enum ProjectSaveError {
IoError(#[fail(cause)] io::Error),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InstanceProjectNodeMetadata {
pub ignore_unknown_instances: bool,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct ProjectNode {
pub class_name: Option<String>,
pub children: HashMap<String, ProjectNode>,
pub properties: HashMap<String, RbxValue>,
pub ignore_unknown_instances: Option<bool>,
impl Default for InstanceProjectNodeMetadata {
fn default() -> InstanceProjectNodeMetadata {
InstanceProjectNodeMetadata {
ignore_unknown_instances: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ProjectNode {
Instance(InstanceProjectNode),
SyncPoint(SyncPointProjectNode),
#[serde(serialize_with = "crate::path_serializer::serialize_option")]
pub path: Option<PathBuf>,
}
impl ProjectNode {
fn to_source_node(&self, project_file_location: &Path) -> SourceProjectNode {
match self {
ProjectNode::Instance(node) => {
let mut children = HashMap::new();
let children = self.children.iter()
.map(|(key, value)| (key.clone(), value.to_source_node(project_file_location)))
.collect();
for (key, child) in &node.children {
children.insert(key.clone(), child.to_source_node(project_file_location));
}
// If paths are relative to the project file, transform them to look
// 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 {
class_name: node.class_name.clone(),
children,
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();
match path.strip_prefix(project_folder_location) {
Ok(stripped) => stripped.to_str().unwrap().replace("\\", "/"),
Err(_) => format!("{}", path.display()),
}
});
let friendly_path = match sync_node.path.strip_prefix(project_folder_location) {
Ok(stripped) => stripped.to_str().unwrap().replace("\\", "/"),
Err(_) => format!("{}", sync_node.path.display()),
};
SourceProjectNode::SyncPoint {
path: friendly_path,
}
},
SourceProjectNode {
class_name: self.class_name.clone(),
properties: self.properties.clone(),
ignore_unknown_instances: self.ignore_unknown_instances,
children,
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)]
pub struct Project {
pub name: String,
@@ -265,33 +217,31 @@ impl Project {
project_fuzzy_path.file_name().unwrap().to_str().unwrap()
};
let tree = ProjectNode::Instance(InstanceProjectNode {
class_name: "DataModel".to_string(),
let tree = ProjectNode {
class_name: Some(String::from("DataModel")),
children: hashmap! {
String::from("ReplicatedStorage") => ProjectNode::Instance(InstanceProjectNode {
class_name: String::from("ReplicatedStorage"),
String::from("ReplicatedStorage") => ProjectNode {
class_name: Some(String::from("ReplicatedStorage")),
children: hashmap! {
String::from("Source") => ProjectNode::SyncPoint(SyncPointProjectNode {
path: project_folder_path.join("src"),
}),
String::from("Source") => ProjectNode {
path: Some(project_folder_path.join("src")),
..Default::default()
},
},
properties: HashMap::new(),
metadata: Default::default(),
}),
String::from("HttpService") => ProjectNode::Instance(InstanceProjectNode {
class_name: String::from("HttpService"),
children: HashMap::new(),
..Default::default()
},
String::from("HttpService") => ProjectNode {
class_name: Some(String::from("HttpService")),
properties: hashmap! {
String::from("HttpEnabled") => RbxValue::Bool {
value: true,
},
},
metadata: Default::default(),
}),
..Default::default()
},
},
properties: HashMap::new(),
metadata: Default::default(),
});
..Default::default()
};
let project = Project {
name: project_name.to_string(),
@@ -316,9 +266,10 @@ impl Project {
project_fuzzy_path.file_name().unwrap().to_str().unwrap()
};
let tree = ProjectNode::SyncPoint(SyncPointProjectNode {
path: project_folder_path.join("src"),
});
let tree = ProjectNode {
path: Some(project_folder_path.join("src")),
..Default::default()
};
let project = Project {
name: project_name.to_string(),

View File

@@ -1,6 +1,6 @@
use std::{
borrow::Cow,
collections::HashMap,
collections::{HashSet, HashMap},
path::{Path, PathBuf},
str,
sync::{Arc, Mutex},
@@ -11,11 +11,11 @@ use log::{info, trace};
use rbx_tree::{RbxTree, RbxId};
use crate::{
project::Project,
project::{Project, ProjectNode},
message_queue::MessageQueue,
imfs::{Imfs, ImfsItem},
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},
};
@@ -23,24 +23,28 @@ const INIT_SCRIPT: &str = "init.lua";
const INIT_SERVER_SCRIPT: &str = "init.server.lua";
const INIT_CLIENT_SCRIPT: &str = "init.client.lua";
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MetadataPerPath {
pub instance_id: Option<RbxId>,
pub instance_name: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
/// `source_path` or `project_definition` or both must both be Some.
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct MetadataPerInstance {
pub source_path: Option<PathBuf>,
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 {
tree: RbxTree,
// TODO(#105): Change metadata_per_path to PathMap<Vec<MetadataPerPath>> for
// path aliasing.
metadata_per_path: PathMap<MetadataPerPath>,
instances_per_path: PathMap<HashSet<RbxId>>,
metadata_per_instance: HashMap<RbxId, MetadataPerInstance>,
message_queue: Arc<MessageQueue<InstanceChanges>>,
imfs: Arc<Mutex<Imfs>>,
@@ -52,17 +56,17 @@ impl RbxSession {
imfs: Arc<Mutex<Imfs>>,
message_queue: Arc<MessageQueue<InstanceChanges>>,
) -> RbxSession {
let mut metadata_per_path = PathMap::new();
let mut instances_per_path = PathMap::new();
let mut metadata_per_instance = HashMap::new();
let tree = {
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 {
tree,
metadata_per_path,
instances_per_path,
metadata_per_instance,
message_queue,
imfs,
@@ -80,7 +84,7 @@ impl RbxSession {
.expect("Path was outside in-memory filesystem roots");
// 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
// should snapshot its parent instead.
@@ -93,42 +97,44 @@ impl RbxSession {
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
.expect("Instance did not exist in tree");
let maybe_snapshot = match &instance_metadata.project_definition {
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
// per-path metadata store.
let instance_name = path_metadata.instance_name.as_ref()
.map(|value| Cow::Owned(value.to_owned()));
let snapshot = match maybe_snapshot {
Some(snapshot) => snapshot,
None => {
trace!("Path resulted in no snapshot being generated.");
return;
},
};
let mut context = SnapshotContext {
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()));
trace!("Snapshot: {:#?}", snapshot);
let snapshot = match maybe_snapshot {
Some(snapshot) => snapshot,
None => {
trace!("Path resulted in no snapshot being generated.");
return;
},
};
trace!("Snapshot: {:#?}", snapshot);
reconcile_subtree(
&mut self.tree,
instance_id,
&snapshot,
&mut self.metadata_per_path,
&mut self.metadata_per_instance,
&mut changes,
);
reconcile_subtree(
&mut self.tree,
*instance_id,
&snapshot,
&mut self.instances_per_path,
&mut self.metadata_per_instance,
&mut changes,
);
}
}
if changes.is_empty() {
@@ -170,13 +176,13 @@ impl RbxSession {
pub fn path_removed(&mut self, path: &Path) {
info!("Path removed: {}", path.display());
self.metadata_per_path.remove(path);
self.instances_per_path.remove(path);
self.path_created_or_updated(path);
}
pub fn path_renamed(&mut self, from_path: &Path, to_path: &Path) {
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(to_path);
}
@@ -188,33 +194,26 @@ impl RbxSession {
pub fn get_instance_metadata(&self, id: RbxId) -> Option<&MetadataPerInstance> {
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 {
let mut metadata_per_path = PathMap::new();
let mut instances_per_path = PathMap::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(
project: &Project,
imfs: &Imfs,
metadata_per_path: &mut PathMap<MetadataPerPath>,
instances_per_path: &mut PathMap<HashSet<RbxId>>,
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
) -> RbxTree {
let mut context = SnapshotContext {
metadata_per_path,
};
let snapshot = snapshot_project_tree(imfs, &mut context, project)
let snapshot = snapshot_project_tree(imfs, project)
.expect("Could not snapshot project tree")
.expect("Project did not produce any instances");
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
}

View File

@@ -22,16 +22,13 @@ use crate::{
project::{
Project,
ProjectNode,
InstanceProjectNode,
SyncPointProjectNode,
},
snapshot_reconciler::{
RbxSnapshotInstance,
snapshot_from_tree,
},
path_map::PathMap,
// TODO: Move MetadataPerPath into this module?
rbx_session::{MetadataPerPath, MetadataPerInstance},
// TODO: Move MetadataPerInstance into this module?
rbx_session::MetadataPerInstance,
};
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 struct SnapshotContext<'meta> {
pub metadata_per_path: &'meta mut PathMap<MetadataPerPath>,
}
#[derive(Debug, Fail)]
pub enum SnapshotError {
DidNotExist(PathBuf),
@@ -55,6 +48,7 @@ pub enum SnapshotError {
},
JsonModelDecodeError {
#[fail(cause)]
inner: serde_json::Error,
path: PathBuf,
},
@@ -68,6 +62,12 @@ pub enum SnapshotError {
inner: rbx_binary::DecodeError,
path: PathBuf,
},
ProjectNodeUnusable,
ProjectNodeInvalidTransmute {
partition_path: PathBuf,
},
}
impl fmt::Display for SnapshotError {
@@ -78,7 +78,7 @@ impl fmt::Display for SnapshotError {
write!(output, "Invalid UTF-8: {} in path {}", inner, path.display())
},
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 } => {
write!(output, "Malformed rbxmx model: {:?} in path {}", inner, path.display())
@@ -86,107 +86,131 @@ impl fmt::Display for SnapshotError {
SnapshotError::BinaryModelDecodeError { inner, path } => {
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>(
imfs: &'source Imfs,
context: &mut SnapshotContext,
project: &'source Project,
) -> 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,
context: &mut SnapshotContext,
node: &'source ProjectNode,
node: &ProjectNode,
instance_name: Cow<'source, str>,
) -> SnapshotResult<'source> {
match node {
ProjectNode::Instance(instance_node) => snapshot_instance_node(imfs, context, instance_node, instance_name),
ProjectNode::SyncPoint(sync_node) => snapshot_sync_point_node(imfs, context, sync_node, instance_name),
}
}
let maybe_snapshot = match &node.path {
Some(path) => snapshot_imfs_path(imfs, &path, Some(instance_name.clone()))?,
None => match &node.class_name {
Some(_class_name) => Some(RbxSnapshotInstance {
name: instance_name.clone(),
fn snapshot_instance_node<'source>(
imfs: &'source Imfs,
context: &mut SnapshotContext,
node: &'source InstanceProjectNode,
instance_name: Cow<'source, str>,
) -> SnapshotResult<'source> {
let mut children = Vec::new();
for (child_name, child_project_node) in &node.children {
if let Some(child) = snapshot_project_node(imfs, context, child_project_node, Cow::Borrowed(child_name))? {
children.push(child);
}
}
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,
// These properties are replaced later in the function to
// reduce code duplication.
class_name: Cow::Borrowed("Folder"),
properties: HashMap::new(),
children: Vec::new(),
metadata: MetadataPerInstance {
source_path: None,
ignore_unknown_instances: true,
project_definition: None,
},
}),
None => {
return Err(SnapshotError::ProjectNodeUnusable);
},
},
}))
}
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
// 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,
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.
let path_meta = context.metadata_per_path.entry(node.path.to_owned()).or_default();
path_meta.instance_name = Some(snapshot.name.clone().into_owned());
// Applies the class name specified in `class_name` from the project, if it's
// set.
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))
}
pub fn snapshot_imfs_path<'source>(
imfs: &'source Imfs,
context: &mut SnapshotContext,
path: &Path,
instance_name: Option<Cow<'source, str>>,
) -> SnapshotResult<'source> {
// If the given path doesn't exist in the in-memory filesystem, we consider
// that an error.
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())),
}
}
fn snapshot_imfs_item<'source>(
imfs: &'source Imfs,
context: &mut SnapshotContext,
item: &'source ImfsItem,
instance_name: Option<Cow<'source, str>>,
) -> SnapshotResult<'source> {
match item {
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>(
imfs: &'source Imfs,
context: &mut SnapshotContext,
directory: &'source ImfsDirectory,
instance_name: Option<Cow<'source, str>>,
) -> SnapshotResult<'source> {
@@ -202,11 +226,11 @@ fn snapshot_imfs_directory<'source>(
});
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) {
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) {
snapshot_imfs_path(imfs, context, &init_client_path, Some(snapshot_name))?.unwrap()
snapshot_imfs_path(imfs, &init_client_path, Some(snapshot_name))?.unwrap()
} else {
RbxSnapshotInstance {
class_name: Cow::Borrowed("Folder"),
@@ -216,6 +240,7 @@ fn snapshot_imfs_directory<'source>(
metadata: MetadataPerInstance {
source_path: None,
ignore_unknown_instances: false,
project_definition: None,
},
}
};
@@ -234,7 +259,7 @@ fn snapshot_imfs_directory<'source>(
// 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);
}
},
@@ -316,6 +341,7 @@ fn snapshot_lua_file<'source>(
metadata: MetadataPerInstance {
source_path: Some(file.path.to_path_buf()),
ignore_unknown_instances: false,
project_definition: None,
},
}))
}
@@ -354,6 +380,7 @@ fn snapshot_txt_file<'source>(
metadata: MetadataPerInstance {
source_path: Some(file.path.to_path_buf()),
ignore_unknown_instances: false,
project_definition: None,
},
}))
}
@@ -387,6 +414,7 @@ fn snapshot_csv_file<'source>(
metadata: MetadataPerInstance {
source_path: Some(file.path.to_path_buf()),
ignore_unknown_instances: false,
project_definition: None,
},
}))
}

View File

@@ -1,8 +1,9 @@
use std::{
str,
borrow::Cow,
cmp::Ordering,
collections::{HashMap, HashSet},
fmt,
str,
};
use rbx_tree::{RbxTree, RbxId, RbxInstanceProperties, RbxValue};
@@ -10,7 +11,7 @@ use serde_derive::{Serialize, Deserialize};
use crate::{
path_map::PathMap,
rbx_session::{MetadataPerPath, MetadataPerInstance},
rbx_session::MetadataPerInstance,
};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
@@ -55,7 +56,7 @@ impl InstanceChanges {
}
}
#[derive(Debug)]
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct RbxSnapshotInstance<'a> {
pub name: Cow<'a, str>,
pub class_name: Cow<'a, str>,
@@ -64,6 +65,13 @@ pub struct RbxSnapshotInstance<'a> {
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>> {
let instance = tree.get_instance(id)?;
@@ -80,31 +88,27 @@ pub fn snapshot_from_tree(tree: &RbxTree, id: RbxId) -> Option<RbxSnapshotInstan
metadata: MetadataPerInstance {
source_path: None,
ignore_unknown_instances: false,
project_definition: None,
},
})
}
pub fn reify_root(
snapshot: &RbxSnapshotInstance,
metadata_per_path: &mut PathMap<MetadataPerPath>,
instance_metadata_map: &mut HashMap<RbxId, MetadataPerInstance>,
instance_per_path: &mut PathMap<HashSet<RbxId>>,
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
changes: &mut InstanceChanges,
) -> RbxTree {
let instance = reify_core(snapshot);
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 {
let path_meta = metadata_per_path.entry(source_path.to_owned()).or_default();
path_meta.instance_id = Some(root_id);
}
reify_metadata(snapshot, id, instance_per_path, metadata_per_instance);
instance_metadata_map.insert(root_id, snapshot.metadata.clone());
changes.added.insert(root_id);
changes.added.insert(id);
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
@@ -114,47 +118,58 @@ pub fn reify_subtree(
snapshot: &RbxSnapshotInstance,
tree: &mut RbxTree,
parent_id: RbxId,
metadata_per_path: &mut PathMap<MetadataPerPath>,
instance_metadata_map: &mut HashMap<RbxId, MetadataPerInstance>,
instance_per_path: &mut PathMap<HashSet<RbxId>>,
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
changes: &mut InstanceChanges,
) {
let instance = reify_core(snapshot);
let id = tree.insert_instance(instance, parent_id);
if let Some(source_path) = &snapshot.metadata.source_path {
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());
reify_metadata(snapshot, id, instance_per_path, metadata_per_instance);
changes.added.insert(id);
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(
tree: &mut RbxTree,
id: RbxId,
snapshot: &RbxSnapshotInstance,
metadata_per_path: &mut PathMap<MetadataPerPath>,
instance_metadata_map: &mut HashMap<RbxId, MetadataPerInstance>,
instance_per_path: &mut PathMap<HashSet<RbxId>>,
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
changes: &mut InstanceChanges,
) {
if let Some(source_path) = &snapshot.metadata.source_path {
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());
reify_metadata(snapshot, id, instance_per_path, metadata_per_instance);
if reconcile_instance_properties(tree.get_instance_mut(id).unwrap(), snapshot) {
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 {
@@ -234,8 +249,8 @@ fn reconcile_instance_children(
tree: &mut RbxTree,
id: RbxId,
snapshot: &RbxSnapshotInstance,
metadata_per_path: &mut PathMap<MetadataPerPath>,
instance_metadata_map: &mut HashMap<RbxId, MetadataPerInstance>,
instance_per_path: &mut PathMap<HashSet<RbxId>>,
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
changes: &mut InstanceChanges,
) {
let mut visited_snapshot_indices = HashSet::new();
@@ -287,19 +302,19 @@ fn reconcile_instance_children(
}
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 {
if let Some(subtree) = tree.remove_instance(*child_id) {
for id in subtree.iter_all_ids() {
instance_metadata_map.remove(&id);
metadata_per_instance.remove(&id);
changes.removed.insert(id);
}
}
}
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::{
imfs::{Imfs, ImfsItem},
rbx_session::RbxSession,
web::InstanceMetadata,
web::PublicInstanceMetadata,
};
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);
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_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.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InstanceMetadata {
pub struct PublicInstanceMetadata {
ignore_unknown_instances: bool,
}
impl InstanceMetadata {
pub fn from_session_metadata(meta: &MetadataPerInstance) -> InstanceMetadata {
InstanceMetadata {
impl PublicInstanceMetadata {
pub fn from_session_metadata(meta: &MetadataPerInstance) -> PublicInstanceMetadata {
PublicInstanceMetadata {
ignore_unknown_instances: meta.ignore_unknown_instances,
}
}
@@ -50,7 +50,7 @@ pub struct InstanceWithMetadata<'a> {
pub instance: Cow<'a, RbxInstance>,
#[serde(rename = "Metadata")]
pub metadata: Option<InstanceMetadata>,
pub metadata: Option<PublicInstanceMetadata>,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -120,9 +120,6 @@ impl Server {
(GET) (/visualize/imfs) => {
self.handle_visualize_imfs()
},
(GET) (/visualize/path_metadata) => {
self.handle_visualize_path_metadata()
},
_ => Response::empty_404()
)
}
@@ -209,7 +206,7 @@ impl Server {
for &requested_id in &requested_ids {
if let Some(instance) = tree.get_instance(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 {
instance: Cow::Borrowed(instance),
@@ -218,7 +215,7 @@ impl Server {
for descendant in tree.descendants(requested_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 {
instance: Cow::Borrowed(descendant),
@@ -254,9 +251,4 @@ impl Server {
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;
extern crate librojo;
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use pretty_assertions::assert_eq;
use rbx_tree::RbxValue;
use librojo::{
project::{Project, ProjectNode, InstanceProjectNode, SyncPointProjectNode},
project::{Project, ProjectNode},
};
lazy_static! {
@@ -44,54 +43,52 @@ fn empty_fuzzy_folder() {
}
#[test]
fn single_sync_point() {
let project_file_location = TEST_PROJECTS_ROOT.join("single-sync-point/default.project.json");
let project = Project::load_exact(&project_file_location).unwrap();
fn single_partition_game() {
let project_location = TEST_PROJECTS_ROOT.join("single_partition_game");
let project = Project::load_fuzzy(&project_location).unwrap();
let expected_project = {
let foo = ProjectNode::SyncPoint(SyncPointProjectNode {
path: project_file_location.parent().unwrap().join("lib"),
});
let foo = ProjectNode {
path: Some(project_location.join("lib")),
..Default::default()
};
let mut replicated_storage_children = HashMap::new();
replicated_storage_children.insert("Foo".to_string(), foo);
let replicated_storage = ProjectNode::Instance(InstanceProjectNode {
class_name: "ReplicatedStorage".to_string(),
let replicated_storage = ProjectNode {
class_name: Some(String::from("ReplicatedStorage")),
children: replicated_storage_children,
properties: HashMap::new(),
metadata: Default::default(),
});
..Default::default()
};
let mut http_service_properties = HashMap::new();
http_service_properties.insert("HttpEnabled".to_string(), RbxValue::Bool {
value: true,
});
let http_service = ProjectNode::Instance(InstanceProjectNode {
class_name: "HttpService".to_string(),
children: HashMap::new(),
let http_service = ProjectNode {
class_name: Some(String::from("HttpService")),
properties: http_service_properties,
metadata: Default::default(),
});
..Default::default()
};
let mut root_children = HashMap::new();
root_children.insert("ReplicatedStorage".to_string(), replicated_storage);
root_children.insert("HttpService".to_string(), http_service);
let root_node = ProjectNode::Instance(InstanceProjectNode {
class_name: "DataModel".to_string(),
let root_node = ProjectNode {
class_name: Some(String::from("DataModel")),
children: root_children,
properties: HashMap::new(),
metadata: Default::default(),
});
..Default::default()
};
Project {
name: "single-sync-point".to_string(),
tree: root_node,
serve_port: 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]
fn test_model() {
let project_file_location = TEST_PROJECTS_ROOT.join("test-model/default.project.json");
let project = Project::load_exact(&project_file_location).unwrap();
fn single_partition_model() {
let project_file_location = TEST_PROJECTS_ROOT.join("single_partition_model");
let project = Project::load_fuzzy(&project_file_location).unwrap();
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
}
]
}
}