/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include namespace apache::thrift { namespace { using namespace test::patch; // A callback to add a PatchOp to a Patch template using AddOp = std::function; template using AddOps = std::vector>; template void testMergePatchOps( const AddOps& addOpList, const typename Patch::value_type& value) { Patch mergedPatch, patchWithMultipleOps; auto v1 = value; auto dv1 = protocol::asValueStruct(value); for (const auto& addOp : addOpList) { Patch patch; addOp(patch); // v1 is the result when we apply each patch individually, e.g., // // p1.Method1() // p1.apply(v1) // p2.Method2() // p2.apply(v1) // p3.Method3() // p3.apply(v1) patch.apply(v1); protocol::applyPatch(patch.toObject(), dv1); EXPECT_TRUE(op::equal(v1, protocol::fromValueStruct(dv1))); { // 1. Test whether applying mergedPatch patch is equivalent to applying // each patch individually, e.g., testing // // p1.apply(v1) // p2.apply(v1) // p3.apply(v1) // // is equivalent to // // Patch mergedPatch // mergedPatch.merge(p1) // mergedPatch.merge(p2) // mergedPatch.merge(p3) // mergedPatch.apply(v2) auto v2 = value; mergedPatch.merge(patch); mergedPatch.apply(v2); EXPECT_TRUE(op::equal(v1, v2)); auto dv2 = protocol::asValueStruct(value); protocol::applyPatch(mergedPatch.toObject(), dv2); EXPECT_TRUE(op::equal(v2, protocol::fromValueStruct(dv2))); } { // 2. Test whether patch APIs have merge semantics. e.g., testing // // p1.Method1() // p1.apply(v1) // p2.Method2() // p2.apply(v1) // p3.Method3() // p3.apply(v1) // // is equivalent to // // Patch patchWithMultipleOps // patchWithMultipleOps.Method1() // patchWithMultipleOps.Method2() // patchWithMultipleOps.Method3() // patchWithMultipleOps.apply(v2) auto v2 = value; addOp(patchWithMultipleOps); patchWithMultipleOps.apply(v2); EXPECT_TRUE(op::equal(v1, v2)); auto dv2 = protocol::asValueStruct(value); protocol::applyPatch(patchWithMultipleOps.toObject(), dv2); EXPECT_TRUE(op::equal(v2, protocol::fromValueStruct(dv2))); } } } template void pickMultipleOpsAndTest( const AddOps& candidates, const std::vector& values, const int numberOfOpToPick, // Ideally we want to pick as many as operations // as possible, but the test would be slow if we // pick too many operations. AddOps pickedOps = {}) { if (numberOfOpToPick == 0) { for (const auto& v : values) { testMergePatchOps(pickedOps, v); } return; } for (auto op : candidates) { pickedOps.push_back(op); pickMultipleOpsAndTest( candidates, values, numberOfOpToPick - 1, pickedOps); pickedOps.pop_back(); } } template void pickMultipleOpsAndTest( const AddOps& candidates, const std::vector& values, const int numberOfOpToPick) { pickMultipleOpsAndTest>( candidates, values, numberOfOpToPick); } TEST(PatchMergeTest, BoolPatch) { AddOps ops; ops.push_back([](auto& patch) { patch = true; }); ops.push_back([](auto& patch) { patch = false; }); ops.push_back([](auto& patch) { patch.clear(); }); ops.push_back([](auto& patch) { patch.invert(); }); pickMultipleOpsAndTest(ops, {true, false}, 6); } template void testNumberPatch() { SCOPED_TRACE(folly::pretty_name()); AddOps ops; ops.push_back([](auto& patch) { patch = 0; }); ops.push_back([](auto& patch) { patch = 42; }); ops.push_back([](auto& patch) { patch.clear(); }); ops.push_back([](auto& patch) { patch += 0; }); ops.push_back([](auto& patch) { patch += 10; }); ops.push_back([](auto& patch) { patch -= 0; }); ops.push_back([](auto& patch) { patch -= 20; }); if constexpr (isFloat) { ops.push_back([](auto& patch) { patch = 1.5; }); ops.push_back([](auto& patch) { patch += 2.5; }); ops.push_back([](auto& patch) { patch -= 3.5; }); } pickMultipleOpsAndTest(ops, {0, 10, 20, 42}, 4); } TEST(PatchMergeTest, BytePatch) { testNumberPatch(); } TEST(PatchMergeTest, I16Patch) { testNumberPatch(); } TEST(PatchMergeTest, I32Patch) { testNumberPatch(); } TEST(PatchMergeTest, I64Patch) { testNumberPatch(); } TEST(PatchMergeTest, FloatPatch) { testNumberPatch(); } TEST(PatchMergeTest, DoublePatch) { testNumberPatch(); } template auto stringOrBinary(std::string s) { if constexpr (std::is_same_v) { return s; } else { return *folly::IOBuf::copyBuffer(s); } } template auto genStringPatchOps() { AddOps ops; ops.push_back([](auto& patch) { patch = stringOrBinary(""); }); ops.push_back([](auto& patch) { patch = stringOrBinary("42"); }); ops.push_back([](auto& patch) { patch.clear(); }); ops.push_back([](auto& patch) { patch.prepend(""); }); ops.push_back([](auto& patch) { patch.prepend("("); }); ops.push_back([](auto& patch) { patch.append(""); }); ops.push_back([](auto& patch) { patch.append(")"); }); return ops; } template void testBaseStringPatch() { SCOPED_TRACE(folly::pretty_name()); pickMultipleOpsAndTest( genStringPatchOps(), {stringOrBinary(""), stringOrBinary("1")}, 4); } TEST(PatchMergeTest, StringPatch) { testBaseStringPatch(); } TEST(PatchMergeTest, BinaryPatch) { testBaseStringPatch< type::cpp_type, op::BinaryPatch>(); } TEST(PatchMergeTest, ListPatch) { using ListPatch = std::decay_t()->optListVal())>; AddOps ops; ops.push_back([](auto& patch) { patch = typename ListPatch::value_type{}; }); ops.push_back([](auto& patch) { patch = {0}; }); ops.push_back([](auto& patch) { patch = {10}; }); ops.push_back([](auto& patch) { patch.clear(); }); ops.push_back([](auto& patch) { patch.push_back(0); }); ops.push_back([](auto& patch) { patch.push_back(10); }); pickMultipleOpsAndTest(ops, {{}, {0}}, 4); } TEST(PatchMergeTest, SetPatch) { using SetPatch = std::decay_t()->optSetVal())>; AddOps ops; ops.push_back([](auto& patch) { patch = typename SetPatch::value_type{}; }); ops.push_back([](auto& patch) { patch = {"10"}; }); ops.push_back([](auto& patch) { patch = {"20"}; }); ops.push_back([](auto& patch) { patch = {"30"}; }); ops.push_back([](auto& patch) { patch.clear(); }); ops.push_back([](auto& patch) { patch.insert("10"); }); ops.push_back([](auto& patch) { patch.insert("20"); }); ops.push_back([](auto& patch) { patch.erase("10"); }); ops.push_back([](auto& patch) { patch.erase("30"); }); pickMultipleOpsAndTest>( ops, {{}, {"10"}, {"10", "20"}, {"10", "20", "30"}}, 3); } TEST(PatchMergeTest, MapPatch) { using MapPatch = std::decay_t()->optMapVal())>; AddOps ops; ops.push_back([](auto& patch) { patch = typename MapPatch::value_type{}; }); ops.push_back([](auto& patch) { patch = {{"10", "1"}}; }); ops.push_back([](auto& patch) { patch = {{"20", "2"}}; }); ops.push_back([](auto& patch) { patch = {{"30", "3"}}; }); ops.push_back([](auto& patch) { patch.clear(); }); ops.push_back([](auto& patch) { patch.insert_or_assign("10", "100"); }); ops.push_back([](auto& patch) { patch.insert_or_assign("20", "100"); }); ops.push_back([](auto& patch) { patch.insert_or_assign("30", "300"); }); ops.push_back([](auto& patch) { patch.add({{"10", "1000"}}); }); ops.push_back([](auto& patch) { patch.add({{"20", "2000"}}); }); ops.push_back([](auto& patch) { patch.add({{"30", "3000"}}); }); ops.push_back([](auto& patch) { patch.erase("10"); }); ops.push_back([](auto& patch) { patch.erase("30"); }); for (auto f : genStringPatchOps()) { ops.push_back([f](auto& patch) { f(patch.patchByKey("10")); }); ops.push_back([f](auto& patch) { f(patch.ensureAndPatchByKey("10")); }); } std::vector values; values.emplace_back(); values.emplace_back(values.back())["10"] = "1"; values.emplace_back(values.back())["20"] = "2"; values.emplace_back(values.back())["30"] = "3"; pickMultipleOpsAndTest>( ops, values, 2); } TEST(PatchMergeTest, StructPatch) { AddOps ops; MyStruct foo; ops.push_back([=](MyStructPatch& patch) { patch = foo; }); foo.optStringVal() = "10"; ops.push_back([=](MyStructPatch& patch) { patch = foo; }); foo.optStringVal().reset(); foo.stringVal() = "10"; ops.push_back([=](MyStructPatch& patch) { patch = foo; }); ops.push_back([](MyStructPatch& patch) { patch.clear(); }); ops.push_back( [](MyStructPatch& patch) { (patch.ensure()); }); ops.push_back( [](MyStructPatch& patch) { (patch.ensure()); }); ops.push_back( [](MyStructPatch& patch) { (patch.remove()); }); ops.push_back( [](MyStructPatch& patch) { (patch.ensure("10")); }); for (auto f : genStringPatchOps()) { ops.push_back( [f](MyStructPatch& patch) { f(patch.patchIfSet()); }); ops.push_back([f](MyStructPatch& patch) { f(patch.patchIfSet()); }); ops.push_back( [f](MyStructPatch& patch) { f(patch.patch()); }); ops.push_back( [f](MyStructPatch& patch) { f(patch.patch()); }); } std::vector values; values.emplace_back(); values.emplace_back().optStringVal() = ""; values.emplace_back().optStringVal() = "10"; values.emplace_back().stringVal() = ""; values.emplace_back().stringVal() = "10"; pickMultipleOpsAndTest(ops, values, 2); } } // namespace } // namespace apache::thrift