Track metadata in tree snapshot tests

This commit is contained in:
Lucien Greathouse 2019-03-13 11:37:13 -07:00
parent 2ed3388e93
commit 78ef26d56c
9 changed files with 999 additions and 469 deletions

View file

@ -0,0 +1,68 @@
mod test_util;
use std::path::Path;
use pretty_assertions::assert_eq;
use librojo::{
imfs::Imfs,
project::Project,
rbx_snapshot::{SnapshotContext, snapshot_project_tree},
};
use crate::test_util::{
snapshot::*,
};
macro_rules! generate_snapshot_tests {
($($name: ident),*) => {
$(
paste::item! {
#[test]
fn [<snapshot_ $name>]() {
let _ = env_logger::try_init();
let tests_folder = Path::new(env!("CARGO_MANIFEST_DIR")).join("../test-projects");
let project_folder = tests_folder.join(stringify!($name));
run_snapshot_test(&project_folder);
}
}
)*
};
}
generate_snapshot_tests!(
empty,
multi_partition_game,
nested_partitions,
single_partition_game,
single_partition_model,
transmute_partition
);
fn run_snapshot_test(path: &Path) {
println!("Running snapshot from project: {}", path.display());
let project = Project::load_fuzzy(path)
.expect("Couldn't load project file for snapshot test");
let mut imfs = Imfs::new();
imfs.add_roots_from_project(&project)
.expect("Could not add IMFS roots to snapshot project");
let context = SnapshotContext {
plugin_context: None,
};
let mut snapshot = snapshot_project_tree(&context, &imfs, &project)
.expect("Could not generate snapshot for snapshot test");
if let Some(snapshot) = snapshot.as_mut() {
anonymize_snapshot(path, snapshot);
}
match read_expected_snapshot(path) {
Some(expected_snapshot) => assert_eq!(snapshot, expected_snapshot),
None => write_expected_snapshot(path, &snapshot),
}
}

View file

