Files
rojo/plugin/src/Reconciler/diff.spec.lua
Lucien Greathouse f66860bdfe 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
2020-11-11 16:30:23 -08:00

292 lines
7.1 KiB
Lua

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