forked from rojo-rbx/rojo
* 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
292 lines
7.1 KiB
Lua
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 |