mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Refactor Reconciler tests and update path handling logic
- Updated test descriptions in `diff.spec.lua` and `diff.stress.spec.lua` to reflect changes in behavior regarding duplicate-named children in both real and virtual DOM contexts. - Adjusted assertions to ensure that duplicate-named children are correctly removed or added based on the new matching logic. - Refactored `hydrate.lua` and `matching.lua` to streamline the matching process by removing unnecessary parameters, enhancing clarity and maintainability. - Updated metadata references in various test files to include the `.model.json5` extension for consistency in path handling. These changes improve the accuracy of reconciliation tests and enhance the overall robustness of the Reconciler's path handling logic.
This commit is contained in:
parent
3ad3c21c01
commit
bb9d241253
18 changed files with 129 additions and 94 deletions
|
|
@ -276,7 +276,7 @@ return function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe("duplicate-named siblings", function()
|
describe("duplicate-named siblings", function()
|
||||||
it("should skip real DOM children with duplicate names", function()
|
it("should remove real DOM children with duplicate names when not in virtual DOM", function()
|
||||||
local knownInstances = InstanceMap.new()
|
local knownInstances = InstanceMap.new()
|
||||||
local virtualInstances = {
|
local virtualInstances = {
|
||||||
ROOT = {
|
ROOT = {
|
||||||
|
|
@ -305,11 +305,11 @@ return function()
|
||||||
|
|
||||||
assert(ok, tostring(patch))
|
assert(ok, tostring(patch))
|
||||||
|
|
||||||
-- Duplicate-named children should be skipped (not removed)
|
-- Duplicate-named children should be removed (matching handles pairing)
|
||||||
assert(isEmpty(patch.removed))
|
expect(#patch.removed).to.equal(2)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("should skip virtual children with duplicate names among siblings", function()
|
it("should add virtual children with duplicate names among siblings", function()
|
||||||
local knownInstances = InstanceMap.new()
|
local knownInstances = InstanceMap.new()
|
||||||
local virtualInstances = {
|
local virtualInstances = {
|
||||||
ROOT = {
|
ROOT = {
|
||||||
|
|
@ -340,8 +340,8 @@ return function()
|
||||||
|
|
||||||
assert(ok, tostring(patch))
|
assert(ok, tostring(patch))
|
||||||
|
|
||||||
-- Duplicate-named virtual children should not be added
|
-- Duplicate-named virtual children should be added (matching handles pairing)
|
||||||
assert(isEmpty(patch.added))
|
expect(size(patch.added)).to.equal(2)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -239,9 +239,8 @@ return function()
|
||||||
local ok, patch = diff(instanceMap, virtualInstances, rootId)
|
local ok, patch = diff(instanceMap, virtualInstances, rootId)
|
||||||
|
|
||||||
expect(ok).to.equal(true)
|
expect(ok).to.equal(true)
|
||||||
-- Duplicates should NOT be marked for removal
|
-- Duplicates should be marked for removal (matching handles pairing)
|
||||||
-- Only if they had unique names would they be candidates
|
expect(#patch.removed).to.equal(2)
|
||||||
expect(#patch.removed).to.equal(0)
|
|
||||||
|
|
||||||
instanceMap:stop()
|
instanceMap:stop()
|
||||||
end)
|
end)
|
||||||
|
|
@ -290,9 +289,9 @@ return function()
|
||||||
local ok, patch = diff(instanceMap, virtualInstances, rootId)
|
local ok, patch = diff(instanceMap, virtualInstances, rootId)
|
||||||
|
|
||||||
expect(ok).to.equal(true)
|
expect(ok).to.equal(true)
|
||||||
-- Neither duplicate should be added since the path is ambiguous
|
-- Both duplicates should be added (matching handles pairing)
|
||||||
expect(patch.added[dup1Id]).to.equal(nil)
|
expect(patch.added[dup1Id]).to.be.ok()
|
||||||
expect(patch.added[dup2Id]).to.equal(nil)
|
expect(patch.added[dup2Id]).to.be.ok()
|
||||||
|
|
||||||
instanceMap:stop()
|
instanceMap:stop()
|
||||||
end)
|
end)
|
||||||
|
|
@ -350,8 +349,8 @@ return function()
|
||||||
local ok, patch = diff(instanceMap, virtualInstances, rootId)
|
local ok, patch = diff(instanceMap, virtualInstances, rootId)
|
||||||
|
|
||||||
expect(ok).to.equal(true)
|
expect(ok).to.equal(true)
|
||||||
-- Grandchild should also NOT be added since its parent is ambiguous
|
-- Grandchild should be added along with its parent (matching handles pairing)
|
||||||
expect(patch.added[grandchildId]).to.equal(nil)
|
expect(patch.added[grandchildId]).to.be.ok()
|
||||||
|
|
||||||
instanceMap:stop()
|
instanceMap:stop()
|
||||||
end)
|
end)
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ local function hydrate(instanceMap, virtualInstances, rootId, rootInstance)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Use the 3-pass matching algorithm to pair virtual ↔ studio children
|
-- Use the 3-pass matching algorithm to pair virtual ↔ studio children
|
||||||
local result = Matching.matchChildren(validVirtualIds, existingChildren, virtualInstances, instanceMap)
|
local result = Matching.matchChildren(validVirtualIds, existingChildren, virtualInstances)
|
||||||
|
|
||||||
-- Recursively hydrate matched pairs
|
-- Recursively hydrate matched pairs
|
||||||
for _, pair in result.matched do
|
for _, pair in result.matched do
|
||||||
|
|
|
||||||
|
|
@ -114,8 +114,7 @@ local Matching = {}
|
||||||
function Matching.matchChildren(
|
function Matching.matchChildren(
|
||||||
virtualChildren: { string },
|
virtualChildren: { string },
|
||||||
studioChildren: { Instance },
|
studioChildren: { Instance },
|
||||||
virtualInstances: { [string]: any },
|
virtualInstances: { [string]: any }
|
||||||
instanceMap: any
|
|
||||||
)
|
)
|
||||||
local matched: { { virtualId: string, studioInstance: Instance } } = {}
|
local matched: { { virtualId: string, studioInstance: Instance } } = {}
|
||||||
local remainingVirtual: { string } = table.clone(virtualChildren)
|
local remainingVirtual: { string } = table.clone(virtualChildren)
|
||||||
|
|
@ -344,7 +343,7 @@ function Matching._computeChangeCount(
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Recursive: match children and sum costs
|
-- Recursive: match children and sum costs
|
||||||
local childResult = Matching.matchChildren(validVChildren, studioKids, virtualInstances, nil)
|
local childResult = Matching.matchChildren(validVChildren, studioKids, virtualInstances)
|
||||||
|
|
||||||
-- Matched children: recursively score each pair
|
-- Matched children: recursively score each pair
|
||||||
for _, pair in childResult.matched do
|
for _, pair in childResult.matched do
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"className": "Model",
|
"className": "Model",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"Rojo_Ref_PrimaryPart": "Workspace/TestModel/Part1"
|
"Rojo_Ref_PrimaryPart": "Workspace/TestModel/Part1.model.json5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ source: tests/rojo_test/syncback_util.rs
|
||||||
expression: normalized_stdout
|
expression: normalized_stdout
|
||||||
---
|
---
|
||||||
Writing OnlyOneCopy/child_of_one.luau
|
Writing OnlyOneCopy/child_of_one.luau
|
||||||
|
Writing ReplicatedStorage/OnlyOneCopy/child_of_one.luau
|
||||||
Writing ReplicatedStorage/child_replicated_storage.luau
|
Writing ReplicatedStorage/child_replicated_storage.luau
|
||||||
|
Writing ReplicatedStorage/OnlyOneCopy
|
||||||
Removing OnlyOneCopy/.gitkeep
|
Removing OnlyOneCopy/.gitkeep
|
||||||
Removing ReplicatedStorage/.gitkeep
|
Removing ReplicatedStorage/.gitkeep
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,12 @@
|
||||||
source: tests/rojo_test/syncback_util.rs
|
source: tests/rojo_test/syncback_util.rs
|
||||||
expression: normalized_stdout
|
expression: normalized_stdout
|
||||||
---
|
---
|
||||||
|
Writing src/ChildWithDuplicates/DuplicateChild/.gitkeep
|
||||||
|
Writing src/ChildWithDuplicates/DuplicateChild~1/init.meta.json5
|
||||||
Writing src/ChildWithoutDuplicates/Child/.gitkeep
|
Writing src/ChildWithoutDuplicates/Child/.gitkeep
|
||||||
Writing src/ChildWithDuplicates
|
Writing src/ChildWithDuplicates
|
||||||
|
Writing src/ChildWithDuplicates/DuplicateChild
|
||||||
|
Writing src/ChildWithDuplicates/DuplicateChild~1
|
||||||
Writing src/ChildWithoutDuplicates
|
Writing src/ChildWithoutDuplicates
|
||||||
Writing src/ChildWithoutDuplicates/Child
|
Writing src/ChildWithoutDuplicates/Child
|
||||||
Removing src/ChildWithDuplicates/.gitkeep
|
Removing src/ChildWithDuplicates/.gitkeep
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,6 @@ expression: normalized_stdout
|
||||||
---
|
---
|
||||||
Writing default.project.json5
|
Writing default.project.json5
|
||||||
Writing src/model_json.model.json5
|
Writing src/model_json.model.json5
|
||||||
|
Writing src/project_node.model.json5
|
||||||
Writing src/rbxm.rbxm
|
Writing src/rbxm.rbxm
|
||||||
Writing src/rbxmx.rbxmx
|
Writing src/rbxmx.rbxmx
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ expression: container.model.json5
|
||||||
{
|
{
|
||||||
attributes: {
|
attributes: {
|
||||||
Rojo_Id: "value_1",
|
Rojo_Id: "value_1",
|
||||||
Rojo_Ref_Value: "ReplicatedStorage/container/value_1"
|
Rojo_Ref_Value: "ReplicatedStorage/container/value_1.model.json5"
|
||||||
},
|
},
|
||||||
className: "ObjectValue",
|
className: "ObjectValue",
|
||||||
name: "value_1"
|
name: "value_1"
|
||||||
|
|
@ -15,7 +15,7 @@ expression: container.model.json5
|
||||||
{
|
{
|
||||||
attributes: {
|
attributes: {
|
||||||
Rojo_Id: "value_1",
|
Rojo_Id: "value_1",
|
||||||
Rojo_Ref_Value: "ReplicatedStorage/container/value_2"
|
Rojo_Ref_Value: "ReplicatedStorage/container/value_2.model.json5"
|
||||||
},
|
},
|
||||||
className: "ObjectValue",
|
className: "ObjectValue",
|
||||||
name: "value_2"
|
name: "value_2"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ expression: src/pointer.model.json5
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
attributes: {
|
attributes: {
|
||||||
Rojo_Ref_Value: "ReplicatedStorage/target"
|
Rojo_Ref_Value: "ReplicatedStorage/target.txt"
|
||||||
},
|
},
|
||||||
className: "ObjectValue"
|
className: "ObjectValue"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ expression: src/Pointer1.model.json5
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
attributes: {
|
attributes: {
|
||||||
Rojo_Ref_Value: "ReplicatedStorage/Target1"
|
Rojo_Ref_Value: "ReplicatedStorage/Target1.model.json5"
|
||||||
},
|
},
|
||||||
className: "ObjectValue"
|
className: "ObjectValue"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ expression: src/Pointer2.model.json5
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
attributes: {
|
attributes: {
|
||||||
Rojo_Ref_Value: "ReplicatedStorage/Target2"
|
Rojo_Ref_Value: "ReplicatedStorage/Target2.model.json5"
|
||||||
},
|
},
|
||||||
className: "ObjectValue"
|
className: "ObjectValue"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ expression: src/Pointer3.model.json5
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
attributes: {
|
attributes: {
|
||||||
Rojo_Ref_Value: "ReplicatedStorage/Target3"
|
Rojo_Ref_Value: "ReplicatedStorage/Target3.model.json5"
|
||||||
},
|
},
|
||||||
className: "ObjectValue"
|
className: "ObjectValue"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -485,10 +485,8 @@ impl JobThreadContext {
|
||||||
// this function is called. The index still stores old filesystem
|
// this function is called. The index still stores old filesystem
|
||||||
// paths. Remap them: replace the old directory segment with the
|
// paths. Remap them: replace the old directory segment with the
|
||||||
// new one so we can find the actual files.
|
// new one so we can find the actual files.
|
||||||
let old_segments = crate::split_ref_path(old_path);
|
let old_segment = old_path.rsplit('/').next().unwrap_or(old_path);
|
||||||
let old_segment = old_segments.last().map(|s| s.as_str()).unwrap_or(old_path);
|
let new_segment = new_path.rsplit('/').next().unwrap_or(new_path);
|
||||||
let new_segments = crate::split_ref_path(new_path);
|
|
||||||
let new_segment = new_segments.last().map(|s| s.as_str()).unwrap_or(new_path);
|
|
||||||
|
|
||||||
// Pre-compute slugified forms for fallback comparison.
|
// Pre-compute slugified forms for fallback comparison.
|
||||||
let slugified_old = if name_needs_slugify(old_segment) {
|
let slugified_old = if name_needs_slugify(old_segment) {
|
||||||
|
|
@ -1202,11 +1200,17 @@ impl JobThreadContext {
|
||||||
|
|
||||||
// Capture the old path BEFORE rename for Rojo_Ref_* path updates.
|
// Capture the old path BEFORE rename for Rojo_Ref_* path updates.
|
||||||
// Must be computed before the `tree.get_instance(id)` borrow.
|
// Must be computed before the `tree.get_instance(id)` borrow.
|
||||||
let old_ref_path = if update.changed_name.is_some() {
|
let old_ref_path =
|
||||||
Some(crate::ref_target_path_from_tree(&tree, id))
|
if update.changed_name.is_some() || update.changed_class_name.is_some() {
|
||||||
} else {
|
Some(crate::ref_target_path_from_tree(&tree, id))
|
||||||
None
|
} else {
|
||||||
};
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// The new filesystem name segment after a rename. Set during
|
||||||
|
// rename handling and used by the ref path update code to build
|
||||||
|
// the correct filesystem-name-based ref path.
|
||||||
|
let mut new_ref_segment: Option<String> = None;
|
||||||
|
|
||||||
if let Some(instance) = tree.get_instance(id) {
|
if let Some(instance) = tree.get_instance(id) {
|
||||||
// Track the current source file path — rename and ClassName
|
// Track the current source file path — rename and ClassName
|
||||||
|
|
@ -1278,6 +1282,8 @@ impl JobThreadContext {
|
||||||
// The init file moved with the directory.
|
// The init file moved with the directory.
|
||||||
overridden_source_path =
|
overridden_source_path =
|
||||||
Some(new_dir_path.join(file_name));
|
Some(new_dir_path.join(file_name));
|
||||||
|
new_ref_segment =
|
||||||
|
Some(slugified_new_name.clone());
|
||||||
let old_meta = grandparent.join(format!(
|
let old_meta = grandparent.join(format!(
|
||||||
"{}.meta.json5",
|
"{}.meta.json5",
|
||||||
dir_name
|
dir_name
|
||||||
|
|
@ -1438,6 +1444,8 @@ impl JobThreadContext {
|
||||||
effective_meta_base = old_base;
|
effective_meta_base = old_base;
|
||||||
} else {
|
} else {
|
||||||
overridden_source_path = Some(new_path.clone());
|
overridden_source_path = Some(new_path.clone());
|
||||||
|
new_ref_segment =
|
||||||
|
Some(new_file_name.clone());
|
||||||
let old_meta = parent
|
let old_meta = parent
|
||||||
.join(format!("{}.meta.json5", old_base));
|
.join(format!("{}.meta.json5", old_base));
|
||||||
let new_meta = parent.join(format!(
|
let new_meta = parent.join(format!(
|
||||||
|
|
@ -1616,6 +1624,14 @@ impl JobThreadContext {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
overridden_source_path = Some(new_path.clone());
|
overridden_source_path = Some(new_path.clone());
|
||||||
|
// For standalone files, the ref path
|
||||||
|
// segment changes with the extension.
|
||||||
|
// For init files, the directory name
|
||||||
|
// (ref segment) is unchanged.
|
||||||
|
if !is_init {
|
||||||
|
new_ref_segment =
|
||||||
|
Some(new_file_name.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1717,31 +1733,32 @@ impl JobThreadContext {
|
||||||
//
|
//
|
||||||
// NOTE: The tree name hasn't been updated yet (apply_patch_set
|
// NOTE: The tree name hasn't been updated yet (apply_patch_set
|
||||||
// runs after this loop), so we can't use full_path_of for the
|
// runs after this loop), so we can't use full_path_of for the
|
||||||
// new path. Construct it by replacing the last path segment.
|
// new path. Construct it by replacing the last path segment
|
||||||
// Use split_ref_path for escape-aware splitting (handles
|
// with the NEW filesystem name (set during rename handling).
|
||||||
// instance names containing "/").
|
|
||||||
if let Some(ref old_ref_path) = old_ref_path {
|
if let Some(ref old_ref_path) = old_ref_path {
|
||||||
if let Some(ref new_name) = update.changed_name {
|
if let Some(ref segment) = new_ref_segment {
|
||||||
let segments = crate::split_ref_path(old_ref_path);
|
// Use the filesystem name computed during rename handling
|
||||||
|
let segments: Vec<&str> = old_ref_path.split('/').collect();
|
||||||
let new_ref_path = if segments.len() > 1 {
|
let new_ref_path = if segments.len() > 1 {
|
||||||
// Rebuild parent path from all segments except the last,
|
let parent = segments[..segments.len() - 1].join("/");
|
||||||
// re-escaping each segment to preserve the original format.
|
format!("{}/{}", parent, segment)
|
||||||
let parent_parts: Vec<_> = segments[..segments.len() - 1]
|
|
||||||
.iter()
|
|
||||||
.map(|s| crate::escape_ref_path_segment(s).into_owned())
|
|
||||||
.collect();
|
|
||||||
format!(
|
|
||||||
"{}/{}",
|
|
||||||
parent_parts.join("/"),
|
|
||||||
crate::escape_ref_path_segment(new_name)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
// Root-level instance (no parent in path)
|
segment.clone()
|
||||||
crate::escape_ref_path_segment(new_name).into_owned()
|
|
||||||
};
|
};
|
||||||
if *old_ref_path != new_ref_path {
|
if *old_ref_path != new_ref_path {
|
||||||
self.update_ref_paths_after_rename(old_ref_path, &new_ref_path);
|
self.update_ref_paths_after_rename(old_ref_path, &new_ref_path);
|
||||||
}
|
}
|
||||||
|
} else if update.changed_name.is_some()
|
||||||
|
|| update.changed_class_name.is_some()
|
||||||
|
{
|
||||||
|
// Rename or class change was requested but no filesystem
|
||||||
|
// rename happened (e.g., ProjectNode, init-file class
|
||||||
|
// change where directory name stays the same). No ref
|
||||||
|
// path update needed.
|
||||||
|
log::trace!(
|
||||||
|
"Skipping ref path update for {:?}: no filesystem rename",
|
||||||
|
id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use rbx_dom_weak::{
|
||||||
|
|
||||||
use crate::{multimap::MultiMap, RojoRef};
|
use crate::{multimap::MultiMap, RojoRef};
|
||||||
|
|
||||||
use super::{InstanceMetadata, InstanceSnapshot};
|
use super::{InstigatingSource, InstanceMetadata, InstanceSnapshot};
|
||||||
|
|
||||||
/// An expanded variant of rbx_dom_weak's `WeakDom` that tracks additional
|
/// An expanded variant of rbx_dom_weak's `WeakDom` that tracks additional
|
||||||
/// metadata per instance that's Rojo-specific.
|
/// metadata per instance that's Rojo-specific.
|
||||||
|
|
@ -291,8 +291,19 @@ impl RojoTree {
|
||||||
pub fn filesystem_name_for(&self, id: Ref) -> String {
|
pub fn filesystem_name_for(&self, id: Ref) -> String {
|
||||||
if let Some(meta) = self.metadata_map.get(&id) {
|
if let Some(meta) = self.metadata_map.get(&id) {
|
||||||
if let Some(source) = &meta.instigating_source {
|
if let Some(source) = &meta.instigating_source {
|
||||||
if let Some(name) = source.path().file_name().and_then(|f| f.to_str()) {
|
match source {
|
||||||
return name.to_string();
|
InstigatingSource::Path(_) => {
|
||||||
|
if let Some(name) = source.path().file_name().and_then(|f| f.to_str()) {
|
||||||
|
return name.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InstigatingSource::ProjectNode { .. } => {
|
||||||
|
// Project-sourced instance: use instance name, not the
|
||||||
|
// project file path (which would be e.g. "default.project.json5").
|
||||||
|
if let Some(inst) = self.inner.get_by_ref(id) {
|
||||||
|
return inst.name.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -628,6 +628,17 @@ pub fn syncback_project<'sync>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Project children with $path are expected in the old DOM (loaded
|
||||||
|
// from their own path) AND may also appear as filesystem children of
|
||||||
|
// the parent's scanned directory. Exclude them from duplicate
|
||||||
|
// detection so the project child loop below can match them.
|
||||||
|
let project_path_child_names: HashSet<String> = node
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, child_node)| child_node.path.is_some())
|
||||||
|
.map(|(name, _)| name.to_lowercase())
|
||||||
|
.collect();
|
||||||
|
|
||||||
let mut old_duplicate_names: HashSet<String> = HashSet::new();
|
let mut old_duplicate_names: HashSet<String> = HashSet::new();
|
||||||
{
|
{
|
||||||
let mut seen: HashSet<String> = HashSet::new();
|
let mut seen: HashSet<String> = HashSet::new();
|
||||||
|
|
@ -636,6 +647,9 @@ pub fn syncback_project<'sync>(
|
||||||
.get_old_instance(*child_ref)
|
.get_old_instance(*child_ref)
|
||||||
.expect("all children of Instances should be in old DOM");
|
.expect("all children of Instances should be in old DOM");
|
||||||
let lower = child.name().to_lowercase();
|
let lower = child.name().to_lowercase();
|
||||||
|
if project_path_child_names.contains(&lower) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if !seen.insert(lower.clone()) {
|
if !seen.insert(lower.clone()) {
|
||||||
old_duplicate_names.insert(lower);
|
old_duplicate_names.insert(lower);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,11 +58,6 @@ pub fn parse_dedup_suffix(stem: &str) -> Option<(&str, u32)> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks whether a filename stem has a dedup suffix.
|
|
||||||
pub fn has_dedup_suffix(stem: &str) -> bool {
|
|
||||||
parse_dedup_suffix(stem).is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds a dedup'd filename from a base stem, optional suffix number, and
|
/// Builds a dedup'd filename from a base stem, optional suffix number, and
|
||||||
/// extension.
|
/// extension.
|
||||||
///
|
///
|
||||||
|
|
@ -180,13 +175,6 @@ mod tests {
|
||||||
assert_eq!(parse_dedup_suffix("My Script~1"), Some(("My Script", 1)));
|
assert_eq!(parse_dedup_suffix("My Script~1"), Some(("My Script", 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn has_suffix() {
|
|
||||||
assert!(has_dedup_suffix("Foo~1"));
|
|
||||||
assert!(!has_dedup_suffix("Foo"));
|
|
||||||
assert!(!has_dedup_suffix("Foo~abc"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_name_file() {
|
fn build_name_file() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
||||||
|
|
@ -6127,7 +6127,7 @@ fn ref_set_primary_part() {
|
||||||
poll_meta_has_ref_attr(
|
poll_meta_has_ref_attr(
|
||||||
&meta_path,
|
&meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/TestModel/Part1",
|
"Workspace/TestModel/Part1.model.json5",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -6156,7 +6156,7 @@ fn ref_set_object_value() {
|
||||||
let model_path = session
|
let model_path = session
|
||||||
.path()
|
.path()
|
||||||
.join("src/Workspace/TestObjectValue.model.json5");
|
.join("src/Workspace/TestObjectValue.model.json5");
|
||||||
poll_meta_has_ref_attr(&model_path, "Rojo_Ref_Value", "Workspace/TestModel/Part1");
|
poll_meta_has_ref_attr(&model_path, "Rojo_Ref_Value", "Workspace/TestModel/Part1.model.json5");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6190,7 +6190,7 @@ fn ref_set_primary_part_to_nil() {
|
||||||
poll_meta_has_ref_attr(
|
poll_meta_has_ref_attr(
|
||||||
&meta_path,
|
&meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/TestModel/Part1",
|
"Workspace/TestModel/Part1.model.json5",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Now set PrimaryPart to nil
|
// Now set PrimaryPart to nil
|
||||||
|
|
@ -6273,7 +6273,7 @@ fn ref_change_target() {
|
||||||
poll_meta_has_ref_attr(
|
poll_meta_has_ref_attr(
|
||||||
&meta_path,
|
&meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/TestModel/Part1",
|
"Workspace/TestModel/Part1.model.json5",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Change PrimaryPart to Part2
|
// Change PrimaryPart to Part2
|
||||||
|
|
@ -6294,7 +6294,7 @@ fn ref_change_target() {
|
||||||
poll_meta_has_ref_attr(
|
poll_meta_has_ref_attr(
|
||||||
&meta_path,
|
&meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/TestModel/Part2",
|
"Workspace/TestModel/Part2.model.json5",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -6324,7 +6324,7 @@ fn ref_set_nil_then_set_again() {
|
||||||
poll_meta_has_ref_attr(
|
poll_meta_has_ref_attr(
|
||||||
&meta_path,
|
&meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/TestModel/Part1",
|
"Workspace/TestModel/Part1.model.json5",
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut props2 = UstrMap::default();
|
let mut props2 = UstrMap::default();
|
||||||
|
|
@ -6359,7 +6359,7 @@ fn ref_set_nil_then_set_again() {
|
||||||
poll_meta_has_ref_attr(
|
poll_meta_has_ref_attr(
|
||||||
&meta_path,
|
&meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/TestModel/Part1",
|
"Workspace/TestModel/Part1.model.json5",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -6402,7 +6402,7 @@ fn ref_with_name_change() {
|
||||||
assert_meta_has_ref_attr(
|
assert_meta_has_ref_attr(
|
||||||
&new_meta,
|
&new_meta,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/RenamedModel/Part1",
|
"Workspace/RenamedModel/Part1.model.json5",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -6433,7 +6433,7 @@ fn ref_only_change_creates_meta() {
|
||||||
poll_meta_has_ref_attr(
|
poll_meta_has_ref_attr(
|
||||||
&meta_path,
|
&meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/TestModel/Part1",
|
"Workspace/TestModel/Part1.model.json5",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -6467,7 +6467,7 @@ fn ref_on_model_json5_instance() {
|
||||||
let model_path = session
|
let model_path = session
|
||||||
.path()
|
.path()
|
||||||
.join("src/Workspace/TestObjectValue.model.json5");
|
.join("src/Workspace/TestObjectValue.model.json5");
|
||||||
poll_meta_has_ref_attr(&model_path, "Rojo_Ref_Value", "Workspace/TestModel/Part1");
|
poll_meta_has_ref_attr(&model_path, "Rojo_Ref_Value", "Workspace/TestModel/Part1.model.json5");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6498,7 +6498,7 @@ fn ref_existing_meta_preserved() {
|
||||||
poll_meta_has_ref_attr(
|
poll_meta_has_ref_attr(
|
||||||
&meta_path,
|
&meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/TestModel/Part1",
|
"Workspace/TestModel/Part1.model.json5",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify existing content wasn't clobbered
|
// Verify existing content wasn't clobbered
|
||||||
|
|
@ -6571,7 +6571,7 @@ fn ref_mixed_valid_and_invalid() {
|
||||||
poll_meta_has_ref_attr(
|
poll_meta_has_ref_attr(
|
||||||
&meta_path,
|
&meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/TestModel/Part1",
|
"Workspace/TestModel/Part1.model.json5",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -6614,7 +6614,7 @@ fn ref_stale_path_after_target_rename() {
|
||||||
poll_meta_has_ref_attr(
|
poll_meta_has_ref_attr(
|
||||||
&meta_path,
|
&meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/TestModel/Part1",
|
"Workspace/TestModel/Part1.model.json5",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Step 2: Rename Part1 to RenamedPart
|
// Step 2: Rename Part1 to RenamedPart
|
||||||
|
|
@ -6637,7 +6637,7 @@ fn ref_stale_path_after_target_rename() {
|
||||||
assert_meta_has_ref_attr(
|
assert_meta_has_ref_attr(
|
||||||
&meta_path,
|
&meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/TestModel/RenamedPart",
|
"Workspace/TestModel/RenamedPart.model.json5",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -6671,7 +6671,7 @@ fn ref_stale_path_after_parent_rename() {
|
||||||
poll_meta_has_ref_attr(
|
poll_meta_has_ref_attr(
|
||||||
&meta_path,
|
&meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/TestModel/Part1",
|
"Workspace/TestModel/Part1.model.json5",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Step 2: Rename the MODEL (parent) from TestModel to RenamedModel
|
// Step 2: Rename the MODEL (parent) from TestModel to RenamedModel
|
||||||
|
|
@ -6700,7 +6700,7 @@ fn ref_stale_path_after_parent_rename() {
|
||||||
assert_meta_has_ref_attr(
|
assert_meta_has_ref_attr(
|
||||||
&new_meta_path,
|
&new_meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/RenamedModel/Part1",
|
"Workspace/RenamedModel/Part1.model.json5",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -6829,7 +6829,7 @@ fn ref_ambiguous_path_no_crash() {
|
||||||
let meta_path = session
|
let meta_path = session
|
||||||
.path()
|
.path()
|
||||||
.join("src/Workspace/DupParent/init.meta.json5");
|
.join("src/Workspace/DupParent/init.meta.json5");
|
||||||
poll_meta_has_ref_attr(&meta_path, "Rojo_Ref_PrimaryPart", "Workspace/Target");
|
poll_meta_has_ref_attr(&meta_path, "Rojo_Ref_PrimaryPart", "Workspace/Target.model.json5");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6926,7 +6926,7 @@ fn ref_startup_index_rename_updates_preexisting_attr() {
|
||||||
assert_meta_has_ref_attr(
|
assert_meta_has_ref_attr(
|
||||||
&meta_path,
|
&meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/TestModel/Part1",
|
"Workspace/TestModel/Part1.model.json5",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Rename Part1 to IndexTestPart WITHOUT any prior /api/write for Refs.
|
// Rename Part1 to IndexTestPart WITHOUT any prior /api/write for Refs.
|
||||||
|
|
@ -6951,7 +6951,7 @@ fn ref_startup_index_rename_updates_preexisting_attr() {
|
||||||
assert_meta_has_ref_attr(
|
assert_meta_has_ref_attr(
|
||||||
&meta_path,
|
&meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/TestModel/IndexTestPart",
|
"Workspace/TestModel/IndexTestPart.model.json5",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -6987,10 +6987,10 @@ fn ref_partial_removal_preserves_remaining_index_entry() {
|
||||||
poll_meta_has_ref_attr(
|
poll_meta_has_ref_attr(
|
||||||
&meta_path,
|
&meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/TestModel/Part1",
|
"Workspace/TestModel/Part1.model.json5",
|
||||||
);
|
);
|
||||||
// Verify second ref is there too
|
// Verify second ref is there too
|
||||||
assert_meta_has_ref_attr(&meta_path, "Rojo_Ref_Adornee", "Workspace/TestModel/Part2");
|
assert_meta_has_ref_attr(&meta_path, "Rojo_Ref_Adornee", "Workspace/TestModel/Part2.model.json5");
|
||||||
|
|
||||||
// Step 2: Remove PrimaryPart (set to nil), keep Adornee
|
// Step 2: Remove PrimaryPart (set to nil), keep Adornee
|
||||||
let mut props2 = UstrMap::default();
|
let mut props2 = UstrMap::default();
|
||||||
|
|
@ -7010,7 +7010,7 @@ fn ref_partial_removal_preserves_remaining_index_entry() {
|
||||||
thread::sleep(Duration::from_millis(300));
|
thread::sleep(Duration::from_millis(300));
|
||||||
assert_meta_no_ref_attr(&meta_path, "Rojo_Ref_PrimaryPart");
|
assert_meta_no_ref_attr(&meta_path, "Rojo_Ref_PrimaryPart");
|
||||||
// Adornee should still be there
|
// Adornee should still be there
|
||||||
assert_meta_has_ref_attr(&meta_path, "Rojo_Ref_Adornee", "Workspace/TestModel/Part2");
|
assert_meta_has_ref_attr(&meta_path, "Rojo_Ref_Adornee", "Workspace/TestModel/Part2.model.json5");
|
||||||
|
|
||||||
// Step 3: Rename Part2. The RefPathIndex should still have the entry
|
// Step 3: Rename Part2. The RefPathIndex should still have the entry
|
||||||
// for Adornee (not overbroad-removed when PrimaryPart was deleted).
|
// for Adornee (not overbroad-removed when PrimaryPart was deleted).
|
||||||
|
|
@ -7032,7 +7032,7 @@ fn ref_partial_removal_preserves_remaining_index_entry() {
|
||||||
assert_meta_has_ref_attr(
|
assert_meta_has_ref_attr(
|
||||||
&meta_path,
|
&meta_path,
|
||||||
"Rojo_Ref_Adornee",
|
"Rojo_Ref_Adornee",
|
||||||
"Workspace/TestModel/RenamedPart2",
|
"Workspace/TestModel/RenamedPart2.model.json5",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -7066,7 +7066,7 @@ fn ref_rename_to_slugified_name_updates_ref_paths() {
|
||||||
poll_meta_has_ref_attr(
|
poll_meta_has_ref_attr(
|
||||||
&meta_path,
|
&meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/TestModel/Part1",
|
"Workspace/TestModel/Part1.model.json5",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Rename Model to a name with forbidden characters.
|
// Rename Model to a name with forbidden characters.
|
||||||
|
|
@ -7097,7 +7097,7 @@ fn ref_rename_to_slugified_name_updates_ref_paths() {
|
||||||
assert_meta_has_ref_attr(
|
assert_meta_has_ref_attr(
|
||||||
&new_meta_path,
|
&new_meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/Slug:Model/Part1",
|
"Workspace/Slug_Model/Part1.model.json5",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -7145,7 +7145,7 @@ fn ref_on_standalone_script() {
|
||||||
poll_meta_has_ref_attr(
|
poll_meta_has_ref_attr(
|
||||||
&meta_path,
|
&meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/TestModel/Part1",
|
"Workspace/TestModel/Part1.model.json5",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify the wrong path does NOT have the attribute
|
// Verify the wrong path does NOT have the attribute
|
||||||
|
|
@ -7191,7 +7191,7 @@ fn ref_on_txt_file_instance() {
|
||||||
poll_meta_has_ref_attr(
|
poll_meta_has_ref_attr(
|
||||||
&meta_path,
|
&meta_path,
|
||||||
"Rojo_Ref_PrimaryPart",
|
"Rojo_Ref_PrimaryPart",
|
||||||
"Workspace/TestModel/Part1",
|
"Workspace/TestModel/Part1.model.json5",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue