Add AZ::Dom::Patch, a Generic DOM analog to JSON patch

- Currently supports JSON patch operations (add/remove/replace/copy/move/test)
- `GenerateHierarchicalDeltaPatch` provides a patch generation mechanism that produces forward and inverse patches
- Patch application comes with a `PatchApplicationStrategy` functor that allows customizing behavior for patch failure that may be useful for the prefab system
- Serialization to/from JSON patch by way of `AZ::Dom::Value` is supported

Benchmarks provided, split into three categories on the Dom value benchmark payload (payloads with up to 10k entries populated with strings up to 100 characters in length):
- Patch generation based on a deep copy of the affected data
```
DomPatchBenchmark/AzDomPatch_Generate_SimpleReplace_DeepCopy/10/5            0.024 ms        0.024 ms        29867 items_per_second=41.5541k/s
DomPatchBenchmark/AzDomPatch_Generate_SimpleReplace_DeepCopy/10/500          0.024 ms        0.024 ms        29867 items_per_second=41.5541k/s
DomPatchBenchmark/AzDomPatch_Generate_SimpleReplace_DeepCopy/100/5           0.346 ms        0.345 ms         2036 items_per_second=2.89564k/s
DomPatchBenchmark/AzDomPatch_Generate_SimpleReplace_DeepCopy/100/500         0.375 ms        0.377 ms         1867 items_per_second=2.65529k/s
DomPatchBenchmark/AzDomPatch_Generate_TopLevelReplace/10/5                   0.003 ms        0.003 ms       203636 items_per_second=289.616k/s
DomPatchBenchmark/AzDomPatch_Generate_TopLevelReplace/10/500                 0.004 ms        0.004 ms       194783 items_per_second=283.321k/s
DomPatchBenchmark/AzDomPatch_Generate_TopLevelReplace/100/5                  0.003 ms        0.003 ms       203636 items_per_second=289.616k/s
DomPatchBenchmark/AzDomPatch_Generate_TopLevelReplace/100/500                0.004 ms        0.004 ms       194783 items_per_second=271.002k/s
DomPatchBenchmark/AzDomPatch_Generate_KeyRemove_DeepCopy/10/5                0.023 ms        0.024 ms        29867 items_per_second=42.4775k/s
DomPatchBenchmark/AzDomPatch_Generate_KeyRemove_DeepCopy/10/500              0.023 ms        0.023 ms        28000 items_per_second=42.6667k/s
DomPatchBenchmark/AzDomPatch_Generate_KeyRemove_DeepCopy/100/5               0.341 ms        0.337 ms         2133 items_per_second=2.96765k/s
DomPatchBenchmark/AzDomPatch_Generate_KeyRemove_DeepCopy/100/500             0.365 ms        0.361 ms         1948 items_per_second=2.77049k/s
DomPatchBenchmark/AzDomPatch_Generate_ArrayAppend_DeepCopy/10/5              0.023 ms        0.023 ms        29867 items_per_second=43.4429k/s
DomPatchBenchmark/AzDomPatch_Generate_ArrayAppend_DeepCopy/10/500            0.023 ms        0.024 ms        29867 items_per_second=42.4775k/s
DomPatchBenchmark/AzDomPatch_Generate_ArrayAppend_DeepCopy/100/5             0.330 ms        0.330 ms         2133 items_per_second=3.0336k/s
DomPatchBenchmark/AzDomPatch_Generate_ArrayAppend_DeepCopy/100/500           0.359 ms        0.360 ms         1867 items_per_second=2.77879k/s
DomPatchBenchmark/AzDomPatch_Generate_ArrayPrepend_DeepCopy/10/5             0.023 ms        0.022 ms        29867 items_per_second=44.4532k/s
DomPatchBenchmark/AzDomPatch_Generate_ArrayPrepend_DeepCopy/10/500           0.023 ms        0.023 ms        32000 items_per_second=43.5745k/s
DomPatchBenchmark/AzDomPatch_Generate_ArrayPrepend_DeepCopy/100/5            0.329 ms        0.330 ms         2133 items_per_second=3.0336k/s
DomPatchBenchmark/AzDomPatch_Generate_ArrayPrepend_DeepCopy/100/500          0.357 ms        0.361 ms         1948 items_per_second=2.77049k/s
```
- Patch generation based on a shallow copy of the affected data (this is faster because when using Dom::Value to copy and mutate, we can bypass expensive array and object comparisons for identical values)
```
DomPatchBenchmark/AzDomPatch_Generate_SimpleReplace_ShallowCopy/10/5         0.010 ms        0.010 ms        74667 items_per_second=99.556k/s
DomPatchBenchmark/AzDomPatch_Generate_SimpleReplace_ShallowCopy/10/500       0.010 ms        0.010 ms        74667 items_per_second=97.5242k/s
DomPatchBenchmark/AzDomPatch_Generate_SimpleReplace_ShallowCopy/100/5        0.079 ms        0.078 ms         8960 items_per_second=12.7431k/s
DomPatchBenchmark/AzDomPatch_Generate_SimpleReplace_ShallowCopy/100/500      0.087 ms        0.087 ms         8960 items_per_second=11.4688k/s
DomPatchBenchmark/AzDomPatch_Generate_KeyRemove_ShallowCopy/10/5             0.009 ms        0.009 ms        74667 items_per_second=116.553k/s
DomPatchBenchmark/AzDomPatch_Generate_KeyRemove_ShallowCopy/10/500           0.009 ms        0.009 ms        74667 items_per_second=113.778k/s
DomPatchBenchmark/AzDomPatch_Generate_KeyRemove_ShallowCopy/100/5            0.072 ms        0.071 ms         8960 items_per_second=13.9863k/s
DomPatchBenchmark/AzDomPatch_Generate_KeyRemove_ShallowCopy/100/500          0.087 ms        0.088 ms         7467 items_per_second=11.3783k/s
DomPatchBenchmark/AzDomPatch_Generate_ArrayAppend_ShallowCopy/10/5           0.014 ms        0.014 ms        49778 items_per_second=72.4044k/s
DomPatchBenchmark/AzDomPatch_Generate_ArrayAppend_ShallowCopy/10/500         0.014 ms        0.014 ms        56000 items_per_second=70.2745k/s
DomPatchBenchmark/AzDomPatch_Generate_ArrayAppend_ShallowCopy/100/5          0.118 ms        0.117 ms         5600 items_per_second=8.53333k/s
DomPatchBenchmark/AzDomPatch_Generate_ArrayAppend_ShallowCopy/100/500        0.140 ms        0.141 ms         4978 items_per_second=7.07982k/s
DomPatchBenchmark/AzDomPatch_Generate_ArrayPrepend_ShallowCopy/10/5          0.009 ms        0.009 ms        89600 items_per_second=108.196k/s
DomPatchBenchmark/AzDomPatch_Generate_ArrayPrepend_ShallowCopy/10/500        0.009 ms        0.009 ms        74667 items_per_second=108.607k/s
DomPatchBenchmark/AzDomPatch_Generate_ArrayPrepend_ShallowCopy/100/5         0.068 ms        0.068 ms        11200 items_per_second=14.6286k/s
DomPatchBenchmark/AzDomPatch_Generate_ArrayPrepend_ShallowCopy/100/500       0.082 ms        0.082 ms         8960 items_per_second=12.2009k/s
```
- Patch application
```
DomPatchBenchmark/AzDomPatch_Apply_SimpleReplace_ShallowCopy/10/5            0.001 ms        0.001 ms       560000 items_per_second=874.146k/s
DomPatchBenchmark/AzDomPatch_Apply_SimpleReplace_ShallowCopy/10/500          0.001 ms        0.001 ms       560000 items_per_second=874.146k/s
DomPatchBenchmark/AzDomPatch_Apply_SimpleReplace_ShallowCopy/100/5           0.004 ms        0.004 ms       172308 items_per_second=250.63k/s
DomPatchBenchmark/AzDomPatch_Apply_SimpleReplace_ShallowCopy/100/500         0.004 ms        0.004 ms       179200 items_per_second=260.655k/s
DomPatchBenchmark/AzDomPatch_Apply_SimpleReplace_DeepCopy/10/5               0.005 ms        0.005 ms       112000 items_per_second=193.73k/s
DomPatchBenchmark/AzDomPatch_Apply_SimpleReplace_DeepCopy/10/500             0.005 ms        0.005 ms       112000 items_per_second=193.73k/s
DomPatchBenchmark/AzDomPatch_Apply_SimpleReplace_DeepCopy/100/5              0.052 ms        0.052 ms        10000 items_per_second=19.3939k/s
DomPatchBenchmark/AzDomPatch_Apply_SimpleReplace_DeepCopy/100/500            0.052 ms        0.052 ms        11200 items_per_second=19.373k/s
DomPatchBenchmark/AzDomPatch_Apply_TopLevelReplace/10/5                      0.001 ms        0.001 ms       640000 items_per_second=910.222k/s
DomPatchBenchmark/AzDomPatch_Apply_TopLevelReplace/10/500                    0.001 ms        0.001 ms       640000 items_per_second=910.222k/s
DomPatchBenchmark/AzDomPatch_Apply_TopLevelReplace/100/5                     0.001 ms        0.001 ms       640000 items_per_second=910.222k/s
DomPatchBenchmark/AzDomPatch_Apply_TopLevelReplace/100/500                   0.001 ms        0.001 ms       640000 items_per_second=910.222k/s
DomPatchBenchmark/AzDomPatch_Apply_KeyRemove_ShallowCopy/10/5                0.001 ms        0.001 ms       560000 items_per_second=896k/s
DomPatchBenchmark/AzDomPatch_Apply_KeyRemove_ShallowCopy/10/500              0.001 ms        0.001 ms       640000 items_per_second=871.489k/s
DomPatchBenchmark/AzDomPatch_Apply_KeyRemove_ShallowCopy/100/5               0.004 ms        0.004 ms       160000 items_per_second=232.727k/s
DomPatchBenchmark/AzDomPatch_Apply_KeyRemove_ShallowCopy/100/500             0.004 ms        0.004 ms       165926 items_per_second=235.984k/s
DomPatchBenchmark/AzDomPatch_Apply_KeyRemove_DeepCopy/10/5                   0.005 ms        0.005 ms       112000 items_per_second=193.73k/s
DomPatchBenchmark/AzDomPatch_Apply_KeyRemove_DeepCopy/10/500                 0.005 ms        0.005 ms       112000 items_per_second=193.73k/s
DomPatchBenchmark/AzDomPatch_Apply_KeyRemove_DeepCopy/100/5                  0.053 ms        0.053 ms        10000 items_per_second=18.8235k/s
DomPatchBenchmark/AzDomPatch_Apply_KeyRemove_DeepCopy/100/500                0.051 ms        0.052 ms        10000 items_per_second=19.3939k/s
DomPatchBenchmark/AzDomPatch_Apply_ArrayAppend_ShallowCopy/10/5              0.001 ms        0.001 ms       497778 items_per_second=692.561k/s
DomPatchBenchmark/AzDomPatch_Apply_ArrayAppend_ShallowCopy/10/500            0.001 ms        0.001 ms       497778 items_per_second=692.561k/s
DomPatchBenchmark/AzDomPatch_Apply_ArrayAppend_ShallowCopy/100/5             0.006 ms        0.006 ms       112000 items_per_second=174.829k/s
DomPatchBenchmark/AzDomPatch_Apply_ArrayAppend_ShallowCopy/100/500           0.006 ms        0.006 ms       100000 items_per_second=177.778k/s
DomPatchBenchmark/AzDomPatch_Apply_ArrayAppend_DeepCopy/10/5                 0.005 ms        0.005 ms       112000 items_per_second=193.73k/s
DomPatchBenchmark/AzDomPatch_Apply_ArrayAppend_DeepCopy/10/500               0.005 ms        0.005 ms       100000 items_per_second=188.235k/s
DomPatchBenchmark/AzDomPatch_Apply_ArrayAppend_DeepCopy/100/5                0.053 ms        0.052 ms        11200 items_per_second=19.373k/s
DomPatchBenchmark/AzDomPatch_Apply_ArrayAppend_DeepCopy/100/500              0.052 ms        0.053 ms        11200 items_per_second=18.8632k/s
DomPatchBenchmark/AzDomPatch_Apply_ArrayPrepend_ShallowCopy/10/5             0.001 ms        0.001 ms       560000 items_per_second=874.146k/s
DomPatchBenchmark/AzDomPatch_Apply_ArrayPrepend_ShallowCopy/10/500           0.001 ms        0.001 ms       560000 items_per_second=874.146k/s
DomPatchBenchmark/AzDomPatch_Apply_ArrayPrepend_ShallowCopy/100/5            0.004 ms        0.004 ms       179200 items_per_second=260.655k/s
DomPatchBenchmark/AzDomPatch_Apply_ArrayPrepend_ShallowCopy/100/500          0.004 ms        0.004 ms       179200 items_per_second=260.655k/s
DomPatchBenchmark/AzDomPatch_Apply_ArrayPrepend_DeepCopy/10/5                0.005 ms        0.005 ms       100000 items_per_second=193.939k/s
DomPatchBenchmark/AzDomPatch_Apply_ArrayPrepend_DeepCopy/10/500              0.005 ms        0.005 ms       100000 items_per_second=193.939k/s
DomPatchBenchmark/AzDomPatch_Apply_ArrayPrepend_DeepCopy/100/5               0.052 ms        0.052 ms        11200 items_per_second=19.373k/s
DomPatchBenchmark/AzDomPatch_Apply_ArrayPrepend_DeepCopy/100/500             0.053 ms        0.053 ms        10000 items_per_second=18.8235k/s
```

