forked from rojo-rbx/rojo
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
292
plugin/src/Reconciler/diff.spec.lua
Normal file
292
plugin/src/Reconciler/diff.spec.lua
Normal file
@@ -0,0 +1,292 @@
|
||||
return function()
|
||||
local Log = require(script.Parent.Parent.Parent.Log)
|
||||
|
||||
local InstanceMap = require(script.Parent.Parent.InstanceMap)
|
||||
local PatchSet = require(script.Parent.Parent.PatchSet)
|
||||
|
||||
local diff = require(script.Parent.diff)
|
||||
|
||||
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 generate an empty patch for empty instances", function()
|
||||
local knownInstances = InstanceMap.new()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
ClassName = "Folder",
|
||||
Name = "Some Name",
|
||||
Properties = {},
|
||||
Children = {},
|
||||
},
|
||||
}
|
||||
|
||||
local rootInstance = Instance.new("Folder")
|
||||
rootInstance.Name = "Some Name"
|
||||
knownInstances:insert("ROOT", rootInstance)
|
||||
|
||||
local ok, patch = diff(knownInstances, virtualInstances, "ROOT")
|
||||
|
||||
assert(ok, tostring(patch))
|
||||
|
||||
assert(isEmpty(patch.removed))
|
||||
assert(isEmpty(patch.added))
|
||||
assert(isEmpty(patch.updated))
|
||||
end)
|
||||
|
||||
it("should generate a patch with a changed name", function()
|
||||
local knownInstances = InstanceMap.new()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
ClassName = "Folder",
|
||||
Name = "Some Name",
|
||||
Properties = {},
|
||||
Children = {},
|
||||
},
|
||||
}
|
||||
|
||||
local rootInstance = Instance.new("Folder")
|
||||
knownInstances:insert("ROOT", rootInstance)
|
||||
|
||||
local ok, patch = diff(knownInstances, virtualInstances, "ROOT")
|
||||
|
||||
assert(ok, tostring(patch))
|
||||
|
||||
assert(isEmpty(patch.removed))
|
||||
assert(isEmpty(patch.added))
|
||||
expect(#patch.updated).to.equal(1)
|
||||
|
||||
local update = patch.updated[1]
|
||||
expect(update.id).to.equal("ROOT")
|
||||
expect(update.changedName).to.equal("Some Name")
|
||||
assert(isEmpty(update.changedProperties))
|
||||
end)
|
||||
|
||||
it("should generate a patch with a changed property", function()
|
||||
local knownInstances = InstanceMap.new()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
ClassName = "StringValue",
|
||||
Name = "Value",
|
||||
Properties = {
|
||||
Value = {
|
||||
Type = "String",
|
||||
Value = "Hello, world!",
|
||||
},
|
||||
},
|
||||
Children = {},
|
||||
},
|
||||
}
|
||||
|
||||
local rootInstance = Instance.new("StringValue")
|
||||
rootInstance.Value = "Initial Value"
|
||||
knownInstances:insert("ROOT", rootInstance)
|
||||
|
||||
local ok, patch = diff(knownInstances, virtualInstances, "ROOT")
|
||||
|
||||
assert(ok, tostring(patch))
|
||||
|
||||
assert(isEmpty(patch.removed))
|
||||
assert(isEmpty(patch.added))
|
||||
expect(#patch.updated).to.equal(1)
|
||||
|
||||
local update = patch.updated[1]
|
||||
expect(update.id).to.equal("ROOT")
|
||||
expect(update.changedName).to.equal(nil)
|
||||
expect(size(update.changedProperties)).to.equal(1)
|
||||
|
||||
local patchProperty = update.changedProperties["Value"]
|
||||
expect(patchProperty).to.be.a("table")
|
||||
expect(patchProperty.Type).to.equal("String")
|
||||
expect(patchProperty.Value).to.equal("Hello, world!")
|
||||
end)
|
||||
|
||||
it("should generate an empty patch if no properties changed", function()
|
||||
local knownInstances = InstanceMap.new()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
ClassName = "StringValue",
|
||||
Name = "Value",
|
||||
Properties = {
|
||||
Value = {
|
||||
Type = "String",
|
||||
Value = "Hello, world!",
|
||||
},
|
||||
},
|
||||
Children = {},
|
||||
},
|
||||
}
|
||||
|
||||
local rootInstance = Instance.new("StringValue")
|
||||
rootInstance.Value = "Hello, world!"
|
||||
knownInstances:insert("ROOT", rootInstance)
|
||||
|
||||
local ok, patch = diff(knownInstances, virtualInstances, "ROOT")
|
||||
|
||||
assert(ok, tostring(patch))
|
||||
assert(PatchSet.isEmpty(patch), "expected empty patch")
|
||||
end)
|
||||
|
||||
it("should ignore unknown properties", function()
|
||||
local knownInstances = InstanceMap.new()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
ClassName = "Folder",
|
||||
Name = "Folder",
|
||||
Properties = {
|
||||
FAKE_PROPERTY = {
|
||||
Type = "String",
|
||||
Value = "Hello, world!",
|
||||
},
|
||||
},
|
||||
Children = {},
|
||||
},
|
||||
}
|
||||
|
||||
local rootInstance = Instance.new("Folder")
|
||||
knownInstances:insert("ROOT", rootInstance)
|
||||
|
||||
local ok, patch = diff(knownInstances, virtualInstances, "ROOT")
|
||||
|
||||
assert(ok, tostring(patch))
|
||||
|
||||
assert(isEmpty(patch.removed))
|
||||
assert(isEmpty(patch.added))
|
||||
assert(isEmpty(patch.updated))
|
||||
end)
|
||||
|
||||
--[[
|
||||
Because rbx_dom_lua resolves non-canonical properties to their canonical
|
||||
variants, this test does not work as intended.
|
||||
|
||||
Instead, heat_xml is diffed with Heat, the canonical property variant,
|
||||
and a patch trying to assign to heat_xml is generated. This is
|
||||
incorrect, but will require more invasive changes to fix later.
|
||||
]]
|
||||
itFIXME("should ignore unreadable properties", function()
|
||||
local knownInstances = InstanceMap.new()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
ClassName = "Fire",
|
||||
Name = "Fire",
|
||||
Properties = {
|
||||
-- heat_xml is a serialization-only property that is not
|
||||
-- exposed to Lua.
|
||||
heat_xml = {
|
||||
Type = "Float32",
|
||||
Value = 5,
|
||||
},
|
||||
},
|
||||
Children = {},
|
||||
},
|
||||
}
|
||||
|
||||
local rootInstance = Instance.new("Fire")
|
||||
knownInstances:insert("ROOT", rootInstance)
|
||||
|
||||
local ok, patch = diff(knownInstances, virtualInstances, "ROOT")
|
||||
|
||||
Log.warn("{:#?}", patch)
|
||||
|
||||
assert(ok, tostring(patch))
|
||||
|
||||
assert(isEmpty(patch.removed))
|
||||
assert(isEmpty(patch.added))
|
||||
assert(isEmpty(patch.updated))
|
||||
end)
|
||||
|
||||
it("should generate a patch removing unknown children by default", function()
|
||||
local knownInstances = InstanceMap.new()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
ClassName = "Folder",
|
||||
Name = "Folder",
|
||||
Properties = {},
|
||||
Children = {},
|
||||
},
|
||||
}
|
||||
|
||||
local rootInstance = Instance.new("Folder")
|
||||
local unknownChild = Instance.new("Folder")
|
||||
unknownChild.Parent = rootInstance
|
||||
knownInstances:insert("ROOT", rootInstance)
|
||||
|
||||
local ok, patch = diff(knownInstances, virtualInstances, "ROOT")
|
||||
|
||||
assert(ok, tostring(patch))
|
||||
|
||||
assert(isEmpty(patch.added))
|
||||
assert(isEmpty(patch.updated))
|
||||
expect(#patch.removed).to.equal(1)
|
||||
expect(patch.removed[1]).to.equal(unknownChild)
|
||||
end)
|
||||
|
||||
it("should generate an empty patch if unknown children should be ignored", function()
|
||||
local knownInstances = InstanceMap.new()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
ClassName = "Folder",
|
||||
Name = "Folder",
|
||||
Properties = {},
|
||||
Children = {},
|
||||
Metadata = {
|
||||
ignoreUnknownInstances = true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local rootInstance = Instance.new("Folder")
|
||||
local unknownChild = Instance.new("Folder")
|
||||
unknownChild.Parent = rootInstance
|
||||
knownInstances:insert("ROOT", rootInstance)
|
||||
|
||||
local ok, patch = diff(knownInstances, virtualInstances, "ROOT")
|
||||
|
||||
assert(ok, tostring(patch))
|
||||
|
||||
assert(isEmpty(patch.added))
|
||||
assert(isEmpty(patch.updated))
|
||||
assert(isEmpty(patch.removed))
|
||||
end)
|
||||
|
||||
it("should generate a patch with an added child", function()
|
||||
local knownInstances = InstanceMap.new()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
ClassName = "Folder",
|
||||
Name = "Folder",
|
||||
Properties = {},
|
||||
Children = {"CHILD"},
|
||||
},
|
||||
|
||||
CHILD = {
|
||||
ClassName = "Folder",
|
||||
Name = "Child",
|
||||
Properties = {},
|
||||
Children = {},
|
||||
},
|
||||
}
|
||||
|
||||
local rootInstance = Instance.new("Folder")
|
||||
knownInstances:insert("ROOT", rootInstance)
|
||||
|
||||
local ok, patch = diff(knownInstances, virtualInstances, "ROOT")
|
||||
|
||||
assert(ok, tostring(patch))
|
||||
|
||||
assert(isEmpty(patch.updated))
|
||||
assert(isEmpty(patch.removed))
|
||||
expect(size(patch.added)).to.equal(1)
|
||||
expect(patch.added["CHILD"]).to.equal(virtualInstances["CHILD"])
|
||||
end)
|
||||
end
|
||||
Reference in New Issue
Block a user