Files
rojo/plugin/src/Reconciler/reify.spec.lua
Kenneth Loeffler 9d0b313261 Add ChangeBatcher to plugin for two-way sync (#478)
* Implement ChangeBatcher

* Use ChangeBatcher for two-way sync

* Pause updates during patch application

* I can English good

* Break after encountering a nil Parent change

This prevents __flush from erroring out when an instance's Parent is
changed to nil and it has other property changes in the same batch.

* Update rbx_dom_lua

* Don't connect changed listeners in a running game

 #468 made me realize how bad of an idea this is in general...

* Update TestEZ and fix sibling Ref reification test

* Add ChangeBatcher tests

* Test instance unpausing by breaking functionality out to __cycle

* Break up the module a bit and improve tests

* Shuffle requires around and edit comment

* Break out more stuff, rename createChangePatch -> createPatchSet

* Make ChangeBatcher responsible for unpausing all paused instances

This somewhat improves the situation (of course, it would preferrable
to not have to hack around this problem with Source at all). It also
sets us up nicely if we come across any other properties that do
anything similar.

* Remove old reference to pausedBatchInstances

* Use RenderStepped instead of Heartbeat and trash multi-frame pauses

I probably should have done this in the first place...

ChangeBatcher still needs to unpause instances, but we don't need to
hold pauses for any longer than one cycle.

* Remove useless branch

* if not next(x) -> if next(x) == nil

* Add InstanceMap:unpauseAllInstances, use it in ChangeBatcher

* Move IsRunning check to InstanceMap:__maybeFireInstanceChanged
2021-10-18 18:18:51 -04:00

348 lines
9.1 KiB
Lua

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 = {
String = "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 = {
Ref = "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 = {
Ref = "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 = {
Ref = "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 children correctly", function()
local virtualInstances = {
ROOT = {
ClassName = "ObjectValue",
Name = "Root",
Properties = {
Value = {
Ref = "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