@ -1,194 +0,0 @@
mod test_util;
use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
sync::Arc,
thread,
time::Duration,
};
use log::error;
use tempfile::{tempdir, TempDir};
use pretty_assertions::assert_eq;
use librojo::{
imfs::Imfs,
project::Project,
live_session::LiveSession,
rbx_snapshot::{SnapshotContext, snapshot_project_tree},
visualize::{VisualizeRbxTree, graphviz_to_svg},
};
use crate::test_util::{
copy_recursive,
snapshot::*,
tree::{read_tree_by_name, write_tree_by_name, trees_equal},
};
macro_rules! generate_snapshot_tests {
($($name: ident),*) => {
$(
paste::item! {
#[test]
fn [<snapshot_ $name>]() {
let _ = env_logger::try_init();
let tests_folder = Path::new(env!("CARGO_MANIFEST_DIR")).join("../test-projects");
let project_folder = tests_folder.join(stringify!($name));
run_snapshot_test(&project_folder);
}
}
)*
};
}
generate_snapshot_tests!(
empty,
multi_partition_game,
nested_partitions,
single_partition_game,
single_partition_model,
transmute_partition
);
#[test]
fn multi_partition_game() {
let _ = env_logger::try_init();
let source_path = project_path("multi_partition_game");
let (dir, live_session) = start_session(&source_path);
tree_step("initial", &live_session, &source_path);
let added_path = dir.path().join("a/added");
fs::create_dir_all(&added_path)
.expect("Couldn't create directory");
thread::sleep(Duration::from_millis(250));
tree_step("with_dir", &live_session, &source_path);
let moved_path = dir.path().join("b/added");
fs::rename(&added_path, &moved_path)
.expect("Couldn't rename directory");
thread::sleep(Duration::from_millis(250));
tree_step("with_moved_dir", &live_session, &source_path);
}
/// Find the path to the given test project relative to the manifest.
fn project_path(name: &str) -> PathBuf {
let mut path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../test-projects");
path.push(name);
path
}
/// Starts a new LiveSession for the project located at the given file path.
fn start_session(source_path: &Path) -> (TempDir, LiveSession) {
let dir = tempdir()
.expect("Couldn't create temporary directory");
copy_recursive(&source_path, dir.path())
.expect("Couldn't copy project to temporary directory");
let project = Arc::new(Project::load_fuzzy(dir.path())
.expect("Couldn't load project from temp directory"));
let live_session = LiveSession::new(Arc::clone(&project))
.expect("Couldn't start live session");
(dir, live_session)
}
/// Marks a 'step' in the test, which will snapshot the session's current
/// RbxTree object and compare it against the saved snapshot if it exists.
fn tree_step(step: &str, live_session: &LiveSession, source_path: &Path) {
let rbx_session = live_session.rbx_session.lock().unwrap();
let tree = rbx_session.get_tree();
match read_tree_by_name(source_path, step) {
Some(expected) => match trees_equal(&expected, tree) {
Ok(_) => {}
Err(e) => {
error!("Trees at step '{}' were not equal.\n{}", step, e);
let expected_gv = format!("{}", VisualizeRbxTree {
tree: &expected,
metadata: &HashMap::new(),
});
let actual_gv = format!("{}", VisualizeRbxTree {
tree: rbx_session.get_tree(),
metadata: &HashMap::new(),
});
let output_dir = PathBuf::from("failed-snapshots");
fs::create_dir_all(&output_dir)
.expect("Could not create failed-snapshots directory");
let expected_basename = format!("{}-{}-expected", live_session.root_project().name, step);
let actual_basename = format!("{}-{}-actual", live_session.root_project().name, step);
let mut expected_out = output_dir.join(expected_basename);
let mut actual_out = output_dir.join(actual_basename);
match (graphviz_to_svg(&expected_gv), graphviz_to_svg(&actual_gv)) {
(Some(expected_svg), Some(actual_svg)) => {
expected_out.set_extension("svg");
actual_out.set_extension("svg");
fs::write(&expected_out, expected_svg)
.expect("Couldn't write expected SVG");
fs::write(&actual_out, actual_svg)
.expect("Couldn't write actual SVG");
}
_ => {
expected_out.set_extension("gv");
actual_out.set_extension("gv");
fs::write(&expected_out, expected_gv)
.expect("Couldn't write expected GV");
fs::write(&actual_out, actual_gv)
.expect("Couldn't write actual GV");
}
}
error!("Output at {} and {}", expected_out.display(), actual_out.display());
panic!("Tree mismatch at step '{}'", step);
}
}
None => {
write_tree_by_name(source_path, step, tree);
}
}
}
fn run_snapshot_test(path: &Path) {
println!("Running snapshot from project: {}", path.display());
let project = Project::load_fuzzy(path)
.expect("Couldn't load project file for snapshot test");
let mut imfs = Imfs::new();
imfs.add_roots_from_project(&project)
.expect("Could not add IMFS roots to snapshot project");
let context = SnapshotContext {
plugin_context: None,
};
let mut snapshot = snapshot_project_tree(&context, &imfs, &project)
.expect("Could not generate snapshot for snapshot test");
if let Some(snapshot) = snapshot.as_mut() {
anonymize_snapshot(path, snapshot);
}
match read_expected_snapshot(path) {
Some(expected_snapshot) => assert_eq!(snapshot, expected_snapshot),
None => write_expected_snapshot(path, &snapshot),
}
}

View file

@ -1,3 +1,5 @@
#![allow(dead_code)]
use std::fs::{create_dir, copy};
use std::path::Path;
use std::io;

View file

