| /* |
| * Copyright (C) 2025 The Android Open Source Project |
| * |
| * 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 <cstdint> |
| |
| #include "perfetto/protozero/field.h" |
| #include "src/protovm/test/utils.h" |
| #include "test/gtest_and_gmock.h" |
| |
| #include "src/protovm/error_handling.h" |
| #include "src/protovm/rw_proto.h" |
| #include "src/protovm/test/protos/incremental_trace.pb.h" |
| #include "src/protovm/test/sample_packets.h" |
| #include "src/protovm/test/utils.h" |
| |
| namespace perfetto { |
| namespace protovm { |
| namespace test { |
| |
| class RwProtoTest : public ::testing::Test { |
| protected: |
| void SetUp() override { PopulateRwProtoWithTwoElements(); } |
| |
| void PopulateRwProtoWithTwoElements() { |
| auto root = data_trace_entry_with_two_elements_.GetRoot(); |
| |
| // elements[0] |
| { |
| auto element = root; |
| element.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| |
| // id field |
| auto id = element; |
| id.EnterField(protos::Element::kIdFieldNumber); |
| id.SetScalar(Scalar::VarInt(0)); |
| |
| // value field |
| auto value = element; |
| value.EnterField(protos::Element::kValueFieldNumber); |
| value.SetScalar(Scalar::VarInt(10)); |
| |
| // value_fixed32 field |
| auto value_fixed32 = element; |
| value_fixed32.EnterField(protos::Element::kValueFixed32FieldNumber); |
| value_fixed32.SetScalar(Scalar::Fixed32(32)); |
| |
| // value_fixed64 field |
| auto value_fixed64 = element; |
| value_fixed64.EnterField(protos::Element::kValueFixed64FieldNumber); |
| value_fixed64.SetScalar(Scalar::Fixed64(64)); |
| } |
| |
| // elements[1] |
| { |
| auto element = root; |
| element.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 1); |
| |
| // id field |
| auto id = element; |
| id.EnterField(protos::Element::kIdFieldNumber); |
| id.SetScalar(Scalar::VarInt(1)); |
| |
| // value field |
| auto value = element; |
| value.EnterField(protos::Element::kValueFieldNumber); |
| value.SetScalar(Scalar::VarInt(11)); |
| } |
| } |
| |
| void CheckProtoWithTwoElements(const std::string& proto) const { |
| protos::TraceEntry entry; |
| entry.ParseFromString(proto); |
| |
| ASSERT_EQ(entry.elements_size(), 2); |
| |
| ASSERT_EQ(entry.elements(0).id(), 0); |
| ASSERT_EQ(entry.elements(0).value(), 10); |
| ASSERT_EQ(entry.elements(0).value_fixed32(), static_cast<uint32_t>(32)); |
| ASSERT_EQ(entry.elements(0).value_fixed64(), static_cast<uint64_t>(64)); |
| |
| ASSERT_EQ(entry.elements(1).id(), 1); |
| ASSERT_EQ(entry.elements(1).value(), 11); |
| } |
| |
| std::string SerializeAsString(const RwProto& rw_proto) const { |
| protozero::HeapBuffered<protozero::Message> proto; |
| rw_proto.Serialize(proto.get()); |
| return proto.SerializeAsString(); |
| } |
| |
| Allocator allocator_{10 * 1024 * 1024}; |
| RwProto data_empty_{&allocator_}; |
| RwProto data_trace_entry_with_two_elements_{&allocator_}; |
| protozero::ConstBytes bytes_empty_{nullptr, 0}; |
| }; |
| |
| TEST_F(RwProtoTest, HasField_IncompatibleWireType) { |
| RwProto::Cursor cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| cursor.EnterField(protos::Element::kIdFieldNumber); |
| ASSERT_TRUE(cursor.HasField(0).IsAbort()); |
| } |
| |
| TEST_F(RwProtoTest, HasField_FieldNotAvailable) { |
| { |
| RwProto::Cursor cursor = data_empty_.GetRoot(); |
| ASSERT_EQ(*cursor.HasField(protos::TraceEntry::kElementsFieldNumber), |
| false); |
| } |
| { |
| RwProto::Cursor cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| ASSERT_EQ(*cursor.HasField(0), false); |
| } |
| { |
| RwProto::Cursor cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| cursor.EnterField(protos::TraceEntry::kElementsFieldNumber); |
| ASSERT_EQ(*cursor.HasField(0), false); |
| } |
| } |
| |
| TEST_F(RwProtoTest, HasField_FieldAvailable) { |
| { |
| RwProto::Cursor cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| ASSERT_EQ(*cursor.HasField(protos::TraceEntry::kElementsFieldNumber), true); |
| } |
| { |
| RwProto::Cursor cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| ASSERT_EQ(*cursor.HasField(protos::Element::kIdFieldNumber), true); |
| ASSERT_EQ(*cursor.HasField(protos::Element::kValueFieldNumber), true); |
| } |
| } |
| |
| TEST_F(RwProtoTest, HasField_FieldNotAvailableAsBytes) { |
| RwProto::Cursor cursor = data_empty_.GetRoot(); |
| auto proto = SamplePackets::TraceEntryWithTwoElements().SerializeAsString(); |
| cursor.SetBytes(AsConstBytes(proto)); |
| ASSERT_EQ(*cursor.HasField(0), false); |
| } |
| |
| TEST_F(RwProtoTest, HasField_FieldAvailableAsBytes) { |
| RwProto::Cursor cursor = data_empty_.GetRoot(); |
| auto proto = SamplePackets::TraceEntryWithTwoElements().SerializeAsString(); |
| cursor.SetBytes(AsConstBytes(proto)); |
| ASSERT_EQ(*cursor.HasField(protos::TraceEntry::kElementsFieldNumber), true); |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| ASSERT_EQ(*cursor.HasField(protos::Element::kIdFieldNumber), true); |
| ASSERT_EQ(*cursor.HasField(protos::Element::kValueFieldNumber), true); |
| } |
| |
| TEST_F(RwProtoTest, EnterField_IncompatibleWireType) { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| ASSERT_TRUE( |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0) |
| .IsOk()); |
| ASSERT_TRUE(cursor.EnterField(protos::Element::kIdFieldNumber).IsOk()); |
| ASSERT_TRUE(cursor.EnterField(0).IsAbort()); |
| } |
| |
| TEST_F(RwProtoTest, EnterField_IncompatibleFieldType_IndexedRepeatedField) { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| ASSERT_TRUE( |
| cursor.EnterField(protos::TraceEntry::kElementsFieldNumber).IsAbort()); |
| } |
| |
| TEST_F(RwProtoTest, EnterField_IncompatibleFieldType_MappedRepeatedField) { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| |
| { |
| // Trigger internal node conversion from "indexed repeated field" to "mapped |
| // repeated field" |
| auto element = cursor; |
| ASSERT_TRUE( |
| element |
| .EnterRepeatedFieldByKey(protos::TraceEntry::kElementsFieldNumber, |
| protos::Element::kIdFieldNumber, 0) |
| .IsOk()); |
| } |
| |
| ASSERT_TRUE( |
| cursor.EnterField(protos::TraceEntry::kElementsFieldNumber).IsAbort()); |
| } |
| |
| TEST_F(RwProtoTest, EnterField_FieldNotAvailableGetsCreated) { |
| RwProto::Cursor cursor = data_empty_.GetRoot(); |
| ASSERT_TRUE( |
| cursor.EnterField(protos::TraceEntry::kElementsFieldNumber).IsOk()); |
| ASSERT_TRUE(cursor.EnterField(protos::Element::kIdFieldNumber).IsOk()); |
| cursor.SetScalar(Scalar::VarInt(10)); |
| |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_empty_)); |
| ASSERT_EQ(entry.elements_size(), 1); |
| ASSERT_EQ(entry.elements(0).id(), 10); |
| } |
| |
| TEST_F(RwProtoTest, EnterField_FieldAvailable) { |
| RwProto::Cursor cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| ASSERT_TRUE( |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0) |
| .IsOk()); |
| ASSERT_TRUE(cursor.EnterField(protos::Element::kIdFieldNumber).IsOk()); |
| |
| CheckProtoWithTwoElements( |
| SerializeAsString(data_trace_entry_with_two_elements_)); |
| } |
| |
| TEST_F(RwProtoTest, EnterField_FieldNotAvailableAsBytesGetsCreated) { |
| RwProto::Cursor cursor = data_empty_.GetRoot(); |
| |
| auto entry = SamplePackets::TraceEntryWithTwoElements(); |
| entry.mutable_elements(0)->clear_value(); |
| auto proto = entry.SerializeAsString(); |
| cursor.SetBytes(AsConstBytes(proto)); |
| |
| // elements[0].value = 10 |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| cursor.EnterField(protos::Element::kValueFieldNumber); |
| cursor.SetScalar(Scalar::VarInt(10)); |
| |
| CheckProtoWithTwoElements(SerializeAsString(data_empty_)); |
| } |
| |
| TEST_F(RwProtoTest, EnterField_FieldAvailableAsBytes) { |
| auto cursor = data_empty_.GetRoot(); |
| auto proto = SamplePackets::TraceEntryWithTwoElements().SerializeAsString(); |
| cursor.SetBytes(AsConstBytes(proto)); |
| |
| ASSERT_TRUE( |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0) |
| .IsOk()); |
| ASSERT_TRUE(cursor.EnterField(protos::Element::kIdFieldNumber).IsOk()); |
| ASSERT_EQ(cursor.GetScalar().value(), Scalar::VarInt(0)); |
| } |
| |
| TEST_F(RwProtoTest, EnterIndexedRepeatedField_IncompatibleWireType) { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| cursor.EnterField(protos::Element::kIdFieldNumber); |
| ASSERT_TRUE(cursor.EnterRepeatedFieldAt(0, 0).IsAbort()); |
| } |
| |
| // TODO: support accessing repeated fields by both index and key. |
| // Currently, a repeated field can be organized internally as either "indexed" |
| // or "mapped", but not both. This means that once a field is accessed using |
| // a key (EnterRepeatedFieldByKey), it can no longer be accessed by |
| // index (EnterRepeatedFieldAt). While it's technically possible to allow |
| // both access methods (same internal node inserted into two intrusive maps), |
| // it's a low priority as current use cases don't require it. |
| TEST_F(RwProtoTest, |
| EnterIndexedRepeatedField_IncompatibleFieldType_MappedRepeatedField) { |
| // Trigger creation of internal "mapped repeated field" node |
| { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| ASSERT_TRUE( |
| cursor |
| .EnterRepeatedFieldByKey(protos::TraceEntry::kElementsFieldNumber, |
| protos::Element::kIdFieldNumber, 0) |
| .IsOk()); |
| } |
| |
| // Attempt to access as "indexed repeated field" |
| { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| ASSERT_TRUE( |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0) |
| .IsAbort()); |
| } |
| } |
| |
| // Only append operations (insert at index == elements.size()) are supported. |
| // Attempting to insert a field with index > elements.size() causes an |
| // abort. |
| TEST_F(RwProtoTest, |
| EnterIndexedRepeatedField_FieldNotAvailable_AbortIfNotSimpleAppend) { |
| // Attempt to enter field_index (1) > elements.size() (0) => abort |
| { |
| auto cursor = data_empty_.GetRoot(); |
| ASSERT_TRUE( |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 1) |
| .IsAbort()); |
| } |
| // Attempt to enter field_index (3) > elements.size() (2) => abort |
| { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| ASSERT_TRUE( |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 3) |
| .IsAbort()); |
| } |
| } |
| |
| TEST_F(RwProtoTest, EnterIndexedRepeatedField_FieldNotAvailable_Append) { |
| auto cursor = data_empty_.GetRoot(); |
| |
| // append elements[0] |
| { |
| auto element = cursor; |
| ASSERT_TRUE( |
| element |
| .EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0) |
| .IsOk()); |
| auto proto = SamplePackets::TraceEntryWithTwoElements() |
| .elements(0) |
| .SerializeAsString(); |
| element.SetBytes(AsConstBytes(proto)); |
| } |
| |
| // append elements[1] |
| { |
| auto element = cursor; |
| ASSERT_TRUE( |
| element |
| .EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 1) |
| .IsOk()); |
| auto proto = SamplePackets::TraceEntryWithTwoElements() |
| .elements(1) |
| .SerializeAsString(); |
| element.SetBytes(AsConstBytes(proto)); |
| } |
| |
| CheckProtoWithTwoElements(SerializeAsString(data_empty_)); |
| } |
| |
| TEST_F(RwProtoTest, EnterIndexedRepeatedField_FieldAvailable) { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| |
| // elements[0].id = 100 |
| { |
| auto id = cursor; |
| ASSERT_TRUE( |
| id.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0) |
| .IsOk()); |
| id.EnterField(protos::Element::kIdFieldNumber); |
| id.SetScalar(Scalar::VarInt(100)); |
| } |
| |
| // elements[1].id = 101 |
| { |
| auto id = cursor; |
| ASSERT_TRUE( |
| id.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 1) |
| .IsOk()); |
| id.EnterField(protos::Element::kIdFieldNumber); |
| id.SetScalar(Scalar::VarInt(101)); |
| } |
| |
| auto proto = SerializeAsString(data_trace_entry_with_two_elements_); |
| protos::TraceEntry entry; |
| entry.ParseFromString(proto); |
| ASSERT_EQ(entry.elements_size(), 2); |
| ASSERT_EQ(entry.elements(0).id(), 100); |
| ASSERT_EQ(entry.elements(0).value(), 10); |
| ASSERT_EQ(entry.elements(1).id(), 101); |
| ASSERT_EQ(entry.elements(1).value(), 11); |
| } |
| |
| TEST_F(RwProtoTest, |
| EnterIndexedRepeatedField_FieldAvailable_NotDetectedAsRepeatedYet) { |
| auto cursor = data_empty_.GetRoot(); |
| |
| // set only elements[0] -> initially considered a simple field (not repeated) |
| auto proto = SamplePackets::TraceEntryWithOneElement().SerializeAsString(); |
| cursor.SetBytes(AsConstBytes(proto)); |
| |
| // append elements[1] -> detect elements is an indexed repeated field and |
| // reorganize the internal nodes accordingly |
| ASSERT_TRUE( |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 1) |
| .IsOk()); |
| auto proto_element1 = SamplePackets::TraceEntryWithTwoElements() |
| .elements(1) |
| .SerializeAsString(); |
| cursor.SetBytes(AsConstBytes(proto_element1)); |
| |
| CheckProtoWithTwoElements(SerializeAsString(data_empty_)); |
| } |
| |
| TEST_F(RwProtoTest, IterateRepeatedField_IncompatibleWireType) { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| cursor.EnterField(protos::Element::kIdFieldNumber); |
| ASSERT_TRUE(cursor.IterateRepeatedField(0).IsAbort()); |
| } |
| |
| TEST_F(RwProtoTest, IterateRepeatedField_FieldsNotAvailable) { |
| auto cursor = data_empty_.GetRoot(); |
| auto status_or_it = |
| cursor.IterateRepeatedField(protos::TraceEntry::kElementsFieldNumber); |
| ASSERT_FALSE(static_cast<bool>(*status_or_it)); |
| } |
| |
| TEST_F(RwProtoTest, IterateRepeatedField_FieldsAvailable) { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| auto it = |
| *cursor.IterateRepeatedField(protos::TraceEntry::kElementsFieldNumber); |
| |
| // elements[0] |
| { |
| ASSERT_TRUE(static_cast<bool>(it)); |
| auto value = it.GetCursor(); |
| value.EnterField(protos::Element::kValueFieldNumber); |
| value.SetScalar(Scalar::VarInt(100)); |
| } |
| |
| // elements[1] |
| { |
| ++it; |
| ASSERT_TRUE(static_cast<bool>(it)); |
| auto value = it.GetCursor(); |
| value.EnterField(protos::Element::kValueFieldNumber); |
| value.SetScalar(Scalar::VarInt(101)); |
| } |
| |
| // elements[one_past_end] |
| { |
| ++it; |
| ASSERT_FALSE(static_cast<bool>(it)); |
| } |
| |
| auto proto = SerializeAsString(data_trace_entry_with_two_elements_); |
| protos::TraceEntry entry; |
| entry.ParseFromString(proto); |
| |
| ASSERT_EQ(entry.elements_size(), 2); |
| ASSERT_EQ(entry.elements(0).id(), 0); |
| ASSERT_EQ(entry.elements(0).value(), 100); |
| ASSERT_EQ(entry.elements(1).id(), 1); |
| ASSERT_EQ(entry.elements(1).value(), 101); |
| } |
| |
| TEST_F(RwProtoTest, |
| IterateRepeatedField_FieldsAvailable_NotDetectedAsRepeatedYet) { |
| auto cursor = data_empty_.GetRoot(); |
| |
| // set only elements[0] -> initially considered a simple field (not repeated) |
| auto proto = SamplePackets::TraceEntryWithOneElement().SerializeAsString(); |
| cursor.SetBytes(AsConstBytes(proto)); |
| |
| // request iteration of elements -> detect elements is an indexed repeated |
| // field and reorganize the internal nodes accordingly |
| auto it = |
| *cursor.IterateRepeatedField(protos::TraceEntry::kElementsFieldNumber); |
| |
| // element[0] |
| { |
| ASSERT_TRUE(static_cast<bool>(it)); |
| auto value = it.GetCursor(); |
| value.EnterField(protos::Element::kValueFieldNumber); |
| value.SetScalar(Scalar::VarInt(100)); |
| } |
| |
| // element[end_past_one] |
| { |
| ++it; |
| ASSERT_FALSE(static_cast<bool>(it)); |
| } |
| |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_empty_)); |
| |
| ASSERT_EQ(entry.elements_size(), 1); |
| ASSERT_EQ(entry.elements(0).id(), 0); |
| ASSERT_EQ(entry.elements(0).value(), 100); |
| } |
| |
| TEST_F(RwProtoTest, EnterMappedRepeatedField_IncompatibleWireType) { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| cursor.EnterField(protos::Element::kIdFieldNumber); |
| ASSERT_TRUE(cursor.EnterRepeatedFieldByKey(0, 0, 0).IsAbort()); |
| } |
| |
| TEST_F(RwProtoTest, EnterMappedRepeatedField_FieldNotAvailable) { |
| auto cursor = data_empty_.GetRoot(); |
| ASSERT_TRUE( |
| cursor |
| .EnterRepeatedFieldByKey(protos::TraceEntry::kElementsFieldNumber, |
| protos::Element::kIdFieldNumber, 10) |
| .IsOk()); |
| |
| // elements[10].value = 100 |
| cursor.EnterField(protos::Element::kValueFieldNumber); |
| cursor.SetScalar(Scalar::VarInt(100)); |
| |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_empty_)); |
| |
| ASSERT_EQ(entry.elements_size(), 1); |
| ASSERT_EQ(entry.elements(0).value(), 100); |
| } |
| |
| TEST_F(RwProtoTest, EnterMappedRepeatedField_FieldAvailable) { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| ASSERT_TRUE( |
| cursor |
| .EnterRepeatedFieldByKey(protos::TraceEntry::kElementsFieldNumber, |
| protos::Element::kIdFieldNumber, 0) |
| .IsOk()); |
| |
| // elements[0].value = 100 |
| cursor.EnterField(protos::Element::kValueFieldNumber); |
| cursor.SetScalar(Scalar::VarInt(100)); |
| |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_trace_entry_with_two_elements_)); |
| |
| ASSERT_EQ(entry.elements_size(), 2); |
| ASSERT_EQ(entry.elements(0).id(), 0); |
| ASSERT_EQ(entry.elements(0).value(), 100); |
| ASSERT_EQ(entry.elements(1).id(), 1); |
| ASSERT_EQ(entry.elements(1).value(), 11); |
| } |
| |
| TEST_F(RwProtoTest, EnterMappedRepeatedField_FieldAvailableAsBytes) { |
| auto cursor = data_empty_.GetRoot(); |
| |
| auto proto = SamplePackets::TraceEntryWithTwoElements().SerializeAsString(); |
| cursor.SetBytes(AsConstBytes(proto)); |
| |
| ASSERT_TRUE( |
| cursor |
| .EnterRepeatedFieldByKey(protos::TraceEntry::kElementsFieldNumber, |
| protos::Element::kIdFieldNumber, 0) |
| .IsOk()); |
| |
| // elements[0].value = 100 |
| cursor.EnterField(protos::Element::kValueFieldNumber); |
| cursor.SetScalar(Scalar::VarInt(100)); |
| |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_empty_)); |
| |
| ASSERT_EQ(entry.elements_size(), 2); |
| ASSERT_EQ(entry.elements(0).id(), 0); |
| ASSERT_EQ(entry.elements(0).value(), 100); |
| ASSERT_EQ(entry.elements(1).id(), 1); |
| ASSERT_EQ(entry.elements(1).value(), 11); |
| } |
| |
| TEST_F(RwProtoTest, |
| EnterMappedRepeatedField_FieldAvailable_NotDetectedAsRepeatedYet) { |
| auto cursor = data_empty_.GetRoot(); |
| |
| auto proto = SamplePackets::TraceEntryWithOneElement().SerializeAsString(); |
| cursor.SetBytes(AsConstBytes(proto)); |
| |
| // elements[0].value = 100 |
| { |
| auto value = cursor; |
| ASSERT_TRUE( |
| value |
| .EnterRepeatedFieldByKey(protos::TraceEntry::kElementsFieldNumber, |
| protos::Element::kIdFieldNumber, 0) |
| .IsOk()); |
| |
| value.EnterField(protos::Element::kValueFieldNumber); |
| value.SetScalar(Scalar::VarInt(100)); |
| } |
| |
| // elements[1].value = 101 |
| { |
| auto value = cursor; |
| ASSERT_TRUE( |
| value |
| .EnterRepeatedFieldByKey(protos::TraceEntry::kElementsFieldNumber, |
| protos::Element::kIdFieldNumber, 1) |
| .IsOk()); |
| |
| value.EnterField(protos::Element::kValueFieldNumber); |
| value.SetScalar(Scalar::VarInt(101)); |
| } |
| |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_empty_)); |
| |
| ASSERT_EQ(entry.elements_size(), 2); |
| ASSERT_EQ(entry.elements(0).value(), 100); |
| ASSERT_EQ(entry.elements(1).value(), 101); |
| } |
| |
| TEST_F(RwProtoTest, GetScalar_IncompatibleWireType) { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| ASSERT_TRUE(cursor.GetScalar().IsAbort()); |
| |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| ASSERT_TRUE(cursor.GetScalar().IsAbort()); |
| |
| cursor.EnterField(1000); |
| ASSERT_TRUE(cursor.GetScalar().IsAbort()); |
| } |
| |
| TEST_F(RwProtoTest, GetScalar_Success) { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| cursor.EnterField(protos::Element::kValueFieldNumber); |
| ASSERT_EQ(cursor.GetScalar().value(), Scalar::VarInt(10)); |
| } |
| |
| TEST_F(RwProtoTest, Delete_RootMessage) { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| ASSERT_TRUE(cursor.Delete().IsOk()); |
| |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_trace_entry_with_two_elements_)); |
| |
| ASSERT_EQ(entry.elements_size(), 0); |
| } |
| |
| TEST_F(RwProtoTest, Delete_Scalar) { |
| // delete elements[0].id |
| { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| cursor.EnterField(protos::Element::kIdFieldNumber); |
| ASSERT_TRUE(cursor.Delete().IsOk()); |
| } |
| // delete elements[1].value |
| { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 1); |
| cursor.EnterField(protos::Element::kValueFieldNumber); |
| ASSERT_TRUE(cursor.Delete().IsOk()); |
| } |
| |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_trace_entry_with_two_elements_)); |
| |
| ASSERT_EQ(entry.elements_size(), 2); |
| ASSERT_FALSE(entry.elements(0).has_id()); |
| ASSERT_EQ(entry.elements(0).value(), 10); |
| ASSERT_EQ(entry.elements(1).id(), 1); |
| ASSERT_FALSE(entry.elements(1).has_value()); |
| } |
| |
| TEST_F(RwProtoTest, Delete_Message) { |
| auto cursor = data_empty_.GetRoot(); |
| auto proto = SamplePackets::TraceEntryWithOneElement().SerializeAsString(); |
| cursor.SetBytes(AsConstBytes(proto)); |
| |
| // field detect elements[0] as simple (not repeated) field |
| cursor.EnterField(protos::TraceEntry::kElementsFieldNumber); |
| |
| cursor.Delete(); |
| |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_empty_)); |
| } |
| |
| // TODO: deleting an element from an indexed repeated field currently creates a |
| // "hole" in the array, but ideally we should shift subsequent elements to |
| // the left. This is low-priority though as the "array delete" operation is not |
| // required by the current use cases. |
| TEST_F(RwProtoTest, Delete_IndexedRepeatedField) { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| ASSERT_TRUE(cursor.Delete().IsOk()); |
| |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_trace_entry_with_two_elements_)); |
| |
| ASSERT_EQ(entry.elements_size(), 1); |
| ASSERT_EQ(entry.elements(0).id(), 1); |
| ASSERT_EQ(entry.elements(0).value(), 11); |
| } |
| |
| TEST_F(RwProtoTest, Delete_MappedRepeatedField) { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| cursor.EnterRepeatedFieldByKey(protos::TraceEntry::kElementsFieldNumber, |
| protos::Element::kIdFieldNumber, 0); |
| ASSERT_TRUE(cursor.Delete().IsOk()); |
| |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_trace_entry_with_two_elements_)); |
| ASSERT_EQ(entry.elements_size(), 1); |
| ASSERT_EQ(entry.elements(0).id(), 1); |
| ASSERT_EQ(entry.elements(0).value(), 11); |
| } |
| |
| TEST_F(RwProtoTest, Merge_IncompatibleWireType) { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| cursor.EnterField(protos::Element::kIdFieldNumber); |
| ASSERT_TRUE(cursor.Merge(bytes_empty_, RwProto::Cursor::kNone).IsAbort()); |
| } |
| |
| TEST_F(RwProtoTest, Merge_EmptySrc) { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| ASSERT_TRUE(cursor.Merge(bytes_empty_, RwProto::Cursor::kNone).IsOk()); |
| CheckProtoWithTwoElements( |
| SerializeAsString(data_trace_entry_with_two_elements_)); |
| } |
| |
| TEST_F(RwProtoTest, Merge_EmptyDst) { |
| auto cursor = data_empty_.GetRoot(); |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| |
| protos::Element element; |
| element.set_id(1); |
| element.set_value(11); |
| auto proto = element.SerializeAsString(); |
| ASSERT_TRUE(cursor.Merge(AsConstBytes(proto), RwProto::Cursor::kNone).IsOk()); |
| |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_empty_)); |
| ASSERT_EQ(entry.elements_size(), 1); |
| ASSERT_EQ(entry.elements(0).id(), 1); |
| ASSERT_EQ(entry.elements(0).value(), 11); |
| } |
| |
| TEST_F(RwProtoTest, Merge_FieldsUnion) { |
| auto cursor = data_empty_.GetRoot(); |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| |
| // initialize element = {id: 1} |
| { |
| protos::Element element; |
| element.set_id(1); |
| auto proto = element.SerializeAsString(); |
| ASSERT_TRUE( |
| cursor.Merge(AsConstBytes(proto), RwProto::Cursor::kNone).IsOk()); |
| } |
| |
| // merge with element = {value: 11} |
| { |
| protos::Element element; |
| element.set_value(11); |
| auto proto = element.SerializeAsString(); |
| ASSERT_TRUE( |
| cursor.Merge(AsConstBytes(proto), RwProto::Cursor::kNone).IsOk()); |
| } |
| |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_empty_)); |
| ASSERT_EQ(entry.elements_size(), 1); |
| ASSERT_EQ(entry.elements(0).id(), 1); |
| ASSERT_EQ(entry.elements(0).value(), 11); |
| } |
| |
| TEST_F(RwProtoTest, Merge_FieldsReplacement) { |
| auto cursor = data_empty_.GetRoot(); |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| |
| // initialize element = {id: 0, value: 10} |
| { |
| protos::Element element; |
| element.set_id(0); |
| element.set_value(10); |
| auto bytes = element.SerializeAsString(); |
| cursor.SetBytes(AsConstBytes(bytes)); |
| } |
| |
| // merge with element = {id: 1, value: 11} |
| { |
| protos::Element element; |
| element.set_id(1); |
| element.set_value(11); |
| auto bytes = element.SerializeAsString(); |
| ASSERT_TRUE( |
| cursor.Merge(AsConstBytes(bytes), RwProto::Cursor::kNone).IsOk()); |
| } |
| |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_empty_)); |
| ASSERT_EQ(entry.elements_size(), 1); |
| ASSERT_EQ(entry.elements(0).id(), 1); |
| ASSERT_EQ(entry.elements(0).value(), 11); |
| } |
| |
| TEST_F(RwProtoTest, Merge_RepeatedField) { |
| // initially empty |
| auto cursor = data_empty_.GetRoot(); |
| |
| // merge with elements = [{id: 1, value: 10}, {id: 2, value: 20}] |
| { |
| protos::TraceEntry entry; |
| |
| auto* element0 = entry.add_elements(); |
| element0->set_id(1); |
| element0->set_value(10); |
| |
| auto* element1 = entry.add_elements(); |
| element1->set_id(2); |
| element1->set_value(20); |
| |
| auto bytes = entry.SerializeAsString(); |
| ASSERT_TRUE( |
| cursor.Merge(AsConstBytes(bytes), RwProto::Cursor::kNone).IsOk()); |
| } |
| |
| // check |
| { |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_empty_)); |
| ASSERT_EQ(entry.elements_size(), 2); |
| ASSERT_EQ(entry.elements(0).id(), 1); |
| ASSERT_EQ(entry.elements(0).value(), 10); |
| ASSERT_EQ(entry.elements(1).id(), 2); |
| ASSERT_EQ(entry.elements(1).value(), 20); |
| } |
| |
| // merge with elements = [{id: 0, value: 1}] |
| // (fully replace original elements) |
| { |
| protos::TraceEntry entry; |
| |
| auto* element0 = entry.add_elements(); |
| element0->set_id(0); |
| element0->set_value(1); |
| |
| auto bytes = entry.SerializeAsString(); |
| ASSERT_TRUE( |
| cursor.Merge(AsConstBytes(bytes), RwProto::Cursor::kNone).IsOk()); |
| } |
| |
| // check |
| { |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_empty_)); |
| ASSERT_EQ(entry.elements_size(), 1); |
| ASSERT_EQ(entry.elements(0).id(), 0); |
| ASSERT_EQ(entry.elements(0).value(), 1); |
| } |
| } |
| |
| TEST_F(RwProtoTest, Merge_SkipSubmessages) { |
| auto cursor = data_empty_.GetRoot(); |
| |
| // initialize state: single_element={id:1, value:10}, id=100 |
| { |
| auto single_cursor = cursor; |
| ASSERT_TRUE( |
| single_cursor.EnterField(protos::TraceEntry::kSingleElementFieldNumber) |
| .IsOk()); |
| |
| auto id_cursor = single_cursor; |
| ASSERT_TRUE(id_cursor.EnterField(protos::Element::kIdFieldNumber).IsOk()); |
| ASSERT_TRUE(id_cursor.SetScalar(Scalar::VarInt(1)).IsOk()); |
| |
| auto value_cursor = single_cursor; |
| ASSERT_TRUE( |
| value_cursor.EnterField(protos::Element::kValueFieldNumber).IsOk()); |
| ASSERT_TRUE(value_cursor.SetScalar(Scalar::VarInt(10)).IsOk()); |
| |
| auto root_id_cursor = cursor; |
| ASSERT_TRUE( |
| root_id_cursor.EnterField(protos::TraceEntry::kIdFieldNumber).IsOk()); |
| ASSERT_TRUE(root_id_cursor.SetScalar(Scalar::VarInt(100)).IsOk()); |
| } |
| |
| // prepare patch for root: single_element={id:2, value:20}, id=200 |
| protos::TraceEntry root_patch; |
| { |
| auto* single = root_patch.mutable_single_element(); |
| single->set_id(2); |
| single->set_value(20); |
| root_patch.set_id(200); |
| } |
| |
| // merge root with skip_submessages = true |
| ASSERT_TRUE(cursor |
| .Merge(AsConstBytes(root_patch.SerializeAsString()), |
| RwProto::Cursor::kSkipSubmessages) |
| .IsOk()); |
| |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_empty_)); |
| ASSERT_TRUE(entry.has_single_element()); |
| ASSERT_EQ(entry.single_element().id(), 1); // not updated |
| ASSERT_EQ(entry.single_element().value(), 10); // not updated |
| ASSERT_TRUE(entry.has_id()); |
| ASSERT_EQ(entry.id(), 200); // updated |
| } |
| |
| TEST_F(RwProtoTest, Merge_DelIfSrcEmpty_SingleField) { |
| auto root = data_empty_.GetRoot(); |
| |
| // root = { single_element { id: 10 }} |
| { |
| auto cursor = root; |
| cursor.EnterField(protos::TraceEntry::kSingleElementFieldNumber); |
| cursor.EnterField(protos::Element::kIdFieldNumber); |
| cursor.SetScalar(Scalar::VarInt(10)); |
| } |
| |
| // patch = { single_element {} } |
| protos::TraceEntry patch; |
| patch.mutable_single_element(); |
| |
| // Merge |
| ASSERT_TRUE(root.Merge(AsConstBytes(patch.SerializeAsString()), |
| RwProto::Cursor::kDelIfSrcEmpty) |
| .IsOk()); |
| |
| // Check root = {} |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_empty_)); |
| ASSERT_FALSE(entry.has_single_element()); |
| } |
| |
| TEST_F(RwProtoTest, Merge_DelIfSrcEmpty_RepeatedField) { |
| // root = { element {...} element {...} } |
| auto root = data_trace_entry_with_two_elements_.GetRoot(); |
| |
| // patch = { element {} } |
| protos::TraceEntry patch; |
| patch.add_elements(); |
| |
| root.Merge(AsConstBytes(patch.SerializeAsString()), |
| RwProto::Cursor::kDelIfSrcEmpty); |
| |
| // Check root = {} |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_trace_entry_with_two_elements_)); |
| ASSERT_EQ(entry.elements_size(), 0); |
| } |
| |
| TEST_F(RwProtoTest, Merge_DelIfSrcEmptyAndSkipSubmessages) { |
| auto root = data_empty_.GetRoot(); |
| |
| // root = { single_element { id: 10 } } |
| { |
| auto cursor = root; |
| ASSERT_TRUE(cursor.EnterField(protos::TraceEntry::kSingleElementFieldNumber) |
| .IsOk()); |
| auto id_cursor = cursor; |
| ASSERT_TRUE(id_cursor.EnterField(protos::Element::kIdFieldNumber).IsOk()); |
| ASSERT_TRUE(id_cursor.SetScalar(Scalar::VarInt(10)).IsOk()); |
| } |
| |
| // patch = { single_element {} } |
| protos::TraceEntry patch; |
| patch.mutable_single_element(); |
| |
| // Merge with both kDelIfSrcEmpty and kSkipSubmessages |
| ASSERT_TRUE(root.Merge(AsConstBytes(patch.SerializeAsString()), |
| RwProto::Cursor::kDelIfSrcEmpty | |
| RwProto::Cursor::kSkipSubmessages) |
| .IsOk()); |
| |
| // Check root = {} (i.e. single_element is deleted, not skipped) |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_empty_)); |
| ASSERT_FALSE(entry.has_single_element()); |
| } |
| |
| TEST_F(RwProtoTest, SetBytes_IncompatibleWireType) { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| cursor.EnterField(protos::Element::kIdFieldNumber); |
| |
| // bytes represent a message, hence replacing a Scalar with bytes means we are |
| // changing the proto schema which is a logic error |
| ASSERT_TRUE(cursor.SetBytes(protozero::ConstBytes{}).IsAbort()); |
| } |
| |
| TEST_F(RwProtoTest, SetBytes_CanHandleEmptyPayload) { |
| // root = <empty bytes> |
| { |
| auto cursor = data_empty_.GetRoot(); |
| cursor.SetBytes(AsConstBytes("")); |
| |
| protos::TraceEntry entry; |
| auto proto = SerializeAsString(data_empty_); |
| entry.ParseFromString(proto); |
| |
| ASSERT_EQ(entry.elements_size(), 0); |
| } |
| // elements[0] = <empty bytes> |
| { |
| auto cursor = data_empty_.GetRoot(); |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| cursor.SetBytes(AsConstBytes("")); |
| |
| protos::TraceEntry entry; |
| auto proto = SerializeAsString(data_empty_); |
| entry.ParseFromString(proto); |
| |
| ASSERT_EQ(entry.elements_size(), 1); |
| ASSERT_FALSE(entry.elements(0).has_id()); |
| ASSERT_FALSE(entry.elements(0).has_value()); |
| } |
| } |
| |
| TEST_F(RwProtoTest, SetBytes_InitializesEmptyField) { |
| auto cursor = data_empty_.GetRoot(); |
| auto proto = SamplePackets{}.TraceEntryWithTwoElements().SerializeAsString(); |
| ASSERT_TRUE(cursor.SetBytes(AsConstBytes(proto)).IsOk()); |
| CheckProtoWithTwoElements(SerializeAsString(data_empty_)); |
| } |
| |
| TEST_F(RwProtoTest, SetBytes_UpdatesExistingField) { |
| auto root = data_empty_.GetRoot(); |
| |
| // elements[0] |
| { |
| auto element = root; |
| element.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| |
| // id field |
| { |
| auto id = element; |
| id.EnterField(protos::Element::kIdFieldNumber); |
| id.SetScalar(Scalar::VarInt(0xdeadbeef)); |
| } |
| |
| // overwrite existing id field |
| auto proto = SamplePackets::TraceEntryWithTwoElements() |
| .elements(0) |
| .SerializeAsString(); |
| element.SetBytes(AsConstBytes(proto)); |
| } |
| |
| // elements[1] |
| { |
| auto element = root; |
| element.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 1); |
| |
| // value field |
| { |
| auto value = element; |
| value.EnterField(protos::Element::kValueFieldNumber); |
| value.SetScalar(Scalar::VarInt(0xdeadbeef)); |
| } |
| |
| // overwrite existing id field |
| auto proto = SamplePackets::TraceEntryWithTwoElements() |
| .elements(1) |
| .SerializeAsString(); |
| element.SetBytes(AsConstBytes(proto)); |
| } |
| |
| CheckProtoWithTwoElements(SerializeAsString(data_empty_)); |
| } |
| |
| TEST_F(RwProtoTest, SetScalar_IncompatibleWireType) { |
| auto cursor = data_trace_entry_with_two_elements_.GetRoot(); |
| ASSERT_TRUE(cursor.SetScalar(Scalar::VarInt(0)).IsAbort()); |
| |
| cursor.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| ASSERT_TRUE(cursor.SetScalar(Scalar::VarInt(0)).IsAbort()); |
| } |
| |
| TEST_F(RwProtoTest, SetScalar_Success) { |
| auto cursor = data_empty_.GetRoot(); |
| |
| auto element0 = cursor; |
| element0.EnterRepeatedFieldAt(protos::TraceEntry::kElementsFieldNumber, 0); |
| |
| // element[0].value = 10 |
| auto value = element0; |
| value.EnterField(protos::Element::kValueFieldNumber); |
| value.SetScalar(Scalar::VarInt(10)); |
| |
| // element[0].value_fixed32 = 32 |
| auto value_fixed32 = element0; |
| value_fixed32.EnterField(protos::Element::kValueFixed32FieldNumber); |
| value_fixed32.SetScalar(Scalar::Fixed32(32)); |
| |
| // element[0].value_fixed64 = 64 |
| auto value_fixed64 = element0; |
| value_fixed64.EnterField(protos::Element::kValueFixed64FieldNumber); |
| value_fixed64.SetScalar(Scalar::Fixed64(64)); |
| |
| protos::TraceEntry entry; |
| entry.ParseFromString(SerializeAsString(data_empty_)); |
| |
| ASSERT_EQ(entry.elements_size(), 1); |
| ASSERT_EQ(entry.elements(0).value(), 10); |
| ASSERT_EQ(entry.elements(0).value_fixed32(), static_cast<uint32_t>(32)); |
| ASSERT_EQ(entry.elements(0).value_fixed64(), static_cast<uint64_t>(64)); |
| } |
| |
| TEST_F(RwProtoTest, SerializeAsString) { |
| CheckProtoWithTwoElements( |
| SerializeAsString(data_trace_entry_with_two_elements_)); |
| } |
| |
| TEST_F(RwProtoTest, AccessNonRootFieldAfterDeleteAborts) { |
| auto cursor = data_empty_.GetRoot(); |
| ASSERT_TRUE(cursor.EnterField(1).IsOk()); |
| ASSERT_TRUE(cursor.Delete().IsOk()); |
| |
| ASSERT_TRUE(cursor.IsRoot().IsAbort()); |
| ASSERT_TRUE(cursor.HasField(1).IsAbort()); |
| ASSERT_TRUE(cursor.EnterField(1).IsAbort()); |
| ASSERT_TRUE(cursor.EnterRepeatedFieldAt(1, 0).IsAbort()); |
| ASSERT_TRUE(cursor.IterateRepeatedField(1).IsAbort()); |
| ASSERT_TRUE(cursor.EnterRepeatedFieldByKey(1, 1, 0).IsAbort()); |
| ASSERT_TRUE(cursor.GetScalar().IsAbort()); |
| ASSERT_TRUE(cursor.SetBytes(protozero::ConstBytes{}).IsAbort()); |
| ASSERT_TRUE(cursor.SetScalar(Scalar::Fixed32(0)).IsAbort()); |
| ASSERT_TRUE(cursor.Merge(protozero::ConstBytes{}, 0).IsAbort()); |
| ASSERT_TRUE(cursor.Delete().IsAbort()); |
| } |
| |
| } // namespace test |
| } // namespace protovm |
| } // namespace perfetto |