At a glance, patch generation using `GenerateHierarchicalDeltaPatch` is slower than applying its created patches, but not prohibitively so. Ideally patches shouldn't be recreated unnecessarily, but especially when diffing `Value`s that have been copied and then mutated, a generate + apply operation similar to what prefabs currently do is reasonably fast.

Signed-off-by: Nicholas Van Sickle <nvsickle@amazon.com>
This commit is contained in:
Nicholas Van Sickle
2022-01-21 15:55:37 -08:00
parent 32e2ba754b
commit e06b8782cc
7 changed files with 1904 additions and 6 deletions
@@ -0,0 +1,179 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/DOM/DomPatch.h>
#include <AzCore/DOM/DomUtils.h>
#include <AzCore/DOM/DomValue.h>
#include <AzCore/Name/NameDictionary.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <Tests/DOM/DomFixtures.h>
namespace AZ::Dom::Benchmark
{
class DomPatchBenchmark : public Tests::DomBenchmarkFixture
{
public:
void TearDownHarness() override
{
m_before = {};
m_after = {};
Tests::DomBenchmarkFixture::TearDownHarness();
}
void SimpleReplace(benchmark::State& state, bool deepCopy, bool apply)
{
m_before = GenerateDomBenchmarkPayload(state.range(0), state.range(1));
m_after = deepCopy ? Utils::DeepCopy(m_before) : m_before;
m_after["entries"]["Key0"] = Value("replacement string", true);
RunBenchmarkInternal(state, apply);
}
void TopLevelReplace(benchmark::State& state, bool apply)
{
m_before = GenerateDomBenchmarkPayload(state.range(0), state.range(1));
m_after = Value(Type::Object);
m_after["UnrelatedKey"] = Value(42);
RunBenchmarkInternal(state, apply);
}
void KeyRemove(benchmark::State& state, bool deepCopy, bool apply)
{
m_before = GenerateDomBenchmarkPayload(state.range(0), state.range(1));
m_after = deepCopy ? Utils::DeepCopy(m_before) : m_before;
m_after["entries"].RemoveMember("Key1");
RunBenchmarkInternal(state, apply);
}
void ArrayAppend(benchmark::State& state, bool deepCopy, bool apply)
{
m_before = GenerateDomBenchmarkPayload(state.range(0), state.range(1));
m_after = deepCopy ? Utils::DeepCopy(m_before) : m_before;
m_after["entries"]["Key2"].ArrayPushBack(Value(0));
RunBenchmarkInternal(state, apply);
}
void ArrayPrepend(benchmark::State& state, bool deepCopy, bool apply)
{
m_before = GenerateDomBenchmarkPayload(state.range(0), state.range(1));
m_after = deepCopy ? Utils::DeepCopy(m_before) : m_before;
auto& arr = m_after["entries"]["Key2"].GetMutableArray();
arr.insert(arr.begin(), Value(42));
RunBenchmarkInternal(state, apply);
}
private:
void RunBenchmarkInternal(benchmark::State& state, bool apply)
{
if (apply)
{
auto patchInfo = GenerateHierarchicalDeltaPatch(m_before, m_after);
for (auto _ : state)
{
auto patchResult = patchInfo.m_forwardPatches.Apply(m_before);
benchmark::DoNotOptimize(patchResult);
}
}
else
{
for (auto _ : state)
{
auto patchInfo = GenerateHierarchicalDeltaPatch(m_before, m_after);
benchmark::DoNotOptimize(patchInfo);
}
}
state.SetItemsProcessed(state.iterations());
}
Value m_before;
Value m_after;
};
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Generate_SimpleReplace_ShallowCopy)(benchmark::State& state)
{
SimpleReplace(state, false, false);
}
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Generate_SimpleReplace_ShallowCopy)
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Generate_SimpleReplace_DeepCopy)(benchmark::State& state)
{
SimpleReplace(state, true, false);
}
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Generate_SimpleReplace_DeepCopy)
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Generate_TopLevelReplace)(benchmark::State& state)
{
TopLevelReplace(state, false);
}
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Generate_TopLevelReplace)
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Generate_KeyRemove_ShallowCopy)(benchmark::State& state)
{
KeyRemove(state, false, false);
}
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Generate_KeyRemove_ShallowCopy)
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Generate_KeyRemove_DeepCopy)(benchmark::State& state)
{
KeyRemove(state, true, false);
}
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Generate_KeyRemove_DeepCopy)
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Generate_ArrayAppend_ShallowCopy)(benchmark::State& state)
{
ArrayAppend(state, false, false);
}
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Generate_ArrayAppend_ShallowCopy)
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Generate_ArrayAppend_DeepCopy)(benchmark::State& state)
{
ArrayAppend(state, true, false);
}
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Generate_ArrayAppend_DeepCopy)
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Generate_ArrayPrepend)(benchmark::State& state)
{
ArrayPrepend(state, true, false);
}
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Generate_ArrayPrepend)
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Apply_SimpleReplace)(benchmark::State& state)
{
SimpleReplace(state, true, true);
}
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Apply_SimpleReplace)
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Apply_TopLevelReplace)(benchmark::State& state)
{
TopLevelReplace(state, true);
}
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Apply_TopLevelReplace)
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Apply_KeyRemove)(benchmark::State& state)
{
KeyRemove(state, true, true);
}
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Apply_KeyRemove)
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Apply_ArrayAppend)(benchmark::State& state)
{
ArrayAppend(state, true, true);
}
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Apply_ArrayAppend)
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Apply_ArrayPrepend)(benchmark::State& state)
{
ArrayPrepend(state, true, true);
}
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Apply_ArrayPrepend)
} // namespace AZ::Dom::Benchmark
@@ -0,0 +1,562 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/DOM/DomPatch.h>
#include <Tests/DOM/DomFixtures.h>
namespace AZ::Dom::Tests
{
class DomPatchTests : public DomTestFixture
{
public:
void SetUp() override
{
DomTestFixture::SetUp();
m_dataset = Value(Type::Object);
m_dataset["arr"].SetArray();
m_dataset["node"].SetNode("SomeNode");
m_dataset["node"]["int"] = 5;
m_dataset["node"]["null"] = Value();
for (int i = 0; i < 5; ++i)
{
m_dataset["arr"].ArrayPushBack(Value(i));
m_dataset["node"].ArrayPushBack(Value(i * 2));
}
m_dataset["obj"].SetObject();
m_dataset["obj"]["foo"] = true;
m_dataset["obj"]["bar"] = false;
m_deltaDataset = m_dataset;
}
void TearDown() override
{
m_dataset = m_deltaDataset = Value();
DomTestFixture::TearDown();
}
PatchInfo GenerateAndVerifyDelta()
{
PatchInfo info = GenerateHierarchicalDeltaPatch(m_dataset, m_deltaDataset);
auto result = info.m_forwardPatches.Apply(m_dataset);
EXPECT_TRUE(result.IsSuccess());
EXPECT_TRUE(Utils::DeepCompareIsEqual(result.GetValue(), m_deltaDataset));
result = info.m_inversePatches.Apply(result.GetValue());
EXPECT_TRUE(result.IsSuccess());
EXPECT_TRUE(Utils::DeepCompareIsEqual(result.GetValue(), m_dataset));
// Verify serialization of the patches
auto VerifySerialization = [](const Patch& patch)
{
Value serializedPatch = patch.GetDomRepresentation();
auto deserializePatchResult = Patch::CreateFromDomRepresentation(serializedPatch);
EXPECT_TRUE(deserializePatchResult.IsSuccess());
EXPECT_EQ(deserializePatchResult.GetValue(), patch);
};
VerifySerialization(info.m_forwardPatches);
VerifySerialization(info.m_inversePatches);
return info;
}
Value m_dataset;
Value m_deltaDataset;
};
TEST_F(DomPatchTests, AddOperation_InsertInObject_Succeeds)
{
Path p("/obj/baz");
PatchOperation op = PatchOperation::AddOperation(p, Value(42));
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(result.GetValue()[p].GetInt64(), 42);
}
TEST_F(DomPatchTests, AddOperation_ReplaceInObject_Succeeds)
{
Path p("/obj/foo");
PatchOperation op = PatchOperation::AddOperation(p, Value(false));
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(result.GetValue()[p].GetBool(), false);
}
TEST_F(DomPatchTests, AddOperation_InsertObjectKeyInArray_Fails)
{
Path p("/arr/key");
PatchOperation op = PatchOperation::AddOperation(p, Value(999));
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, AddOperation_AppendInArray_Succeeds)
{
Path p("/arr/-");
PatchOperation op = PatchOperation::AddOperation(p, Value(42));
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(result.GetValue()["arr"][5].GetInt64(), 42);
}
TEST_F(DomPatchTests, AddOperation_InsertKeyInNode_Succeeds)
{
Path p("/node/attr");
PatchOperation op = PatchOperation::AddOperation(p, Value(500));
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(result.GetValue()[p].GetInt64(), 500);
}
TEST_F(DomPatchTests, AddOperation_ReplaceIndexInNode_Succeeds)
{
Path p("/node/0");
PatchOperation op = PatchOperation::AddOperation(p, Value(42));
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(result.GetValue()[p].GetInt64(), 42);
}
TEST_F(DomPatchTests, AddOperation_AppendInNode_Succeeds)
{
Path p("/node/-");
PatchOperation op = PatchOperation::AddOperation(p, Value(42));
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(result.GetValue()["node"][5].GetInt64(), 42);
}
TEST_F(DomPatchTests, AddOperation_InvalidPath_Fails)
{
Path p("/non/existent/path");
PatchOperation op = PatchOperation::AddOperation(p, Value(0));
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, RemoveOperation_RemoveKeyFromObject_Succeeds)
{
Path p("/obj/foo");
PatchOperation op = PatchOperation::RemoveOperation(p);
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
EXPECT_FALSE(result.GetValue()["obj"].HasMember("foo"));
}
TEST_F(DomPatchTests, RemoveOperation_RemoveIndexFromArray_Succeeds)
{
Path p("/arr/0");
PatchOperation op = PatchOperation::RemoveOperation(p);
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(result.GetValue()["arr"].ArraySize(), 4);
EXPECT_EQ(result.GetValue()["arr"][0].GetInt64(), 1);
}
TEST_F(DomPatchTests, RemoveOperation_PopArray_Succeeds)
{
Path p("/arr/-");
PatchOperation op = PatchOperation::RemoveOperation(p);
auto result = op.Apply(m_dataset);
EXPECT_EQ(result.GetValue()["arr"].ArraySize(), 4);
}
TEST_F(DomPatchTests, RemoveOperation_RemoveKeyFromNode_Succeeds)
{
Path p("/node/int");
PatchOperation op = PatchOperation::RemoveOperation(p);
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
EXPECT_FALSE(result.GetValue()["node"].HasMember("int"));
}
TEST_F(DomPatchTests, RemoveOperation_RemoveIndexFromNode_Succeeds)
{
Path p("/node/1");
PatchOperation op = PatchOperation::RemoveOperation(p);
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(result.GetValue()["node"].ArraySize(), 4);
EXPECT_EQ(result.GetValue()["node"][1].GetInt64(), 4);
}
TEST_F(DomPatchTests, RemoveOperation_PopIndexFromNode_Succeeds)
{
Path p("/node/-");
PatchOperation op = PatchOperation::RemoveOperation(p);
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(result.GetValue()["node"].ArraySize(), 4);
}
TEST_F(DomPatchTests, RemoveOperation_RemoveKeyFromArray_Fails)
{
Path p("/arr/foo");
PatchOperation op = PatchOperation::RemoveOperation(p);
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, RemoveOperation_InvalidPath_Fails)
{
Path p("/non/existent/path");
PatchOperation op = PatchOperation::RemoveOperation(p);
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, ReplaceOperation_InsertInObject_Fails)
{
Path p("/obj/baz");
PatchOperation op = PatchOperation::ReplaceOperation(p, Value(42));
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, ReplaceOperation_ReplaceInObject_Succeeds)
{
Path p("/obj/foo");
PatchOperation op = PatchOperation::ReplaceOperation(p, Value(false));
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(result.GetValue()[p].GetBool(), false);
}
TEST_F(DomPatchTests, ReplaceOperation_InsertObjectKeyInArray_Fails)
{
Path p("/arr/key");
PatchOperation op = PatchOperation::ReplaceOperation(p, Value(999));
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, ReplaceOperation_AppendInArray_Fails)
{
Path p("/arr/-");
PatchOperation op = PatchOperation::ReplaceOperation(p, Value(42));
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, ReplaceOperation_InsertKeyInNode_Fails)
{
Path p("/node/attr");
PatchOperation op = PatchOperation::ReplaceOperation(p, Value(500));
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, ReplaceOperation_ReplaceIndexInNode_Succeeds)
{
Path p("/node/0");
PatchOperation op = PatchOperation::ReplaceOperation(p, Value(42));
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(result.GetValue()[p].GetInt64(), 42);
}
TEST_F(DomPatchTests, ReplaceOperation_AppendInNode_Fails)
{
Path p("/node/-");
PatchOperation op = PatchOperation::ReplaceOperation(p, Value(42));
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, ReplaceOperation_InvalidPath_Fails)
{
Path p("/non/existent/path");
PatchOperation op = PatchOperation::ReplaceOperation(p, Value(0));
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, CopyOperation_ArrayToObject_Succeeds)
{
Path dest("/obj/arr");
Path src("/arr");
PatchOperation op = PatchOperation::CopyOperation(dest, src);
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
EXPECT_TRUE(Utils::DeepCompareIsEqual(m_dataset[src], result.GetValue()[dest]));
EXPECT_TRUE(Utils::DeepCompareIsEqual(result.GetValue()[src], result.GetValue()[dest]));
}
TEST_F(DomPatchTests, CopyOperation_ObjectToArrayInRange_Succeeds)
{
Path dest("/arr/0");
Path src("/obj");
PatchOperation op = PatchOperation::CopyOperation(dest, src);
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
EXPECT_TRUE(Utils::DeepCompareIsEqual(m_dataset[src], result.GetValue()[dest]));
EXPECT_TRUE(Utils::DeepCompareIsEqual(result.GetValue()[src], result.GetValue()[dest]));
}
TEST_F(DomPatchTests, CopyOperation_ObjectToArrayOutOfRange_Fails)
{
Path dest("/arr/5");
Path src("/obj");
PatchOperation op = PatchOperation::CopyOperation(dest, src);
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, CopyOperation_ObjectToNodeChildInRange_Succeeds)
{
Path dest("/node/0");
Path src("/obj");
PatchOperation op = PatchOperation::CopyOperation(dest, src);
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
EXPECT_TRUE(Utils::DeepCompareIsEqual(m_dataset[src], result.GetValue()[dest]));
EXPECT_TRUE(Utils::DeepCompareIsEqual(result.GetValue()[src], result.GetValue()[dest]));
}
TEST_F(DomPatchTests, CopyOperation_ObjectToNodeChildOutOfRange_Fails)
{
Path dest("/node/5");
Path src("/obj");
PatchOperation op = PatchOperation::CopyOperation(dest, src);
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, CopyOperation_InvalidSourcePath_Fails)
{
Path dest("/node/0");
Path src("/invalid/path");
PatchOperation op = PatchOperation::CopyOperation(dest, src);
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, CopyOperation_InvalidDestinationPath_Fails)
{
Path dest("/invalid/path");
Path src("/arr/0");
PatchOperation op = PatchOperation::CopyOperation(dest, src);
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, MoveOperation_ArrayToObject_Succeeds)
{
Path dest("/obj/arr");
Path src("/arr");
PatchOperation op = PatchOperation::MoveOperation(dest, src);
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
EXPECT_TRUE(Utils::DeepCompareIsEqual(m_dataset[src], result.GetValue()[dest]));
EXPECT_FALSE(result.GetValue().HasMember("arr"));
}
TEST_F(DomPatchTests, MoveOperation_ObjectToArrayInRange_Succeeds)
{
Path dest("/arr/0");
Path src("/obj");
PatchOperation op = PatchOperation::MoveOperation(dest, src);
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
EXPECT_TRUE(Utils::DeepCompareIsEqual(m_dataset[src], result.GetValue()[dest]));
EXPECT_FALSE(result.GetValue().HasMember("obj"));
}
TEST_F(DomPatchTests, MoveOperation_ObjectToArrayOutOfRange_Fails)
{
Path dest("/arr/5");
Path src("/obj");
PatchOperation op = PatchOperation::MoveOperation(dest, src);
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, MoveOperation_ObjectToNodeChildInRange_Succeeds)
{
Path dest("/node/0");
Path src("/obj");
PatchOperation op = PatchOperation::MoveOperation(dest, src);
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
EXPECT_TRUE(Utils::DeepCompareIsEqual(m_dataset[src], result.GetValue()[dest]));
EXPECT_FALSE(result.GetValue().HasMember("obj"));
}
TEST_F(DomPatchTests, MoveOperation_ObjectToNodeChildOutOfRange_Fails)
{
Path dest("/node/5");
Path src("/obj");
PatchOperation op = PatchOperation::MoveOperation(dest, src);
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, MoveOperation_InvalidSourcePath_Fails)
{
Path dest("/node/0");
Path src("/invalid/path");
PatchOperation op = PatchOperation::MoveOperation(dest, src);
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, MoveOperation_InvalidDestinationPath_Fails)
{
Path dest("/invalid/path");
Path src("/arr/0");
PatchOperation op = PatchOperation::MoveOperation(dest, src);
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, TestOperation_TestCorrectValue_Succeeds)
{
Path path("/arr/1");
Value value(1);
PatchOperation op = PatchOperation::TestOperation(path, value);
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
}
TEST_F(DomPatchTests, TestOperation_TestIncorrectValue_Fails)
{
Path path("/arr/1");
Value value(55);
PatchOperation op = PatchOperation::TestOperation(path, value);
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, TestOperation_TestCorrectComplexValue_Succeeds)
{
Path path;
Value value = m_dataset;
PatchOperation op = PatchOperation::TestOperation(path, value);
auto result = op.Apply(m_dataset);
ASSERT_TRUE(result.IsSuccess());
}
TEST_F(DomPatchTests, TestOperation_TestIncorrectComplexValue_Fails)
{
Path path;
Value value = m_dataset;
value["arr"][4] = 9;
PatchOperation op = PatchOperation::TestOperation(path, value);
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, TestOperation_TestInvalidPath_Fails)
{
Path path("/invalid/path");
Value value;
PatchOperation op = PatchOperation::TestOperation(path, value);
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, TestOperation_TestInsertArrayPath_Fails)
{
Path path("/arr/-");
Value value(4);
PatchOperation op = PatchOperation::TestOperation(path, value);
auto result = op.Apply(m_dataset);
ASSERT_FALSE(result.IsSuccess());
}
TEST_F(DomPatchTests, TestPatch_ReplaceArrayValue)
{
m_deltaDataset["arr"][0] = 5;
GenerateAndVerifyDelta();
}
TEST_F(DomPatchTests, TestPatch_AppendArrayValue)
{
m_deltaDataset["arr"].ArrayPushBack(Value(7));
auto result = GenerateAndVerifyDelta();
// Ensure the generated patch uses the array append operation
ASSERT_EQ(result.m_forwardPatches.Size(), 1);
EXPECT_TRUE(result.m_forwardPatches[0].GetDestinationPath()[1].IsEndOfArray());
}
TEST_F(DomPatchTests, TestPatch_AppendArrayValues)
{
m_deltaDataset["arr"].ArrayPushBack(Value(7));
m_deltaDataset["arr"].ArrayPushBack(Value(8));
m_deltaDataset["arr"].ArrayPushBack(Value(9));
GenerateAndVerifyDelta();
}
TEST_F(DomPatchTests, TestPatch_InsertArrayValue)
{
auto& arr = m_deltaDataset["arr"].GetMutableArray();
arr.insert(arr.begin(), Value(42));
GenerateAndVerifyDelta();
}
TEST_F(DomPatchTests, TestPatch_InsertObjectKey)
{
m_deltaDataset["obj"]["newKey"].CopyFromString("test");
GenerateAndVerifyDelta();
}
TEST_F(DomPatchTests, TestPatch_DeleteObjectKey)
{
m_deltaDataset["obj"].RemoveMember("foo");
GenerateAndVerifyDelta();
}
TEST_F(DomPatchTests, TestPatch_AppendNodeValues)
{
m_deltaDataset["node"].ArrayPushBack(Value(7));
m_deltaDataset["node"].ArrayPushBack(Value(8));
m_deltaDataset["node"].ArrayPushBack(Value(9));
GenerateAndVerifyDelta();
}
TEST_F(DomPatchTests, TestPatch_InsertNodeValue)
{
auto& node = m_deltaDataset["node"].GetMutableNode();
node.GetChildren().insert(node.GetChildren().begin(), Value(42));
GenerateAndVerifyDelta();
}
TEST_F(DomPatchTests, TestPatch_InsertNodeKey)
{
m_deltaDataset["node"]["newKey"].CopyFromString("test");
GenerateAndVerifyDelta();
}
TEST_F(DomPatchTests, TestPatch_DeleteNodeKey)
{
m_deltaDataset["node"].RemoveMember("int");
GenerateAndVerifyDelta();
}
TEST_F(DomPatchTests, TestPatch_RenameNode)
{
m_deltaDataset["node"].SetNodeName("RenamedNode");
GenerateAndVerifyDelta();
}
TEST_F(DomPatchTests, TestPatch_ReplaceRoot)
{
m_deltaDataset = Value(Type::Array);
m_deltaDataset.ArrayPushBack(Value(2));
m_deltaDataset.ArrayPushBack(Value(4));
m_deltaDataset.ArrayPushBack(Value(6));
GenerateAndVerifyDelta();
}
} // namespace AZ::Dom::Tests