rojo/src/snapshot/tests/compute.rs
Joe c181172460 Refactor test cases and improve snapshot handling
- Simplified the formatting of function calls in `encodePatchUpdate.spec.lua` for better readability.
- Added multiple new snapshot files to enhance end-to-end testing for reference path changes, including scenarios for adding, removing, and handling nonexistent attributes.
- Updated test cases in `apply.rs`, `compute.rs`, and `api.rs` to ensure consistent handling of reference properties and improve clarity in property updates.

These changes enhance the maintainability of the test suite and improve the robustness of snapshot testing.
2026-02-14 11:36:48 -06:00

495 lines
15 KiB
Rust

use std::borrow::Cow;
use insta::assert_yaml_snapshot;
use rbx_dom_weak::{types::Ref, ustr, UstrMap};
use rojo_insta_ext::RedactionMap;
use crate::snapshot::{compute_patch_set, InstanceSnapshot, RojoTree};
#[test]
fn set_name_and_class_name() {
let mut redactions = RedactionMap::default();
let tree = empty_tree();
redactions.intern(tree.get_root_id());
let snapshot = InstanceSnapshot {
snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("Some Folder"),
class_name: ustr("Folder"),
properties: Default::default(),
children: Vec::new(),
};
let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value);
}
#[test]
fn set_property() {
let mut redactions = RedactionMap::default();
let tree = empty_tree();
redactions.intern(tree.get_root_id());
let snapshot = InstanceSnapshot {
snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("ROOT"),
class_name: ustr("ROOT"),
properties: UstrMap::from_iter([(ustr("PropertyName"), "Hello, world!".into())]),
children: Vec::new(),
};
let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value);
}
#[test]
fn remove_property() {
let mut redactions = RedactionMap::default();
let mut tree = empty_tree();
redactions.intern(tree.get_root_id());
{
let root_id = tree.get_root_id();
let mut root_instance = tree.get_instance_mut(root_id).unwrap();
root_instance
.properties_mut()
.insert(ustr("Foo"), "This should be removed by the patch.".into());
}
let snapshot = InstanceSnapshot {
snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("ROOT"),
class_name: ustr("ROOT"),
properties: Default::default(),
children: Vec::new(),
};
let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value);
}
#[test]
fn add_child() {
let mut redactions = RedactionMap::default();
let tree = empty_tree();
redactions.intern(tree.get_root_id());
let snapshot = InstanceSnapshot {
snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("ROOT"),
class_name: ustr("ROOT"),
properties: Default::default(),
children: vec![InstanceSnapshot {
snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("New"),
class_name: ustr("Folder"),
properties: Default::default(),
children: Vec::new(),
}],
};
let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value);
}
#[test]
fn remove_child() {
let mut redactions = RedactionMap::default();
let mut tree = empty_tree();
redactions.intern(tree.get_root_id());
{
let root_id = tree.get_root_id();
let new_id = tree.insert_instance(
root_id,
InstanceSnapshot::new().name("Should not appear in snapshot"),
);
redactions.intern(new_id);
}
let snapshot = InstanceSnapshot {
snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("ROOT"),
class_name: ustr("ROOT"),
properties: Default::default(),
children: Vec::new(),
};
let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value);
}
fn empty_tree() -> RojoTree {
RojoTree::new(InstanceSnapshot::new().name("ROOT").class_name("ROOT"))
}
// ---------------------------------------------------------------------------
// Rojo_Ref_* attribute resolution tests
// ---------------------------------------------------------------------------
use rbx_dom_weak::types::{Attributes, Variant};
/// Helper: build a tree with ROOT > Workspace > Part (to support path resolution).
fn tree_with_workspace_part() -> RojoTree {
let snapshot = InstanceSnapshot::new()
.name("ROOT")
.class_name("DataModel")
.children(vec![InstanceSnapshot::new()
.name("Workspace")
.class_name("Workspace")
.children(vec![InstanceSnapshot::new()
.name("Part")
.class_name("Part")])]);
RojoTree::new(snapshot)
}
/// Helper: build a tree with ROOT > Workspace > Model > Target + OtherPart.
fn tree_with_model_and_parts() -> RojoTree {
let snapshot = InstanceSnapshot::new()
.name("ROOT")
.class_name("DataModel")
.children(vec![InstanceSnapshot::new()
.name("Workspace")
.class_name("Workspace")
.children(vec![InstanceSnapshot::new()
.name("Model")
.class_name("Model")
.children(vec![
InstanceSnapshot::new().name("Target").class_name("Part"),
InstanceSnapshot::new().name("OtherPart").class_name("Part"),
])])]);
RojoTree::new(snapshot)
}
#[test]
fn ref_attr_resolves_primary_part() {
let tree = tree_with_model_and_parts();
// Find the Model instance in the tree
let root_id = tree.get_root_id();
let workspace_id = tree
.get_instance(root_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
let model_id = tree
.get_instance(workspace_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
// Create a snapshot of Model with Rojo_Ref_PrimaryPart attribute
let mut attrs = Attributes::new();
attrs.insert(
"Rojo_Ref_PrimaryPart".into(),
Variant::String("Workspace/Model/Target".into()),
);
let snapshot = InstanceSnapshot::new()
.name("Model")
.class_name("Model")
.properties(UstrMap::from_iter([(
ustr("Attributes"),
Variant::Attributes(attrs),
)]))
.children(vec![
InstanceSnapshot::new().name("Target").class_name("Part"),
InstanceSnapshot::new().name("OtherPart").class_name("Part"),
]);
let patch_set = compute_patch_set(Some(snapshot), &tree, model_id);
// The patch should contain an update with PrimaryPart as a Variant::Ref
let has_primary_part_ref = patch_set.updated_instances.iter().any(|u| {
u.changed_properties
.get(&ustr("PrimaryPart"))
.map_or(false, |v| matches!(v, Some(Variant::Ref(r)) if r.is_some()))
});
assert!(
has_primary_part_ref,
"Rojo_Ref_PrimaryPart should resolve to a Variant::Ref in the patch"
);
}
#[test]
fn ref_attr_resolves_value() {
let tree = tree_with_workspace_part();
let root_id = tree.get_root_id();
let workspace_id = tree
.get_instance(root_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
// Add an ObjectValue child to Workspace in the tree
let mut tree = tree;
let objval_id = tree.insert_instance(
workspace_id,
InstanceSnapshot::new()
.name("MyRef")
.class_name("ObjectValue"),
);
// Create snapshot with Rojo_Ref_Value pointing to Workspace/Part
let mut attrs = Attributes::new();
attrs.insert(
"Rojo_Ref_Value".into(),
Variant::String("Workspace/Part".into()),
);
let snapshot = InstanceSnapshot::new()
.name("MyRef")
.class_name("ObjectValue")
.properties(UstrMap::from_iter([(
ustr("Attributes"),
Variant::Attributes(attrs),
)]));
let patch_set = compute_patch_set(Some(snapshot), &tree, objval_id);
let has_value_ref = patch_set.updated_instances.iter().any(|u| {
u.changed_properties
.get(&ustr("Value"))
.map_or(false, |v| matches!(v, Some(Variant::Ref(r)) if r.is_some()))
});
assert!(
has_value_ref,
"Rojo_Ref_Value should resolve to a Variant::Ref in the patch"
);
}
#[test]
fn ref_attr_multiple_on_same_instance() {
let tree = tree_with_model_and_parts();
let root_id = tree.get_root_id();
let workspace_id = tree
.get_instance(root_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
let model_id = tree
.get_instance(workspace_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
// Multiple Rojo_Ref_* attributes
let mut attrs = Attributes::new();
attrs.insert(
"Rojo_Ref_PrimaryPart".into(),
Variant::String("Workspace/Model/Target".into()),
);
attrs.insert(
"Rojo_Ref_CustomRef".into(),
Variant::String("Workspace/Model/OtherPart".into()),
);
let snapshot = InstanceSnapshot::new()
.name("Model")
.class_name("Model")
.properties(UstrMap::from_iter([(
ustr("Attributes"),
Variant::Attributes(attrs),
)]))
.children(vec![
InstanceSnapshot::new().name("Target").class_name("Part"),
InstanceSnapshot::new().name("OtherPart").class_name("Part"),
]);
let patch_set = compute_patch_set(Some(snapshot), &tree, model_id);
let update = patch_set
.updated_instances
.iter()
.find(|u| u.id == model_id);
assert!(update.is_some(), "Should have an update for the Model");
let update = update.unwrap();
let has_pp = update
.changed_properties
.get(&ustr("PrimaryPart"))
.map_or(false, |v| matches!(v, Some(Variant::Ref(r)) if r.is_some()));
let has_custom = update
.changed_properties
.get(&ustr("CustomRef"))
.map_or(false, |v| matches!(v, Some(Variant::Ref(r)) if r.is_some()));
assert!(has_pp, "PrimaryPart ref should resolve");
assert!(has_custom, "CustomRef ref should resolve");
}
#[test]
fn ref_attr_nonexistent_path_does_not_produce_valid_ref() {
let tree = tree_with_workspace_part();
let root_id = tree.get_root_id();
let workspace_id = tree
.get_instance(root_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
let mut tree = tree;
let model_id = tree.insert_instance(
workspace_id,
InstanceSnapshot::new().name("Model").class_name("Model"),
);
let mut attrs = Attributes::new();
attrs.insert(
"Rojo_Ref_PrimaryPart".into(),
Variant::String("Workspace/DoesNotExist".into()),
);
let snapshot = InstanceSnapshot::new()
.name("Model")
.class_name("Model")
.properties(UstrMap::from_iter([(
ustr("Attributes"),
Variant::Attributes(attrs),
)]));
let patch_set = compute_patch_set(Some(snapshot), &tree, model_id);
// A non-existent path should NOT produce a valid (non-nil) Ref.
// compute_ref_properties returns None for unresolvable paths, which
// means "property value not set" -- not "set to nil Ref".
let has_valid_ref = patch_set.updated_instances.iter().any(|u| {
u.changed_properties
.get(&ustr("PrimaryPart"))
.map_or(false, |v| matches!(v, Some(Variant::Ref(r)) if r.is_some()))
});
assert!(
!has_valid_ref,
"Non-existent path should not produce a valid (non-nil) Ref"
);
}
#[test]
fn ref_attr_with_regular_attributes_mixed() {
let tree = tree_with_model_and_parts();
let root_id = tree.get_root_id();
let workspace_id = tree
.get_instance(root_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
let model_id = tree
.get_instance(workspace_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
// Mix of Rojo_Ref_* and regular attributes
let mut attrs = Attributes::new();
attrs.insert(
"Rojo_Ref_PrimaryPart".into(),
Variant::String("Workspace/Model/Target".into()),
);
attrs.insert("CustomAttribute".into(), Variant::String("hello".into()));
attrs.insert("NumberAttr".into(), Variant::Float64(42.0));
let snapshot = InstanceSnapshot::new()
.name("Model")
.class_name("Model")
.properties(UstrMap::from_iter([(
ustr("Attributes"),
Variant::Attributes(attrs),
)]))
.children(vec![
InstanceSnapshot::new().name("Target").class_name("Part"),
InstanceSnapshot::new().name("OtherPart").class_name("Part"),
]);
let patch_set = compute_patch_set(Some(snapshot), &tree, model_id);
// Should have PrimaryPart as Ref AND the regular attributes should
// remain in the Attributes property (not be extracted as Ref properties).
let has_pp = patch_set.updated_instances.iter().any(|u| {
u.changed_properties
.get(&ustr("PrimaryPart"))
.map_or(false, |v| matches!(v, Some(Variant::Ref(_))))
});
assert!(has_pp, "PrimaryPart should be resolved from Rojo_Ref_*");
}
#[test]
fn ref_attr_empty_path_resolves_to_root() {
let tree = tree_with_workspace_part();
let root_id = tree.get_root_id();
let workspace_id = tree
.get_instance(root_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
let mut tree = tree;
let model_id = tree.insert_instance(
workspace_id,
InstanceSnapshot::new().name("Model").class_name("Model"),
);
let mut attrs = Attributes::new();
attrs.insert("Rojo_Ref_PrimaryPart".into(), Variant::String("".into()));
let snapshot = InstanceSnapshot::new()
.name("Model")
.class_name("Model")
.properties(UstrMap::from_iter([(
ustr("Attributes"),
Variant::Attributes(attrs),
)]));
let patch_set = compute_patch_set(Some(snapshot), &tree, model_id);
// Empty path should resolve to root (RojoTree.get_instance_by_path("") returns root)
let has_ref = patch_set.updated_instances.iter().any(|u| {
u.changed_properties.get(&ustr("PrimaryPart")).map_or(
false,
|v| matches!(v, Some(Variant::Ref(r)) if *r == root_id),
)
});
assert!(has_ref, "Empty path should resolve to root instance");
}