@ -6,6 +6,7 @@ use std::{
use librojo::{
project::ProjectNode,
snapshot_reconciler::RbxSnapshotInstance,
rbx_session::MetadataPerInstance,
};
const SNAPSHOT_EXPECTED_NAME: &str = "expected-snapshot.json";
@ -19,15 +20,7 @@ const SNAPSHOT_EXPECTED_NAME: &str = "expected-snapshot.json";
/// We also need to sort children, since Rojo tends to enumerate the filesystem
/// in an unpredictable order.
pub fn anonymize_snapshot(project_folder_path: &Path, snapshot: &mut RbxSnapshotInstance) {
match snapshot.metadata.source_path.as_mut() {
Some(path) => *path = anonymize_path(project_folder_path, path),
None => {},
}
match snapshot.metadata.project_definition.as_mut() {
Some((_, project_node)) => anonymize_project_node(project_folder_path, project_node),
None => {},
}
anonymize_metadata(project_folder_path, &mut snapshot.metadata);
snapshot.children.sort_by(|a, b| a.partial_cmp(b).unwrap());
@ -36,6 +29,18 @@ pub fn anonymize_snapshot(project_folder_path: &Path, snapshot: &mut RbxSnapshot
}
}
pub fn anonymize_metadata(project_folder_path: &Path, metadata: &mut MetadataPerInstance) {
match metadata.source_path.as_mut() {
Some(path) => *path = anonymize_path(project_folder_path, path),
None => {},
}
match metadata.project_definition.as_mut() {
Some((_, project_node)) => anonymize_project_node(project_folder_path, project_node),
None => {},
}
}
pub fn anonymize_project_node(project_folder_path: &Path, project_node: &mut ProjectNode) {
match project_node.path.as_mut() {
Some(path) => *path = anonymize_path(project_folder_path, path),

View file

@ -1,7 +1,7 @@
//! Defines a mechanism to compare two RbxTree objects and generate a useful
//! diff if they aren't the same. These methods ignore IDs, which are randomly
//! generated whenever a tree is constructed anyways. This makes matching up
//! pairs of instances that should be the same pretty difficult.
//! pairs of instances that should be the same potentially difficult.
//!
//! It relies on a couple different ideas:
//! - Instances with the same name and class name are matched as the same
@ -12,26 +12,131 @@
use std::{
borrow::Cow,
collections::HashSet,
path::Path,
fs::{self, File},
collections::{HashMap, HashSet},
fmt,
fs::{self, File},
hash::Hash,
path::{Path, PathBuf},
};
use log::error;
use serde_derive::{Serialize, Deserialize};
use rbx_dom_weak::{RbxId, RbxTree};
pub fn read_tree_by_name(path: &Path, identifier: &str) -> Option<RbxTree> {
use librojo::{
rbx_session::MetadataPerInstance,
live_session::LiveSession,
visualize::{VisualizeRbxTree, graphviz_to_svg},
};
use super::snapshot::anonymize_metadata;
/// Marks a 'step' in the test, which will snapshot the session's current
/// RbxTree object and compare it against the saved snapshot if it exists.
pub fn tree_step(step: &str, live_session: &LiveSession, source_path: &Path) {
let rbx_session = live_session.rbx_session.lock().unwrap();
let tree = rbx_session.get_tree();
let project_folder = live_session.root_project().folder_location();
let metadata = rbx_session.get_all_instance_metadata()
.iter()
.map(|(key, meta)| {
let mut meta = meta.clone();
anonymize_metadata(project_folder, &mut meta);
(*key, meta)
})
.collect();
let tree_with_metadata = TreeWithMetadata {
tree: Cow::Borrowed(&tree),
metadata: Cow::Owned(metadata),
};
match read_tree_by_name(source_path, step) {
Some(expected) => match trees_equal(&expected, &tree_with_metadata) {
Ok(_) => {}
Err(e) => {
error!("Trees at step '{}' were not equal.\n{}", step, e);
let expected_gv = format!("{}", VisualizeRbxTree {
tree: &expected.tree,
metadata: &expected.metadata,
});
let actual_gv = format!("{}", VisualizeRbxTree {
tree: &tree_with_metadata.tree,
metadata: &tree_with_metadata.metadata,
});
let output_dir = PathBuf::from("failed-snapshots");
fs::create_dir_all(&output_dir)
.expect("Could not create failed-snapshots directory");
let expected_basename = format!("{}-{}-expected", live_session.root_project().name, step);
let actual_basename = format!("{}-{}-actual", live_session.root_project().name, step);
let mut expected_out = output_dir.join(expected_basename);
let mut actual_out = output_dir.join(actual_basename);
match (graphviz_to_svg(&expected_gv), graphviz_to_svg(&actual_gv)) {
(Some(expected_svg), Some(actual_svg)) => {
expected_out.set_extension("svg");
actual_out.set_extension("svg");
fs::write(&expected_out, expected_svg)
.expect("Couldn't write expected SVG");
fs::write(&actual_out, actual_svg)
.expect("Couldn't write actual SVG");
}
_ => {
expected_out.set_extension("gv");
actual_out.set_extension("gv");
fs::write(&expected_out, expected_gv)
.expect("Couldn't write expected GV");
fs::write(&actual_out, actual_gv)
.expect("Couldn't write actual GV");
}
}
error!("Output at {} and {}", expected_out.display(), actual_out.display());
panic!("Tree mismatch at step '{}'", step);
}
}
None => {
write_tree_by_name(source_path, step, &tree_with_metadata);
}
}
}
fn new_cow_map<K: Clone + Eq + Hash, V: Clone>() -> Cow<'static, HashMap<K, V>> {
Cow::Owned(HashMap::new())
}
#[derive(Debug, Serialize, Deserialize)]
struct TreeWithMetadata<'a> {
#[serde(flatten)]
pub tree: Cow<'a, RbxTree>,
#[serde(default = "new_cow_map")]
pub metadata: Cow<'a, HashMap<RbxId, MetadataPerInstance>>,
}
fn read_tree_by_name(path: &Path, identifier: &str) -> Option<TreeWithMetadata<'static>> {
let mut file_path = path.join(identifier);
file_path.set_extension("tree.json");
let contents = fs::read(&file_path).ok()?;
let tree: RbxTree = serde_json::from_slice(&contents)
let tree: TreeWithMetadata = serde_json::from_slice(&contents)
.expect("Could not deserialize tree");
Some(tree)
}
pub fn write_tree_by_name(path: &Path, identifier: &str, tree: &RbxTree) {
fn write_tree_by_name(path: &Path, identifier: &str, tree: &TreeWithMetadata) {
let mut file_path = path.join(identifier);
file_path.set_extension("tree.json");
@ -43,7 +148,7 @@ pub fn write_tree_by_name(path: &Path, identifier: &str, tree: &RbxTree) {
}
#[derive(Debug)]
pub struct TreeMismatch {
struct TreeMismatch {
pub path: Cow<'static, str>,
pub detail: Cow<'static, str>,
}
@ -74,27 +179,38 @@ impl fmt::Display for TreeMismatch {
}
}
pub fn trees_equal(left_tree: &RbxTree, right_tree: &RbxTree) -> Result<(), TreeMismatch> {
let left_id = left_tree.get_root_id();
let right_id = right_tree.get_root_id();
fn trees_equal(
left_tree: &TreeWithMetadata,
right_tree: &TreeWithMetadata,
) -> Result<(), TreeMismatch> {
let left_id = left_tree.tree.get_root_id();
let right_id = right_tree.tree.get_root_id();
basic_equal(left_tree, left_id, right_tree, right_id)?;
properties_equal(left_tree, left_id, right_tree, right_id)?;
children_equal(left_tree, left_id, right_tree, right_id)
.map_err(|e| {
let left_instance = left_tree.get_instance(left_id)
.expect("ID did not exist in left tree");
e.add_parent(&left_instance.name)
})
instances_equal(left_tree, left_id, right_tree, right_id)
}
fn basic_equal(left_tree: &RbxTree, left_id: RbxId, right_tree: &RbxTree, right_id: RbxId) -> Result<(), TreeMismatch> {
let left_instance = left_tree.get_instance(left_id)
fn instances_equal(
left_tree: &TreeWithMetadata,
left_id: RbxId,
right_tree: &TreeWithMetadata,
right_id: RbxId,
) -> Result<(), TreeMismatch> {
basic_equal(left_tree, left_id, right_tree, right_id)?;
properties_equal(left_tree, left_id, right_tree, right_id)?;
children_equal(left_tree, left_id, right_tree, right_id)?;
metadata_equal(left_tree, left_id, right_tree, right_id)
}
fn basic_equal(
left_tree: &TreeWithMetadata,
left_id: RbxId,
right_tree: &TreeWithMetadata,
right_id: RbxId,
) -> Result<(), TreeMismatch> {
let left_instance = left_tree.tree.get_instance(left_id)
.expect("ID did not exist in left tree");
let right_instance = right_tree.get_instance(right_id)
let right_instance = right_tree.tree.get_instance(right_id)
.expect("ID did not exist in right tree");
if left_instance.name != right_instance.name {
@ -112,11 +228,16 @@ fn basic_equal(left_tree: &RbxTree, left_id: RbxId, right_tree: &RbxTree, right_
Ok(())
}
fn properties_equal(left_tree: &RbxTree, left_id: RbxId, right_tree: &RbxTree, right_id: RbxId) -> Result<(), TreeMismatch> {
let left_instance = left_tree.get_instance(left_id)
fn properties_equal(
left_tree: &TreeWithMetadata,
left_id: RbxId,
right_tree: &TreeWithMetadata,
right_id: RbxId,
) -> Result<(), TreeMismatch> {
let left_instance = left_tree.tree.get_instance(left_id)
.expect("ID did not exist in left tree");
let right_instance = right_tree.get_instance(right_id)
let right_instance = right_tree.tree.get_instance(right_id)
.expect("ID did not exist in right tree");
let mut visited = HashSet::new();
@ -147,7 +268,7 @@ fn properties_equal(left_tree: &RbxTree, left_id: RbxId, right_tree: &RbxTree, r
if left_value != Some(right_value) {
let message = format!(
"Property {} was {:?} on left and {:?} on right",
"Property {}:\n\tLeft: {:?}\n\tRight: {:?}",
key,
left_value,
Some(right_value),
@ -160,11 +281,16 @@ fn properties_equal(left_tree: &RbxTree, left_id: RbxId, right_tree: &RbxTree, r
Ok(())
}
fn children_equal(left_tree: &RbxTree, left_id: RbxId, right_tree: &RbxTree, right_id: RbxId) -> Result<(), TreeMismatch> {
let left_instance = left_tree.get_instance(left_id)
fn children_equal(
left_tree: &TreeWithMetadata,
left_id: RbxId,
right_tree: &TreeWithMetadata,
right_id: RbxId,
) -> Result<(), TreeMismatch> {
let left_instance = left_tree.tree.get_instance(left_id)
.expect("ID did not exist in left tree");
let right_instance = right_tree.get_instance(right_id)
let right_instance = right_tree.tree.get_instance(right_id)
.expect("ID did not exist in right tree");
let left_children = left_instance.get_children_ids();
@ -175,15 +301,34 @@ fn children_equal(left_tree: &RbxTree, left_id: RbxId, right_tree: &RbxTree, rig
}
for (left_child_id, right_child_id) in left_children.iter().zip(right_children) {
basic_equal(left_tree, *left_child_id, right_tree, *right_child_id)
.map_err(|e| e.add_parent(&left_instance.name))?;
properties_equal(left_tree, *left_child_id, right_tree, *right_child_id)
.map_err(|e| e.add_parent(&left_instance.name))?;
children_equal(left_tree, *left_child_id, right_tree, *right_child_id)
instances_equal(left_tree, *left_child_id, right_tree, *right_child_id)
.map_err(|e| e.add_parent(&left_instance.name))?;
}
Ok(())
}
fn metadata_equal(
left_tree: &TreeWithMetadata,
left_id: RbxId,
right_tree: &TreeWithMetadata,
right_id: RbxId,
) -> Result<(), TreeMismatch> {
let left_meta = left_tree.metadata.get(&left_id);
let right_meta = right_tree.metadata.get(&right_id);
if left_meta != right_meta {
let left_instance = left_tree.tree.get_instance(left_id)
.expect("Left instance didn't exist in tree");
let message = format!(
"Metadata mismatch:\n\tLeft: {:?}\n\tRight: {:?}",
left_meta,
right_meta,
);
return Err(TreeMismatch::new(&left_instance.name, message));
}
Ok(())
}

View file

@ -0,0 +1,68 @@
mod test_util;
use std::{
fs,
path::{Path, PathBuf},
sync::Arc,
thread,
time::Duration,
};
use tempfile::{tempdir, TempDir};
use librojo::{
live_session::LiveSession,
project::Project,
};
use crate::test_util::{
copy_recursive,
tree::tree_step,
};
#[test]
fn multi_partition_game() {
let _ = env_logger::try_init();
let source_path = project_path("multi_partition_game");
let (dir, live_session) = start_session(&source_path);
tree_step("initial", &live_session, &source_path);
let added_path = dir.path().join("a/added");
fs::create_dir_all(&added_path)
.expect("Couldn't create directory");
thread::sleep(Duration::from_millis(250));
tree_step("with_dir", &live_session, &source_path);
let moved_path = dir.path().join("b/added");
fs::rename(&added_path, &moved_path)
.expect("Couldn't rename directory");
thread::sleep(Duration::from_millis(250));
tree_step("with_moved_dir", &live_session, &source_path);
}
/// Find the path to the given test project relative to the manifest.
fn project_path(name: &str) -> PathBuf {
let mut path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../test-projects");
path.push(name);
path
}
/// Starts a new LiveSession for the project located at the given file path.
fn start_session(source_path: &Path) -> (TempDir, LiveSession) {
let dir = tempdir()
.expect("Couldn't create temporary directory");
copy_recursive(&source_path, dir.path())
.expect("Couldn't copy project to temporary directory");
let project = Arc::new(Project::load_fuzzy(dir.path())
.expect("Couldn't load project from temp directory"));
let live_session = LiveSession::new(Arc::clone(&project))
.expect("Couldn't start live session");
(dir, live_session)
}