mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-21 21:25:16 +00:00
Break apart plugin reconciler (#332)
* Start splitting apart reconciler, with tests * Reify children in reify * Baseline hydrate implementation * Remove debug print * Scaffold out diff implementation, just supporting name changes * invariant -> error in decodeValue * Flesh out diff and add getProperty * Clear out top-level reconciler interface, start updating code that touches it * Address review feedback * Add (experimental) Selene configuration * Add emptiness checks to PatchSet, remove unimplement invert method * Improve descendant destruction behavior in InstanceMap * Track instanceId on all reify errors * Base implementation of applyPatch, returning partial patches on failure * Change reify to accept InstanceMap and insert instances into it * Start testing applyPatch for removals * Add test for applyPatch adding instances successfully and not * Add , which is just error with formatting * Correctly use new diff and applyPatch APIs * Improve applyPatch logging and fix field name typo * Better debug output when reify fails * Print out unapplied patch in debug mode * Don't write properties if their values are not different. This was exposed trying to sync the Rojo plugin, which has a gigantic ModuleScript in it with the reflection database. This workaround was present in some form in many versions of Rojo, and I guess we still need it. This time, I actually documented why it's here so that I don't forget for the umpteenth time... * Add placeholder test that needs to happen still * Introduce easier plugin testing, write applyPatch properties test * Delete legacy get/setCanonicalProperty files * Fix trying to remove numbers instead of instances * Change applyPatch to return partial patches instead of binary success * Work towards being able to decode and apply refs * Add helpers for PatchSet assertions * Apply refs in reify, test all cases * Improve diagnostics when patches fail to apply * Stop logging when destroying untracked instances, it's ok * Remove read before setting property in applyPatch * Fix diff thinking all properties are changed
This commit is contained in:
committed by
GitHub
parent
50f0a2bd2e
commit
f66860bdfe
352
plugin/src/Reconciler/reify.spec.lua
Normal file
352
plugin/src/Reconciler/reify.spec.lua
Normal file
@@ -0,0 +1,352 @@
|
||||
return function()
|
||||
local reify = require(script.Parent.reify)
|
||||
|
||||
local PatchSet = require(script.Parent.Parent.PatchSet)
|
||||
local InstanceMap = require(script.Parent.Parent.InstanceMap)
|
||||
local Error = require(script.Parent.Error)
|
||||
|
||||
local function isEmpty(table)
|
||||
return next(table) == nil, "Table was not empty"
|
||||
end
|
||||
|
||||
local function size(dict)
|
||||
local len = 0
|
||||
|
||||
for _ in pairs(dict) do
|
||||
len = len + 1
|
||||
end
|
||||
|
||||
return len
|
||||
end
|
||||
|
||||
it("should throw when given a bogus ID", function()
|
||||
expect(function()
|
||||
reify(InstanceMap.new(), {}, "Hi, mom!", game)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return an error when given bogus class names", function()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
ClassName = "Balogna",
|
||||
Name = "Food",
|
||||
Properties = {},
|
||||
Children = {},
|
||||
},
|
||||
}
|
||||
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT", nil)
|
||||
|
||||
assert(instanceMap:size() == 0, "expected instanceMap to be empty")
|
||||
|
||||
expect(size(unappliedPatch.added)).to.equal(1)
|
||||
expect(unappliedPatch.added["ROOT"]).to.equal(virtualInstances["ROOT"])
|
||||
|
||||
assert(isEmpty(unappliedPatch.removed), "expected no removes")
|
||||
assert(isEmpty(unappliedPatch.updated), "expected no updates")
|
||||
end)
|
||||
|
||||
it("should assign name and properties", function()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
ClassName = "StringValue",
|
||||
Name = "Spaghetti",
|
||||
Properties = {
|
||||
Value = {
|
||||
Type = "String",
|
||||
Value = "Hello, world!",
|
||||
},
|
||||
},
|
||||
Children = {},
|
||||
},
|
||||
}
|
||||
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||
|
||||
local instance = instanceMap.fromIds["ROOT"]
|
||||
expect(instance.ClassName).to.equal("StringValue")
|
||||
expect(instance.Name).to.equal("Spaghetti")
|
||||
expect(instance.Value).to.equal("Hello, world!")
|
||||
|
||||
expect(instanceMap:size()).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should construct children", function()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
ClassName = "Folder",
|
||||
Name = "Parent",
|
||||
Properties = {},
|
||||
Children = {"CHILD"},
|
||||
},
|
||||
|
||||
CHILD = {
|
||||
ClassName = "Folder",
|
||||
Name = "Child",
|
||||
Properties = {},
|
||||
Children = {},
|
||||
},
|
||||
}
|
||||
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||
|
||||
local instance = instanceMap.fromIds["ROOT"]
|
||||
expect(instance.ClassName).to.equal("Folder")
|
||||
expect(instance.Name).to.equal("Parent")
|
||||
|
||||
local child = instance.Child
|
||||
expect(child.ClassName).to.equal("Folder")
|
||||
|
||||
expect(instanceMap:size()).to.equal(2)
|
||||
end)
|
||||
|
||||
it("should still construct parents if children fail", function()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
ClassName = "Folder",
|
||||
Name = "Parent",
|
||||
Properties = {},
|
||||
Children = {"CHILD"},
|
||||
},
|
||||
|
||||
CHILD = {
|
||||
ClassName = "this ain't an Instance",
|
||||
Name = "Child",
|
||||
Properties = {},
|
||||
Children = {},
|
||||
},
|
||||
}
|
||||
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
expect(size(unappliedPatch.added)).to.equal(1)
|
||||
expect(unappliedPatch.added["CHILD"]).to.equal(virtualInstances["CHILD"])
|
||||
assert(isEmpty(unappliedPatch.updated), "expected no updates")
|
||||
assert(isEmpty(unappliedPatch.removed), "expected no removes")
|
||||
|
||||
local instance = instanceMap.fromIds["ROOT"]
|
||||
expect(instance.ClassName).to.equal("Folder")
|
||||
expect(instance.Name).to.equal("Parent")
|
||||
|
||||
expect(#instance:GetChildren()).to.equal(0)
|
||||
expect(instanceMap:size()).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should fail gracefully when setting erroneous properties", function()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
ClassName = "StringValue",
|
||||
Name = "Root",
|
||||
Properties = {
|
||||
Value = {
|
||||
Type = "Vector3",
|
||||
Value = {1, 2, 3},
|
||||
},
|
||||
},
|
||||
Children = {},
|
||||
},
|
||||
}
|
||||
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
local instance = instanceMap.fromIds["ROOT"]
|
||||
expect(instance.ClassName).to.equal("StringValue")
|
||||
expect(instance.Name).to.equal("Root")
|
||||
|
||||
assert(isEmpty(unappliedPatch.added), "expected no additions")
|
||||
expect(#unappliedPatch.updated).to.equal(1)
|
||||
assert(isEmpty(unappliedPatch.removed), "expected no removes")
|
||||
|
||||
local update = unappliedPatch.updated[1]
|
||||
expect(update.id).to.equal("ROOT")
|
||||
expect(size(update.changedProperties)).to.equal(1)
|
||||
|
||||
local property = update.changedProperties["Value"]
|
||||
expect(property).to.equal(virtualInstances["ROOT"].Properties.Value)
|
||||
end)
|
||||
|
||||
-- This is the simplest ref case: ensure that setting a ref property that
|
||||
-- points to an instance that was previously created as part of the same
|
||||
-- reify operation works.
|
||||
it("should apply properties containing refs to ancestors", function()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
ClassName = "Folder",
|
||||
Name = "Root",
|
||||
Properties = {},
|
||||
Children = {"CHILD"},
|
||||
},
|
||||
|
||||
CHILD = {
|
||||
ClassName = "ObjectValue",
|
||||
Name = "Child",
|
||||
Properties = {
|
||||
Value = {
|
||||
Type = "Ref",
|
||||
Value = "ROOT",
|
||||
},
|
||||
},
|
||||
Children = {},
|
||||
},
|
||||
}
|
||||
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||
|
||||
local root = instanceMap.fromIds["ROOT"]
|
||||
local child = instanceMap.fromIds["CHILD"]
|
||||
expect(child.Value).to.equal(root)
|
||||
end)
|
||||
|
||||
-- This is another simple case: apply a ref property that points to an
|
||||
-- existing instance. In this test, that instance was created before the
|
||||
-- reify operation started and is present in instanceMap.
|
||||
it("should apply properties containing refs to previously-existing instances", function()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
ClassName = "ObjectValue",
|
||||
Name = "Root",
|
||||
Properties = {
|
||||
Value = {
|
||||
Type = "Ref",
|
||||
Value = "EXISTING",
|
||||
},
|
||||
},
|
||||
Children = {},
|
||||
},
|
||||
}
|
||||
|
||||
local instanceMap = InstanceMap.new()
|
||||
|
||||
local existing = Instance.new("Folder")
|
||||
existing.Name = "Existing"
|
||||
instanceMap:insert("EXISTING", existing)
|
||||
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||
|
||||
local root = instanceMap.fromIds["ROOT"]
|
||||
expect(root.Value).to.equal(existing)
|
||||
end)
|
||||
|
||||
-- This is a tricky ref case: CHILD_A points to CHILD_B, but is constructed
|
||||
-- first. Deferred ref application is required to implement this case
|
||||
-- correctly.
|
||||
it("should apply properties containing refs to later siblings correctly", function()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
ClassName = "Folder",
|
||||
Name = "Root",
|
||||
Properties = {},
|
||||
Children = {"CHILD_A", "CHILD_B"},
|
||||
},
|
||||
|
||||
CHILD_A = {
|
||||
ClassName = "ObjectValue",
|
||||
Name = "Child A",
|
||||
Properties = {
|
||||
Value = {
|
||||
Type = "Ref",
|
||||
Value = "Child B",
|
||||
},
|
||||
},
|
||||
Children = {},
|
||||
},
|
||||
|
||||
CHILD_B = {
|
||||
ClassName = "Folder",
|
||||
Name = "Child B",
|
||||
Properties = {},
|
||||
Children = {},
|
||||
},
|
||||
}
|
||||
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||
|
||||
local childA = instanceMap.fromIds["CHILD_A"]
|
||||
local childB = instanceMap.fromIds["CHILD_B"]
|
||||
expect(childA.Value).to.equal(childB)
|
||||
end)
|
||||
|
||||
-- This is the classic case that calls for deferred ref application. In this
|
||||
-- test, the root instance has a ref property that refers to its child. The
|
||||
-- root is definitely constructed first.
|
||||
--
|
||||
-- This is distinct from the sibling case in that the child will be
|
||||
-- constructed as part of a recursive call before the parent has totally
|
||||
-- finished. Given deferred refs, this should not fail, but it is a good
|
||||
-- case to test.
|
||||
it("should apply properties containing refs to later siblings correctly", function()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
ClassName = "ObjectValue",
|
||||
Name = "Root",
|
||||
Properties = {
|
||||
Value = {
|
||||
Type = "Ref",
|
||||
Value = "CHILD",
|
||||
},
|
||||
},
|
||||
Children = {"CHILD"},
|
||||
},
|
||||
|
||||
CHILD = {
|
||||
ClassName = "Folder",
|
||||
Name = "Child",
|
||||
Properties = {},
|
||||
Children = {},
|
||||
},
|
||||
}
|
||||
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||
|
||||
local root = instanceMap.fromIds["ROOT"]
|
||||
local child = instanceMap.fromIds["CHILD"]
|
||||
expect(root.Value).to.equal(child)
|
||||
end)
|
||||
|
||||
it("should return a partial patch when applying invalid refs", function()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
ClassName = "ObjectValue",
|
||||
Name = "Root",
|
||||
Properties = {
|
||||
Value = {
|
||||
Type = "Ref",
|
||||
Value = "SORRY",
|
||||
},
|
||||
},
|
||||
Children = {},
|
||||
},
|
||||
}
|
||||
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
assert(not PatchSet.hasRemoves(unappliedPatch), "expected no removes")
|
||||
assert(not PatchSet.hasAdditions(unappliedPatch), "expected no additions")
|
||||
expect(#unappliedPatch.updated).to.equal(1)
|
||||
|
||||
local update = unappliedPatch.updated[1]
|
||||
expect(update.id).to.equal("ROOT")
|
||||
expect(update.changedProperties.Value).to.equal(virtualInstances["ROOT"].Properties.Value)
|
||||
end)
|
||||
end
|
||||
Reference in New Issue
Block a user