예제 #1
0
class ResourceReferenceTest(unittest.TestCase):
    # This is for tesing the behavior of resources in comparators.
    # The resource can be defined in file-level and message-level.
    # And it is referenced in field-level. So whether the removal of a resource is a breaking change
    # depends on the information from multiple levels (From File or Message to Field).
    # UnittestInvoker helps us to execute the protoc command to compile the proto file,
    # get a *_descriptor_set.pb file (by -o option) which contains the serialized data in protos, and
    # create a FileDescriptorSet (_PB_ORIGNAL and _PB_UPDATE) out of it.
    PROTO_DIR = os.path.join(os.getcwd(), "test/testdata/protos/example/")
    COMMON_PROTOS_DIR = os.path.join(os.getcwd(), "api-common-protos")

    def setUp(self):
        self.finding_container = FindingContainer()

    def test_resources_change(self):
        _INVOKER_ORIGNAL = Loader(
            proto_definition_dirs=[self.PROTO_DIR, self.COMMON_PROTOS_DIR],
            proto_files=[os.path.join(self.PROTO_DIR, "resource_database_v1.proto")],
            descriptor_set=None,
        )
        _INVOKER_UPDATE = Loader(
            proto_definition_dirs=[self.PROTO_DIR, self.COMMON_PROTOS_DIR],
            proto_files=[
                os.path.join(self.PROTO_DIR, "resource_database_v1beta1.proto")
            ],
            descriptor_set=None,
        )
        FileSetComparator(
            FileSet(_INVOKER_ORIGNAL.get_descriptor_set()),
            FileSet(_INVOKER_UPDATE.get_descriptor_set()),
            self.finding_container,
        ).compare()

        addition_finding = next(
            f
            for f in self.finding_container.get_all_findings()
            if f.category.name == "RESOURCE_PATTERN_ADDITION"
        )
        removal_finding = next(
            f
            for f in self.finding_container.get_all_findings()
            if f.category.name == "RESOURCE_PATTERN_REMOVAL"
        )
        resource_definition_removal_finding = next(
            f
            for f in self.finding_container.get_all_findings()
            if f.category.name == "RESOURCE_DEFINITION_REMOVAL"
        )
        self.assertEqual(
            addition_finding.location.proto_file_name,
            "resource_database_v1.proto",
        )
        self.assertEqual(addition_finding.location.source_code_line, 13)

        self.assertEqual(
            removal_finding.location.proto_file_name,
            "resource_database_v1.proto",
        )
        self.assertEqual(
            removal_finding.location.source_code_line,
            13,
        )
        self.assertEqual(
            resource_definition_removal_finding.location.proto_file_name,
            "resource_database_v1.proto",
        )
        self.assertEqual(
            resource_definition_removal_finding.location.source_code_line, 34
        )
        self.assertEqual(resource_definition_removal_finding.change_type.value, 1)

    def test_resource_reference_change(self):
        _INVOKER_ORIGNAL = Loader(
            proto_definition_dirs=[self.PROTO_DIR, self.COMMON_PROTOS_DIR],
            proto_files=[os.path.join(self.PROTO_DIR, "resource_reference_v1.proto")],
            descriptor_set=None,
        )
        _INVOKER_UPDATE = Loader(
            proto_definition_dirs=[self.PROTO_DIR, self.COMMON_PROTOS_DIR],
            proto_files=[
                os.path.join(self.PROTO_DIR, "resource_reference_v1beta1.proto")
            ],
            descriptor_set=None,
        )
        FileSetComparator(
            FileSet(_INVOKER_ORIGNAL.get_descriptor_set()),
            FileSet(_INVOKER_UPDATE.get_descriptor_set()),
            self.finding_container,
        ).compare()
        finding = next(
            f
            for f in self.finding_container.get_all_findings()
            if f.category.name == "RESOURCE_REFERENCE_CHANGE_CHILD_TYPE"
        )
        self.assertEqual(
            finding.location.proto_file_name, "resource_reference_v1beta1.proto"
        )
        self.assertEqual(finding.location.source_code_line, 25)
        # Find more details in comments of `resource_reference_v1beta1.proto`
        # 1. Resource_reference annotation is removed for `string name=1`,
        # but it is added in message-level. Non-breaking change.
        # 2. File-level resource definition `t2` is removed, but is added
        # to message-level resource. Non-breaking change.
        breaking_changes = self.finding_container.get_actionable_findings()
        self.assertEqual(len(breaking_changes), 1)
class FieldComparatorTest(unittest.TestCase):
    def setUp(self):
        self.finding_container = FindingContainer()

    def test_field_removal(self):
        field_foo = make_field("Foo")
        FieldComparator(
            field_foo, None, self.finding_container, context="ctx"
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "FIELD_REMOVAL")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_field_addition(self):
        field_foo = make_field("Foo")
        FieldComparator(
            None, field_foo, self.finding_container, context="ctx"
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "FIELD_ADDITION")

    def test_name_change(self):
        field_foo = make_field("Foo", nested_path=["foo"])
        field_bar = make_field("Bar", nested_path=["bar"])
        FieldComparator(
            field_foo, field_bar, self.finding_container, context="ctx"
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "FIELD_NAME_CHANGE")
        self.assertEqual(finding.extra_info[0], "foo")

    def test_repeated_label_change(self):
        field_repeated = make_field(repeated=True)
        field_non_repeated = make_field(repeated=False)
        FieldComparator(
            field_repeated, field_non_repeated, self.finding_container, context="ctx"
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "FIELD_REPEATED_CHANGE")

    def test_field_behavior_change(self):
        field_required = make_field(required=True)
        field_non_required = make_field(required=False)
        # Required to optional, non-breaking change.
        FieldComparator(
            field_required, field_non_required, self.finding_container, context="ctx"
        ).compare()
        findings = self.finding_container.get_all_findings()
        self.assertFalse(findings)
        # Required to optional, non-breaking change.
        FieldComparator(
            field_non_required, field_required, self.finding_container, context="ctx"
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "FIELD_BEHAVIOR_CHANGE")

    def test_primitive_type_change(self):
        field_int = make_field(proto_type="TYPE_INT32")
        field_string = make_field(proto_type="TYPE_STRING")
        FieldComparator(
            field_int, field_string, self.finding_container, context="ctx"
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "FIELD_TYPE_CHANGE")

    def test_message_type_change(self):
        field_message = make_field(type_name=".example.v1.Enum")
        field_message_update = make_field(type_name=".example.v1beta1.EnumUpdate")
        FieldComparator(
            field_message, field_message_update, self.finding_container, context="ctx"
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "FIELD_TYPE_CHANGE")

    def test_message_type_change_minor_version_update(self):
        field_message = make_field(type_name=".example.v1.Enum", api_version="v1")
        field_message_update = make_field(
            type_name=".example.v1beta1.Enum", api_version="v1beta1"
        )
        FieldComparator(
            field_message, field_message_update, self.finding_container, context="ctx"
        ).compare()
        findings = self.finding_container.get_all_findings()
        self.assertFalse(findings)

    def test_type_change_map_entry1(self):
        # Existing field is message_type, while the update field type is map. Breaking change.
        # Normally it will catch by type_name comparison. But in case we have a
        # message name as `{FieldName}Entry` which is the same as auto-generated nested message name,
        # we still consider the condition that the type of an existing field is changed from
        # normal message (`{FieldName}Entry`) to map entry message (`{FieldName}Entry`).
        field_no_map = make_field(
            proto_type="TYPE_MESSAGE", type_name=".exmaple.MapEntry"
        )
        # [Constructing] map<string, string> field
        key_field = make_field(proto_type="TYPE_STRING", number=1)
        value_field = make_field(proto_type="TYPE_STRING", number=2)
        field_map = make_field(
            proto_type="TYPE_MESSAGE",
            type_name=".exmaple.MapEntry",
            map_entry={"key": key_field, "value": value_field},
        )
        FieldComparator(
            field_no_map, field_map, self.finding_container, context="ctx"
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.category.name, "FIELD_TYPE_CHANGE")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_type_change_map_entry2(self):
        # Existing field type is a map, while the update field type is a normal message. Breaking change.
        # Normally it will catch by type_name comparison. But in case we have a
        # message name as `{FieldName}Entry` which is the same as auto-generated nested message name,
        # we still consider the condition that the type of an existing field is changed from
        # map entry message (`{FieldName}Entry`) to normal message (`{FieldName}Entry`).
        field_no_map = make_field(
            proto_type="TYPE_MESSAGE", type_name=".exmaple.MapEntry"
        )
        # [Constructing] map<string, string> field
        key_field = make_field(proto_type="TYPE_STRING", number=1)
        value_field = make_field(proto_type="TYPE_STRING", number=2)
        field_map = make_field(
            proto_type="TYPE_MESSAGE",
            type_name=".exmaple.MapEntry",
            map_entry={"key": key_field, "value": value_field},
        )
        FieldComparator(
            field_map, field_no_map, self.finding_container, context="ctx"
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.category.name, "FIELD_TYPE_CHANGE")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_type_change_map_entry3(self):
        # Both fields are map type. But the key, value types are not identical. Breaking change.
        # [Constructing] map<string, string> field
        key_original = make_field(proto_type="TYPE_STRING", number=1)
        value_original = make_field(proto_type="TYPE_STRING", number=2)
        field_original = make_field(
            proto_type="TYPE_MESSAGE",
            type_name=".exmaple.MapEntry",
            map_entry={"key": key_original, "value": value_original},
        )
        # [Constructing] map<key, value> field
        key_update = make_field(
            proto_type="TYPE_MESSAGE", number=1, type_name=".example.key"
        )
        value_update = make_field(
            proto_type="TYPE_MESSAGE", number=2, type_name=".example.value"
        )
        field_update = make_field(
            proto_type="TYPE_MESSAGE",
            type_name=".exmaple.MapEntry",
            map_entry={"key": key_update, "value": value_update},
        )

        FieldComparator(
            field_original, field_update, self.finding_container, context="ctx"
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.category.name, "FIELD_TYPE_CHANGE")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_type_change_map_entry4(self):
        # Both fields are map type. But the key, value types are not identical.
        # But only the versions pare are different. Non-breaking change.
        # [Constructing] map<string, .example.v1.value> field
        key_original = make_field(proto_type="TYPE_STRING", number=1)
        value_original = make_field(
            proto_type="TYPE_MESSAGE", type_name=".example.v1.value", number=2
        )
        field_original = make_field(
            proto_type="TYPE_MESSAGE",
            type_name=".exmaple.MapEntry",
            map_entry={"key": key_original, "value": value_original},
            api_version="v1",
        )
        # [Constructing] map<string, .example.v1beta1.value> field
        key_update = make_field(proto_type="TYPE_STRING", number=1)
        value_update = make_field(
            proto_type="TYPE_MESSAGE", number=2, type_name=".example.v1beta1.value"
        )
        field_update = make_field(
            proto_type="TYPE_MESSAGE",
            type_name=".exmaple.MapEntry",
            map_entry={"key": key_update, "value": value_update},
            api_version="v1beta1",
        )

        FieldComparator(
            field_original, field_update, self.finding_container, context="ctx"
        ).compare()
        finding = self.finding_container.get_all_findings()
        self.assertFalse(finding)

    def test_out_oneof(self):
        field_oneof = make_field(name="Foo", oneof_index=0, oneof_name="oneof_field")
        field_not_oneof = make_field(name="Foo")
        FieldComparator(
            field_oneof, field_not_oneof, self.finding_container, context="ctx"
        ).compare()
        finding = next(
            f
            for f in self.finding_container.get_all_findings()
            if f.category.name == "FIELD_ONEOF_MOVE_OUT"
        )
        self.assertTrue(finding)

    def test_into_oneof(self):
        field_oneof = make_field(name="Foo", oneof_index=0, oneof_name="oneof_field")
        field_not_oneof = make_field(name="Foo")
        FieldComparator(
            field_not_oneof, field_oneof, self.finding_container, context="ctx"
        ).compare()
        finding = next(
            f
            for f in self.finding_container.get_all_findings()
            if f.category.name == "FIELD_ONEOF_MOVE_IN"
        )
        self.assertTrue(finding)

    def test_proto3_optional_to_required(self):
        # Change an proto3 optional field to required. Breaking change.
        field_optional = make_field(
            name="Foo", oneof_index=0, oneof_name="oneof_field", proto3_optional=True
        )
        field_not_optional = make_field(
            name="Foo", oneof_index=0, oneof_name="oneof_field"
        )
        FieldComparator(
            field_optional, field_not_optional, self.finding_container, context="ctx"
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "FIELD_PROTO3_OPTIONAL_CHANGE")

    def test_proto3_required_to_optional(self):
        # Change required field to be proto3 optional. Non-breaking change.
        field_optional = make_field(
            name="Foo", oneof_index=0, oneof_name="oneof_field", proto3_optional=True
        )
        field_not_optional = make_field(
            name="Foo", oneof_index=0, oneof_name="oneof_field"
        )
        FieldComparator(
            field_not_optional, field_optional, self.finding_container, context="ctx"
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "FIELD_PROTO3_OPTIONAL_CHANGE")

    def test_resource_reference_addition_breaking(self):
        # The added resource reference is not in the database. Breaking change.
        # The original field is without resource reference.
        field_without_reference = make_field(name="Test")
        # The update field has resource reference, but it does not exist
        # in the global database.
        field_options = make_field_annotation_resource_reference(
            resource_type="example.v1/Foo", is_child_type=False
        )
        field_with_reference = make_field(name="Test", options=field_options)
        FieldComparator(
            field_without_reference,
            field_with_reference,
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "RESOURCE_REFERENCE_ADDITION")

    def test_resource_reference_type_addition_non_breaking(self):
        # The added resource reference is in the database. Non-breaking change.
        # The original field is without resource reference.
        field_without_reference = make_field(name="Test")
        # Create a database with resource `example.v1/Foo` registered.
        resource = make_resource_descriptor(
            resource_type="example.v1/Foo", resource_patterns=["foo/{foo}"]
        )
        resource_database = make_resource_database(resources=[resource])
        # The update field has resource reference of type `example.v1/Foo`.
        field_options = desc.FieldOptions()
        field_options.Extensions[
            resource_pb2.resource_reference
        ].type = "example.v1/Foo"
        field_with_reference = make_field(
            name="Test", options=field_options, resource_database=resource_database
        )
        FieldComparator(
            field_without_reference,
            field_with_reference,
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "RESOURCE_REFERENCE_ADDITION")
        self.assertEqual(finding.change_type.name, "MINOR")

    def test_resource_reference_child_type_addition_non_breaking(self):
        # The added resource reference is in the database. Non-breaking change.
        # The original field is without resource reference.
        field_without_reference = make_field(name="Test")
        # Create a database with resource `example.v1/Foo` registered.
        resource = make_resource_descriptor(
            resource_type="example.v1/Foo", resource_patterns=["foo/{foo}"]
        )
        resource_child = make_resource_descriptor(
            resource_type="example.v1/Bar",
            resource_patterns=["foo/{foo}/bar/{bar}"],
        )
        resource_database = make_resource_database(resources=[resource, resource_child])
        # The update field has resource reference of child_type `example.v1/Bar`.
        field_options = desc.FieldOptions()
        field_options.Extensions[
            resource_pb2.resource_reference
        ].child_type = "example.v1/Bar"
        field_with_reference = make_field(
            name="Test", options=field_options, resource_database=resource_database
        )
        FieldComparator(
            field_without_reference,
            field_with_reference,
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "RESOURCE_REFERENCE_ADDITION")
        self.assertEqual(finding.change_type.name, "MINOR")

    def test_resource_reference_removal_breaking1(self):
        # Removed resource reference is not added in message options. Breaking.
        # Original field has resource reference `example.v1/Foo`.
        field_options = make_field_annotation_resource_reference(
            resource_type="example.v1/Foo", is_child_type=False
        )
        field_with_reference = make_field(name="Test", options=field_options)
        # The update field has no resource reference, and no resource reference is
        # defined in the message.
        field_without_reference = make_field(name="Test")
        FieldComparator(
            field_with_reference,
            field_without_reference,
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_actionable_findings()[0]
        self.assertEqual(finding.category.name, "RESOURCE_REFERENCE_REMOVAL")
        self.assertEqual(finding.change_type.name, "MAJOR")

    def test_resource_reference_removal_breaking2(self):
        # Removed resource reference is defined by type, which is not identical
        # with the message options.
        # Original field has resource reference `example.v1/Foo`.
        field_options = make_field_annotation_resource_reference(
            resource_type="example.v1/Foo", is_child_type=False
        )
        field_with_reference = make_field(name="Test", options=field_options)
        # Update field has no resource reference, and the resource type
        # is different from the message options type.
        message_resource = make_resource_descriptor(
            resource_type="NotInteresting", resource_patterns=["NotInteresting"]
        )
        field_without_reference = make_field(
            name="Test", message_resource=message_resource
        )
        FieldComparator(
            field_with_reference,
            field_without_reference,
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_actionable_findings()[0]
        self.assertEqual(finding.category.name, "RESOURCE_REFERENCE_REMOVAL")
        self.assertEqual(finding.change_type.name, "MAJOR")

    def test_resource_reference_removal_breaking3(self):
        # Removed resource reference is defined by child type, which can not
        # be resolved to identical resource with the message options.
        # Original field has resource reference `example.v1/Foo`.
        field_options = make_field_annotation_resource_reference(
            resource_type="example.v1/Foo", is_child_type=True
        )
        field_with_reference = make_field(name="Test", options=field_options)
        # Update field has no resource reference, and the removed resource child type
        # is not identical with the message resource option.
        message_resource = make_resource_descriptor(
            resource_type="example.v1/Bar", resource_patterns=["bar/{bar}"]
        )
        field_resource = make_resource_descriptor(
            resource_type="example.v1/Foo", resource_patterns=["foo/{foo}"]
        )
        # Register the two resources in the database.
        resource_database = make_resource_database(
            resources=[message_resource, field_resource]
        )

        field_without_reference = make_field(
            name="Test",
            resource_database=resource_database,
            message_resource=message_resource,
        )
        FieldComparator(
            field_with_reference,
            field_without_reference,
            self.finding_container,
            context="ctx",
        ).compare()
        # `bar/{bar}` is not parent resource of `foo/{foo}`.
        finding = self.finding_container.get_actionable_findings()[0]
        self.assertEqual(finding.category.name, "RESOURCE_REFERENCE_REMOVAL")
        self.assertEqual(finding.change_type.name, "MAJOR")

    def test_resource_reference_removal_non_breaking1(self):
        # Removed resource reference is defined by type, and it is
        # added back to the message options.
        # Original field has resource reference `example.v1/Foo`.
        field_options = make_field_annotation_resource_reference(
            resource_type="example.v1/Foo", is_child_type=False
        )
        field_with_reference = make_field(name="Test", options=field_options)
        # Update field has no resource reference. But the message has
        # resource options `example.v1/Foo`.
        message_resource = make_resource_descriptor(
            resource_type="example.v1/Foo", resource_patterns=["bar/{bar}"]
        )

        field_without_reference = make_field(
            name="Test", message_resource=message_resource
        )
        FieldComparator(
            field_with_reference,
            field_without_reference,
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "RESOURCE_REFERENCE_MOVED")
        self.assertEqual(finding.change_type.name, "MINOR")

    def test_resource_reference_removal_non_breaking2(self):
        # Removed resource reference is defined by child type, and it
        # can be resolved to the same resource with the message options.
        # Original field has resource reference `example.v1/Foo`.
        field_options = make_field_annotation_resource_reference(
            resource_type="example.v1/Foo", is_child_type=False
        )
        field_with_reference = make_field(name="Test", options=field_options)
        # Update field has no resource reference. But the message has
        # resource options `example.v1/Foo`.
        message_resource = make_resource_descriptor(
            resource_type="example.v1/Foo", resource_patterns=["bar/{bar}"]
        )
        field_resource = make_resource_descriptor(
            resource_type="example.v1/Foo", resource_patterns=["bar/{bar}/foo/{foo}"]
        )
        # Register the two resources in the database.
        resource_database = make_resource_database(
            resources=[message_resource, field_resource]
        )
        field_without_reference = make_field(
            name="Test",
            message_resource=message_resource,
            resource_database=resource_database,
        )
        # `bar/{bar}` is the parent resource of `bar/{bar}/foo/{foo}`.
        FieldComparator(
            field_with_reference,
            field_without_reference,
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "RESOURCE_REFERENCE_MOVED")
        self.assertEqual(finding.change_type.name, "MINOR")

    def test_resource_reference_change_same_type_non_breaking(self):
        # The field has the identical resource reference.
        field_options = make_field_annotation_resource_reference(
            resource_type="example.v1/Foo", is_child_type=False
        )
        field_with_reference = make_field(name="Test", options=field_options)
        FieldComparator(
            field_with_reference,
            field_with_reference,
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()
        # No breaking change should be detected.
        self.assertFalse(finding)

    def test_resource_reference_change_same_child_type_non_breaking(self):
        # The field has the identical resource reference.
        field_options = make_field_annotation_resource_reference(
            resource_type="example.v1/Foo", is_child_type=True
        )
        field_with_reference = make_field(name="Test", options=field_options)
        FieldComparator(
            field_with_reference,
            field_with_reference,
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()
        # No breaking change should be detected.
        self.assertFalse(finding)

    def test_resource_reference_change_same_type_breaking(self):
        # Both fields have resource reference identified by type.
        # But the type value is different. Breaking change.
        field_options_foo = make_field_annotation_resource_reference(
            resource_type="example.v1/Foo", is_child_type=False
        )
        field_options_bar = make_field_annotation_resource_reference(
            resource_type="example.v1/Bar", is_child_type=False
        )
        field_with_reference_foo = make_field(name="Test", options=field_options_foo)
        field_with_reference_bar = make_field(name="Test", options=field_options_bar)
        FieldComparator(
            field_with_reference_foo,
            field_with_reference_bar,
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.change_type.name, "MAJOR")

    def test_resource_reference_change_type_conversion_non_breaking(self):
        child_resource = make_resource_descriptor(
            resource_type="example.v1/Foo",
            resource_patterns=["bar/{bar}/foo/{foo}", "bar/{bar}/foo"],
        )
        parent_resource = make_resource_descriptor(
            resource_type="example.v1/Bar", resource_patterns=["bar/{bar}"]
        )
        # Register two resources in database.
        resource_database = make_resource_database(
            resources=[child_resource, parent_resource]
        )
        # The original field is defined by child type.
        field_options_child = make_field_annotation_resource_reference(
            resource_type="example.v1/Foo", is_child_type=True
        )
        field_with_reference_child = make_field(
            name="Test",
            options=field_options_child,
            resource_database=resource_database,
        )

        # The update field is defined by parent type.
        field_options_parent = make_field_annotation_resource_reference(
            resource_type="example.v1/Bar", is_child_type=False
        )
        field_with_reference_parent = make_field(
            name="Test",
            options=field_options_parent,
            resource_database=resource_database,
        )
        # The two resources can be resolved to the identical resource.
        FieldComparator(
            field_with_reference_child,
            field_with_reference_parent,
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()
        # No breaking change should be detected.
        self.assertFalse(finding)

        # Reverse should be same since the two resources can
        # be resolved to the identical resource.
        FieldComparator(
            field_with_reference_parent,
            field_with_reference_child,
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()
        # No breaking change should be detected.
        self.assertFalse(finding)

    def test_resource_reference_change_type_conversion_breaking(self):
        resource_bar = make_resource_descriptor(
            resource_type="example.v1/Bar",
            resource_patterns=["bar/{bar}/foo/{foo}", "bar/{bar}/foo"],
        )
        resource_foo = make_resource_descriptor(
            resource_type="example.v1/Foo", resource_patterns=["foo/{foo}"]
        )
        # Register two resources in database.
        resource_database = make_resource_database(
            resources=[resource_bar, resource_foo]
        )
        # The original field is defined by child type.
        field_options_child = make_field_annotation_resource_reference(
            resource_type="example.v1/Bar", is_child_type=True
        )
        field_with_reference_child = make_field(
            name="Test",
            options=field_options_child,
            resource_database=resource_database,
        )

        # The update field is defined by parent type.
        field_options_parent = make_field_annotation_resource_reference(
            resource_type="example.v1/Foo", is_child_type=False
        )
        field_with_reference_parent = make_field(
            name="Test",
            options=field_options_parent,
            resource_database=resource_database,
        )
        # The two resources can nnot be resolved to the identical resource.
        FieldComparator(
            field_with_reference_child,
            field_with_reference_parent,
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "RESOURCE_REFERENCE_CHANGE_CHILD_TYPE")
        self.assertEqual(finding.change_type.name, "MAJOR")
예제 #3
0
class FileSetComparatorTest(unittest.TestCase):
    def setUp(self):
        self.finding_container = FindingContainer()

    def test_service_removal(self):
        file_set = make_file_set(
            files=[make_file_pb2(services=[make_service()], )])
        FileSetComparator(
            file_set,
            make_file_set(),
            self.finding_container,
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.change_type.name, "MAJOR")

    def test_service_addition(self):
        file_set = make_file_set(
            files=[make_file_pb2(services=[make_service()], )])
        FileSetComparator(
            make_file_set(),
            file_set,
            self.finding_container,
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "SERVICE_ADDITION")
        self.assertEqual(finding.change_type.name, "MINOR")

    def test_service_change(self):
        input_message = make_message(name="request",
                                     full_name=".example.v1.request")
        output_message = make_message(name="response",
                                      full_name=".example.v1.response")
        service_original = make_service(methods=[
            make_method(
                name="DoThing",
                input_message=input_message,
                output_message=output_message,
            )
        ])
        service_update = make_service()
        FileSetComparator(
            make_file_set(files=[
                make_file_pb2(
                    services=[service_original],
                    messages=[input_message, output_message],
                )
            ]),
            make_file_set(files=[make_file_pb2(services=[service_update])]),
            self.finding_container,
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "METHOD_REMOVAL")
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.location.proto_file_name, "my_proto.proto")

    def test_message_change_breaking(self):
        message_original = make_message(
            fields=(make_field(name="field_one", number=1), ))
        message_update = make_message(
            fields=(make_field(name="field_two", number=1), ))
        FileSetComparator(
            make_file_set(files=[make_file_pb2(messages=[message_original])]),
            make_file_set(files=[make_file_pb2(messages=[message_update])]),
            self.finding_container,
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.category.name, "FIELD_NAME_CHANGE")
        self.assertEqual(finding.location.proto_file_name, "my_proto.proto")

    def test_message_in_dependency_change_breaking(self):
        # Message "dep_message" is imported from dep.proto and referenced as a field type.
        field_type_original = make_message(
            name="dep_message",
            proto_file_name="dep.proto",
        )
        message_original = make_message(
            fields=[make_field(type_name=".test.import.dep_message")], )
        # Message "test_message" is defined in my_proto.proto referenced as a field type.
        field_type_update = make_message(name="test_message", )
        message_update = make_message(
            fields=[make_field(type_name="test_message")])
        FileSetComparator(
            make_file_set(files=[
                make_file_pb2(
                    name="original.proto",
                    messages=[message_original],
                    dependency=["test/import/dep.proto"],
                    package="example.v1",
                ),
                make_file_pb2(
                    name="test/import/dep.proto",
                    messages=[field_type_original],
                    package="test.import",
                ),
            ]),
            make_file_set(files=[
                make_file_pb2(
                    name="update.proto",
                    messages=[field_type_update, message_update],
                    package="example.v1beta1",
                )
            ]),
            self.finding_container,
        ).compare()
        # The breaking change should be in field level, instead of message removal,
        # since the message is imported from dependency file.
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.category.name, "FIELD_TYPE_CHANGE")
        self.assertEqual(finding.location.proto_file_name, "update.proto")

    def test_enum_change(self):
        enum_original = make_enum(
            name="Irrelevant",
            values=(
                ("RED", 1),
                ("GREEN", 2),
                ("BLUE", 3),
            ),
        )
        enum_update = make_enum(
            name="Irrelevant",
            values=(
                ("RED", 1),
                ("GREEN", 2),
            ),
        )
        FileSetComparator(
            make_file_set(files=[make_file_pb2(enums=[enum_original])]),
            make_file_set(files=[make_file_pb2(enums=[enum_update])]),
            self.finding_container,
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "ENUM_VALUE_REMOVAL")
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.location.proto_file_name, "my_proto.proto")

    def test_enum_in_dependency_change_breaking(self):
        # Enum "dep_message" is imported from dep.proto and referenced as a field type.
        field_type_original = make_enum(
            name="dep_enum",
            proto_file_name="dep.proto",
        )
        message_original = make_message(
            fields=[make_field(type_name=".test.import.dep_enum")], )
        # Message "test_enum" is defined in update.proto referenced as a field type.
        field_type_update = make_enum(name="test_enum", )
        message_update = make_message(
            fields=[make_field(type_name="test_enum")])
        FileSetComparator(
            make_file_set(files=[
                make_file_pb2(
                    name="original.proto",
                    messages=[message_original],
                    dependency=["test/import/dep.proto"],
                    package="example.v1",
                ),
                make_file_pb2(
                    name="test/import/dep.proto",
                    enums=[field_type_original],
                    package="test.import",
                ),
            ]),
            make_file_set(files=[
                make_file_pb2(
                    name="update.proto",
                    messages=[message_update],
                    enums=[field_type_update],
                    package="example.v1beta1",
                )
            ]),
            self.finding_container,
        ).compare()
        # The breaking change should be in field level, instead of message removal,
        # since the message is imported from dependency file.
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.category.name, "FIELD_TYPE_CHANGE")
        self.assertEqual(finding.location.proto_file_name, "update.proto")

    def test_resources_existing_pattern_change(self):
        options_original = make_file_options_resource_definition(
            resource_type=".example.v1.Bar",
            resource_patterns=["foo/{foo}/bar/{bar}"])
        file_pb2 = make_file_pb2(name="foo.proto",
                                 package=".example.v1",
                                 options=options_original)
        file_set_original = make_file_set(files=[file_pb2])
        options_update = make_file_options_resource_definition(
            resource_type=".example.v1.Bar",
            resource_patterns=["foo/{foo}/bar/"])
        file_pb2 = make_file_pb2(name="foo.proto",
                                 package=".example.v1",
                                 options=options_update)
        file_set_update = make_file_set(files=[file_pb2])

        FileSetComparator(file_set_original, file_set_update,
                          self.finding_container).compare()
        finding = next(f for f in self.finding_container.get_all_findings()
                       if f.change_type.name == "MAJOR")
        self.assertEqual(finding.category.name, "RESOURCE_PATTERN_REMOVAL")
        self.assertEqual(
            finding.location.proto_file_name,
            "foo.proto",
        )

    def test_resources_existing_pattern_removal(self):
        options_original = make_file_options_resource_definition(
            resource_type=".example.v1.Bar",
            resource_patterns=["bar/{bar}", "foo/{foo}/bar"],
        )
        file_pb2 = make_file_pb2(name="foo.proto",
                                 package=".example.v1",
                                 options=options_original)
        file_set_original = make_file_set(files=[file_pb2])

        options_update = make_file_options_resource_definition(
            resource_type=".example.v1.Bar", resource_patterns=["bar/{bar}"])
        file_pb2 = make_file_pb2(name="foo.proto",
                                 package=".example.v1",
                                 options=options_update)
        file_set_update = make_file_set(files=[file_pb2])

        FileSetComparator(file_set_original, file_set_update,
                          self.finding_container).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.category.name, "RESOURCE_PATTERN_REMOVAL")
        self.assertEqual(
            finding.location.proto_file_name,
            "foo.proto",
        )

    def test_resources_addition(self):
        file_set_original = make_file_set(
            files=[make_file_pb2(name="foo.proto", package=".example.v1")])

        options_update = make_file_options_resource_definition(
            resource_type=".example.v1.Bar",
            resource_patterns=["foo/{foo}/bar/{bar}"])
        file_pb2 = make_file_pb2(name="foo.proto",
                                 package=".example.v1",
                                 options=options_update)
        file_set_update = make_file_set(files=[file_pb2])
        FileSetComparator(file_set_original, file_set_update,
                          self.finding_container).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.change_type.name, "MINOR")
        self.assertEqual(
            finding.category.name,
            "RESOURCE_DEFINITION_ADDITION",
        )
        self.assertEqual(
            finding.location.proto_file_name,
            "foo.proto",
        )

    def test_resources_removal(self):
        # Create message with resource options.
        message_options = make_message_options_resource_definition(
            resource_type="example.v1/Bar",
            resource_patterns=["user/{user}", "user/{user}/bar/"],
        )
        message = make_message("Test", options=message_options)
        # Original file set with one resource defined at message level.
        file_set_original = make_file_set(files=[
            make_file_pb2(
                name="bar.proto", package=".example.v1", messages=[message])
        ])
        # Update file set without any resources.
        file_set_update = make_file_set(
            files=[make_file_pb2(name="foo.proto", package=".example.v1")])
        FileSetComparator(file_set_original, file_set_update,
                          self.finding_container).compare()
        file_resource_removal = next(
            f for f in self.finding_container.get_all_findings()
            if f.category.name == "RESOURCE_DEFINITION_REMOVAL")
        self.assertEqual(
            file_resource_removal.location.proto_file_name,
            "bar.proto",
        )

    def test_java_multiple_files_removal(self):
        option1 = descriptor_pb2.FileOptions()
        option1.java_multiple_files = True
        file1 = make_file_pb2(
            name="file.proto",
            package="example.v1",
            options=option1,
        )
        file_set_original = make_file_set(files=[file1])

        option2 = descriptor_pb2.FileOptions()
        option2.java_multiple_files = False
        file2 = make_file_pb2(
            name="file.proto",
            package="example.v1",
            options=option2,
        )
        file_set_update = make_file_set(files=[file2])
        FileSetComparator(file_set_original, file_set_update,
                          self.finding_container).compare()
        self.assertTrue(len(self.finding_container.get_all_findings()))
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "PACKAGING_OPTION_REMOVAL")
        self.assertEqual(finding.change_type.name, "MAJOR")

    def test_java_multiple_files_addition(self):
        option1 = descriptor_pb2.FileOptions()
        option1.java_multiple_files = False
        file1 = make_file_pb2(
            name="file.proto",
            package="example.v1",
            options=option1,
        )
        file_set_original = make_file_set(files=[file1])

        option2 = descriptor_pb2.FileOptions()
        option2.java_multiple_files = True
        file2 = make_file_pb2(
            name="file.proto",
            package="example.v1",
            options=option2,
        )
        file_set_update = make_file_set(files=[file2])
        FileSetComparator(file_set_original, file_set_update,
                          self.finding_container).compare()
        self.assertTrue(len(self.finding_container.get_all_findings()))
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "PACKAGING_OPTION_ADDITION")
        self.assertEqual(finding.change_type.name, "MAJOR")

    def test_java_outer_classname_removal(self):
        option1 = descriptor_pb2.FileOptions()
        option1.java_outer_classname = "Foo"
        file1 = make_file_pb2(
            name="fil1.proto",
            package="example.v1",
            options=option1,
        )
        option2 = descriptor_pb2.FileOptions()
        option2.java_outer_classname = "Bar"
        file2 = make_file_pb2(
            name="fil2.proto",
            package="example.v1",
            options=option2,
        )
        file_set_original = make_file_set(files=[file1, file2])
        option3 = descriptor_pb2.FileOptions()
        option3.java_outer_classname = "Bar"
        file3 = make_file_pb2(name="file3.proto",
                              package="example.v1beta",
                              options=option3)
        file_set_update = make_file_set(files=[file3])
        FileSetComparator(file_set_original, file_set_update,
                          self.finding_container).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "PACKAGING_OPTION_REMOVAL")
        self.assertEqual(finding.change_type.name, "MAJOR")

    def test_packaging_options_change(self):
        file_options_original = descriptor_pb2.FileOptions()
        file_options_original.php_namespace = "Google\\Cloud\\Service\\V1"
        file_options_original.csharp_namespace = "Google.Cloud.Service.V1"
        file_options_original.java_outer_classname = "ServiceProto"
        file_original = make_file_pb2(
            name="original.proto",
            package="google.cloud.service.v1",
            options=file_options_original,
        )

        file_options_update = descriptor_pb2.FileOptions()
        # Breaking since version should be updated to `v1alpha`
        file_options_update.php_namespace = "Google\\Cloud\\Service\\V1beta"
        # No breaking change
        file_options_update.csharp_namespace = "Google.Cloud.Service.V1alpha"
        file_options_update.java_outer_classname = "ServiceUpdateProto"
        file_update = make_file_pb2(
            name="update.proto",
            package="google.cloud.service.v1alpha",
            options=file_options_update,
        )

        FileSetComparator(
            make_file_set(files=[file_original]),
            make_file_set(files=[file_update]),
            self.finding_container,
        ).compare()
        java_classname_option_removal = next(
            f for f in self.finding_container.get_all_findings()
            if f.category.name == "PACKAGING_OPTION_REMOVAL"
            and f.subject == "java_outer_classname")
        php_namespace_option_removal = next(
            f for f in self.finding_container.get_all_findings()
            if f.category.name == "PACKAGING_OPTION_REMOVAL"
            and f.subject == "php_namespace")
        self.assertTrue(java_classname_option_removal)
        self.assertTrue(php_namespace_option_removal)

    def test_packaging_options_version_update(self):
        file_options_original = descriptor_pb2.FileOptions()
        file_options_original.java_outer_classname = "ServiceProto"
        file_options_original.java_package = "com.google.cloud.service.v1"
        file_options_original.csharp_namespace = "Google.Cloud.Service.V1"
        file_options_original.php_namespace = "Google\\Cloud\\Service\\V1"
        file_options_original.ruby_package = "Google::Cloud::Service::V1"
        file_original = make_file_pb2(
            name="original.proto",
            package="google.cloud.service.v1",
            options=file_options_original,
        )

        file_options_update = descriptor_pb2.FileOptions()
        file_options_update.java_outer_classname = "ServiceProto"
        file_options_update.java_package = "com.google.cloud.service.v1alpha"
        file_options_update.csharp_namespace = "Google.Cloud.Service.V1alpha"
        file_options_update.php_namespace = "Google\\Cloud\\Service\\V1alpha"
        file_options_update.ruby_package = "Google::Cloud::Service::V1alpha"
        file_update = make_file_pb2(
            name="update.proto",
            package="google.cloud.service.v1alpha",
            options=file_options_update,
        )

        FileSetComparator(
            make_file_set(files=[file_original]),
            make_file_set(files=[file_update]),
            self.finding_container,
        ).compare()
        self.assertEqual(self.finding_container.get_all_findings(), [])
예제 #4
0
class ServiceComparatorTest(unittest.TestCase):
    def setUp(self):
        self.service_foo = make_service(name="Foo")
        self.finding_container = FindingContainer()

    def test_service_removal(self):
        ServiceComparator(self.service_foo,
                          None,
                          self.finding_container,
                          context="ctx").compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "SERVICE_REMOVAL")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_service_addition(self):
        ServiceComparator(None,
                          self.service_foo,
                          self.finding_container,
                          context="ctx").compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "SERVICE_ADDITION")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_service_host_addition(self):
        service_without_host = make_service()
        service_with_host = make_service(host="api.google.com")
        ServiceComparator(
            service_without_host,
            service_with_host,
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "SERVICE_HOST_ADDITION")
        self.assertEqual(finding.change_type.name, "MINOR")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_service_host_removal(self):
        service_without_host = make_service()
        service_with_host = make_service(host="api.google.com")
        ServiceComparator(
            service_with_host,
            service_without_host,
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "SERVICE_HOST_REMOVAL")
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_service_host_change(self):
        service_original = make_service(host="default.host")
        service_update = make_service(host="default.host.update")
        ServiceComparator(service_original,
                          service_update,
                          self.finding_container,
                          context="ctx").compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "SERVICE_HOST_CHANGE")
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_service_oauth_scopes_change(self):
        service_original = make_service(scopes=("https://foo/user/",
                                                "https://foo/admin/"))
        service_update = make_service(
            scopes=("https://www.googleapis.com/auth/cloud-platform"))
        ServiceComparator(service_original,
                          service_update,
                          self.finding_container,
                          context="ctx").compare()
        finding = next(f for f in self.finding_container.get_all_findings()
                       if f.category.name == "OAUTH_SCOPE_REMOVAL"
                       and f.subject == "https://foo/user/")
        self.assertEqual(finding.location.proto_file_name, "foo")
        finding = next(f for f in self.finding_container.get_all_findings()
                       if f.category.name == "OAUTH_SCOPE_REMOVAL"
                       and f.subject == "https://foo/admin/")
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_method_removal(self):
        method_foo = make_method(name="foo")
        method_bar = make_method(name="bar")
        service_original = make_service(methods=(method_foo, method_bar))
        service_update = make_service(methods=(method_foo, ))
        ServiceComparator(service_original,
                          service_update,
                          self.finding_container,
                          context="ctx").compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "METHOD_REMOVAL")
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_method_addition(self):
        method_foo = make_method(name="foo")
        method_bar = make_method(name="bar")
        service_original = make_service(methods=(method_foo, ))
        service_update = make_service(methods=(method_foo, method_bar))
        ServiceComparator(service_original,
                          service_update,
                          self.finding_container,
                          context="ctx").compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "METHOD_ADDTION")
        self.assertEqual(finding.change_type.name, "MINOR")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_method_input_type_change(self):
        message_foo_request = make_message(name="FooRequest",
                                           full_name="FooRequest")
        message_bar_request = make_message(name="BarRequest",
                                           full_name="BarRequest")
        service_original = make_service(methods=(
            make_method(name="Foo", input_message=message_foo_request), ))
        service_update = make_service(methods=(
            make_method(name="Foo", input_message=message_bar_request), ))
        ServiceComparator(service_original,
                          service_update,
                          self.finding_container,
                          context="ctx").compare()
        finding = next(f for f in self.finding_container.get_all_findings()
                       if f.category.name == "METHOD_INPUT_TYPE_CHANGE")
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_method_output_type_change(self):
        service_original = make_service(methods=(make_method(
            name="Foo",
            output_message=make_message(name="FooResponse",
                                        full_name=".example.FooResponse"),
        ), ))
        service_update = make_service(methods=(make_method(
            name="Foo",
            output_message=make_message(name="BarResponse",
                                        full_name=".example.BarResponse"),
        ), ))
        ServiceComparator(service_original,
                          service_update,
                          self.finding_container,
                          context="ctx").compare()
        finding = next(f for f in self.finding_container.get_all_findings()
                       if f.category.name == "METHOD_RESPONSE_TYPE_CHANGE")
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_method_streaming_state_change(self):
        service_original = make_service(
            methods=(make_method(name="Bar", client_streaming=True), ))
        service_update = make_service(
            methods=(make_method(name="Bar", server_streaming=True), ))
        ServiceComparator(service_original,
                          service_update,
                          self.finding_container,
                          context="ctx").compare()

        client_streaming_finding = next(
            f for f in self.finding_container.get_all_findings()
            if f.category.name == "METHOD_CLIENT_STREAMING_CHANGE")
        self.assertEqual(client_streaming_finding.location.proto_file_name,
                         "foo")
        server_streaming_finding = next(
            f for f in self.finding_container.get_all_findings()
            if f.category.name == "METHOD_SERVER_STREAMING_CHANGE")
        self.assertEqual(server_streaming_finding.change_type.name, "MAJOR")
        self.assertEqual(server_streaming_finding.location.proto_file_name,
                         "foo")

    def test_method_paginated_state_change(self):
        paginated_response_message = make_message(
            name="PaginatedResponseMessage",
            fields=[
                make_field(
                    name="repeated_field",
                    proto_type="TYPE_STRING",
                    repeated=True,
                    number=1,
                ),
                make_field(
                    name="next_page_token",
                    proto_type="TYPE_STRING",
                    number=2,
                ),
            ],
            full_name=".example.v1.PaginatedResponseMessage",
        )
        paginated_request_message = make_message(
            name="PaginatedRequestMessage",
            fields=[
                make_field(
                    name="page_size",
                    proto_type="TYPE_INT32",
                    number=1,
                ),
                make_field(
                    name="page_token",
                    proto_type="TYPE_STRING",
                    number=2,
                ),
            ],
            full_name=".example.v1.PaginatedRequestMessage",
        )
        messages_map_original = {
            ".example.v1.PaginatedResponseMessage": paginated_response_message,
            ".example.v1.PaginatedRequestMessage": paginated_request_message,
        }
        paged_method = make_method(
            name="NotInteresting",
            input_message=paginated_request_message,
            output_message=paginated_response_message,
        )
        messages_map_update = {
            ".example.v1alpha.MethodInput":
            make_message(name="MethodInput",
                         full_name=".example.v1alpha.MethodInput"),
            ".example.v1alpha.MethodOutput":
            make_message(name="MethodOutput",
                         full_name=".example.v1alpha.MethodOutput"),
        }
        non_paged_method = make_method(name="NotInteresting")
        service_original = make_service(methods=(paged_method, ),
                                        messages_map=messages_map_original)
        service_update = make_service(methods=(non_paged_method, ),
                                      messages_map=messages_map_update)
        ServiceComparator(service_original,
                          service_update,
                          self.finding_container,
                          context="ctx").compare()
        finding = next(
            f for f in self.finding_container.get_all_findings()
            if f.category.name == "METHOD_PAGINATED_RESPONSE_CHANGE")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_method_signature_removal(self):
        ServiceComparator(
            make_service(methods=(make_method(name="NotInteresting",
                                              signatures=["sig1", "sig2"]), )),
            make_service(methods=(
                make_method(name="NotInteresting", signatures=["sig1"]), )),
            self.finding_container,
            context="ctx",
        ).compare()
        finding = next(f for f in self.finding_container.get_all_findings()
                       if f.category.name == "METHOD_SIGNATURE_REMOVAL")
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_lro_annotation_addition(self):
        lro_output_msg = make_message(
            name=".google.longrunning.Operation",
            full_name=".google.longrunning.Operation",
        )
        method_lro = make_method(
            name="Method",
            output_message=lro_output_msg,
            lro_response_type="response_type",
            lro_metadata_type="FooMetadata",
        )
        method_not_lro = make_method(name="Method", )
        ServiceComparator(
            make_service(methods=(method_not_lro, )),
            make_service(methods=(method_lro, )),
            self.finding_container,
            context="ctx",
        ).compare()
        finding = next(f for f in self.finding_container.get_all_findings()
                       if f.category.name == "LRO_ANNOTATION_ADDITION")
        self.assertTrue(finding)

    def test_lro_annotation_removal(self):
        lro_output_msg = make_message(
            name=".google.longrunning.Operation",
            full_name=".google.longrunning.Operation",
        )
        method_lro = make_method(
            name="Method",
            output_message=lro_output_msg,
            lro_response_type="response_type",
            lro_metadata_type="FooMetadata",
        )
        method_not_lro = make_method(name="Method", )
        ServiceComparator(
            make_service(methods=(method_lro, )),
            make_service(methods=(method_not_lro, )),
            self.finding_container,
            context="ctx",
        ).compare()
        finding = next(f for f in self.finding_container.get_all_findings()
                       if f.category.name == "LRO_ANNOTATION_REMOVAL")
        self.assertTrue(finding)

    def test_lro_annotation_invalid(self):
        lro_output_msg = make_message(
            name=".google.longrunning.Operation",
            full_name=".google.longrunning.Operation",
        )
        method_lro = make_method(
            name="Method",
            output_message=lro_output_msg,
            lro_response_type="response_type",
            lro_metadata_type="FooMetadata",
        )
        method_not_lro = make_method(
            name="Method",
            output_message=lro_output_msg,
        )
        # `method_not_lro` returns `google.longrunning.Operation`
        # but is missing a response type or metadata type, the definition
        # is invalid. We still compare the two methods and take it as
        # lro annotation removal.
        ServiceComparator(
            make_service(methods=(method_lro, )),
            make_service(methods=(method_not_lro, )),
            self.finding_container,
            context="ctx",
        ).compare()
        finding = next(f for f in self.finding_container.get_all_findings()
                       if f.category.name == "LRO_ANNOTATION_REMOVAL")
        self.assertTrue(finding)

    def test_lro_annotation_response_change(self):
        lro_output_msg = make_message(
            name=".google.longrunning.Operation",
            full_name=".google.longrunning.Operation",
        )
        method_original = make_method(
            name="Method",
            output_message=lro_output_msg,
            lro_response_type="response_type",
            lro_metadata_type="FooMetadata",
        )
        method_update = make_method(
            name="Method",
            output_message=lro_output_msg,
            lro_response_type="response_type_update",
            lro_metadata_type="FooMetadata",
        )
        ServiceComparator(
            make_service(methods=(method_original, )),
            make_service(methods=(method_update, )),
            self.finding_container,
            context="ctx",
        ).compare()
        finding = next(f for f in self.finding_container.get_all_findings()
                       if f.category.name == "LRO_RESPONSE_CHANGE")
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_lro_annotation_metadata_change(self):
        lro_output_msg = make_message(
            name=".google.longrunning.Operation",
            full_name=".google.longrunning.Operation",
        )
        method_original = make_method(
            name="Method",
            output_message=lro_output_msg,
            lro_response_type="response_type",
            lro_metadata_type="FooMetadata",
        )
        method_update = make_method(
            name="Method",
            output_message=lro_output_msg,
            lro_response_type="response_type",
            lro_metadata_type="FooMetadataUpdate",
        )
        ServiceComparator(
            make_service(methods=(method_original, )),
            make_service(methods=(method_update, )),
            self.finding_container,
            context="ctx",
        ).compare()
        finding = next(f for f in self.finding_container.get_all_findings()
                       if f.category.name == "LRO_METADATA_CHANGE")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_http_annotation_addition(self):
        method_without_http_annotation = make_method(name="Method", )
        method_with_http_annotation = make_method(
            name="Method",
            http_uri="http_uri_update",
            http_body="http_body",
        )
        ServiceComparator(
            make_service(methods=(method_without_http_annotation, )),
            make_service(methods=(method_with_http_annotation, )),
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "HTTP_ANNOTATION_ADDITION")
        self.assertEqual(finding.change_type.name, "MINOR")

    def test_http_annotation_removal(self):
        method_without_http_annotation = make_method(name="Method", )
        method_with_http_annotation = make_method(
            name="Method",
            http_uri="http_uri_update",
            http_body="http_body",
        )
        ServiceComparator(
            make_service(methods=(method_with_http_annotation, )),
            make_service(methods=(method_without_http_annotation, )),
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "HTTP_ANNOTATION_REMOVAL")
        self.assertEqual(finding.change_type.name, "MAJOR")

    def test_http_annotation_change(self):
        method_original = make_method(
            name="Method",
            http_uri="http_uri",
            http_body="*",
        )
        method_update = make_method(
            name="Method",
            http_uri="http_uri_update",
            http_body="http_body",
        )
        ServiceComparator(
            make_service(methods=(method_original, )),
            make_service(methods=(method_update, )),
            self.finding_container,
            context="ctx",
        ).compare()
        uri_change_finding = next(
            f for f in self.finding_container.get_all_findings()
            if f.category.name == "HTTP_ANNOTATION_CHANGE"
            and f.type == "http_uri")
        body_change_finding = next(
            f for f in self.finding_container.get_all_findings()
            if f.category.name == "HTTP_ANNOTATION_CHANGE"
            and f.type == "http_body")
        self.assertEqual(
            uri_change_finding.location.proto_file_name,
            "foo",
        )
        self.assertEqual(
            body_change_finding.location.proto_file_name,
            "foo",
        )

    def test_http_annotation_minor_version_update(self):
        method_original = make_method(
            name="Method",
            http_uri="/v1/{name=projects/*}",
            http_body="*",
        )
        method_update = make_method(
            name="Method",
            http_uri="/v1alpha/{name=projects/*}",
            http_body="*",
        )
        ServiceComparator(
            make_service(methods=(method_original, ), api_version="v1"),
            make_service(methods=(method_update, ), api_version="v1alpha"),
            self.finding_container,
            context="ctx",
        ).compare()
        findings_map = {
            f.message: f
            for f in self.finding_container.get_all_findings()
        }
        # No breaking changes, since only minor version update in the http URI.
        self.assertFalse(findings_map)
class DescriptorComparatorTest(unittest.TestCase):
    def setUp(self):
        self.message_foo = make_message("Message")
        self.finding_container = FindingContainer()

    def test_message_removal(self):
        DescriptorComparator(self.message_foo,
                             None,
                             self.finding_container,
                             context="ctx").compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "MESSAGE_REMOVAL")
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_message_addition(self):
        DescriptorComparator(None,
                             self.message_foo,
                             self.finding_container,
                             context="ctx").compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "MESSAGE_ADDITION")
        self.assertEqual(finding.change_type.name, "MINOR")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_nested_field_type_change(self):
        field_int = make_field(proto_type="TYPE_INT32")
        field_string = make_field(proto_type="TYPE_STRING")
        message1 = make_message(fields=[field_int])
        message2 = make_message(fields=[field_string])
        DescriptorComparator(message1,
                             message2,
                             self.finding_container,
                             context="ctx").compare()
        finding = next(f for f in self.finding_container.get_all_findings()
                       if f.category.name == "FIELD_TYPE_CHANGE")
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_nested_field_addition(self):
        field_int = make_field(proto_type="TYPE_INT32")
        message_update = make_message(fields=[field_int])
        DescriptorComparator(make_message(),
                             message_update,
                             self.finding_container,
                             context="ctx").compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "FIELD_ADDITION")
        self.assertEqual(finding.change_type.name, "MINOR")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_nested_message_removal(self):
        DescriptorComparator(
            make_message(nested_messages=[make_message(
                name="nested_message")]),
            make_message(),
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "MESSAGE_REMOVAL")
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_nested_message_addition(self):
        DescriptorComparator(
            make_message(),
            make_message(nested_messages=[make_message(
                name="nested_message")]),
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "MESSAGE_ADDITION")
        self.assertEqual(finding.change_type.name, "MINOR")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_nested_enum_addition(self):
        DescriptorComparator(
            make_message(),
            make_message(nested_enums=[make_enum(name="nested_message")]),
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "ENUM_ADDITION")
        self.assertEqual(finding.change_type.name, "MINOR")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_nested_enum_removal(self):
        DescriptorComparator(
            make_message(nested_enums=[make_enum(name="nested_message")]),
            make_message(),
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "ENUM_REMOVAL")
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_nested_message_change(self):
        nested_field = make_field(name="nested_field")
        nested_message_with_fields = make_message(name="nested_message",
                                                  fields=[nested_field])
        nested_message_without_fields = make_message(name="nested_message")
        message1 = make_message(nested_messages=[nested_message_with_fields])
        message2 = make_message(
            nested_messages=[nested_message_without_fields])
        DescriptorComparator(message1,
                             message2,
                             self.finding_container,
                             context="ctx").compare()
        finding = next(f for f in self.finding_container.get_all_findings()
                       if f.category.name == "FIELD_REMOVAL")
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.location.proto_file_name, "foo")

    def test_nested_enum_change(self):
        nested_enum1 = make_enum(
            name="Foo",
            values=(
                ("RED", 1),
                ("GREEN", 2),
            ),
        )
        nested_enum2 = make_enum(
            name="Foo",
            values=(
                ("RED", 1),
                ("GREEN", 2),
                ("BLUE", 3),
            ),
        )
        message1 = make_message(nested_enums=[nested_enum1])
        message2 = make_message(nested_enums=[nested_enum2])
        DescriptorComparator(message1,
                             message2,
                             self.finding_container,
                             context="ctx").compare()
        finding = next(f for f in self.finding_container.get_all_findings()
                       if f.category.name == "ENUM_VALUE_ADDITION")
        self.assertEqual(finding.change_type.name, "MINOR")
        self.assertEqual(finding.location.proto_file_name, "foo")
class EnumValueComparatorTest(unittest.TestCase):
    def setUp(self):
        L = descriptor_pb2.SourceCodeInfo.Location
        locations = [L(path=(2, 1), span=(1, 2))]
        self.enum_foo = make_enum_value(
            name="FOO",
            number=1,
            proto_file_name="test.proto",
            locations=locations,
            path=(2, 1),
        )
        self.enum_bar = make_enum_value(
            name="BAR",
            number=1,
            proto_file_name="test_update.proto",
            locations=locations,
            path=(2, 1),
        )
        self.finding_container = FindingContainer()

    def test_enum_value_removal(self):
        EnumValueComparator(
            self.enum_foo,
            None,
            self.finding_container,
            context="ctx",
        ).compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "ENUM_VALUE_REMOVAL")
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.location.proto_file_name, "test.proto")
        self.assertEqual(finding.location.source_code_line, 2)

    def test_enum_value_addition(self):
        EnumValueComparator(None,
                            self.enum_foo,
                            self.finding_container,
                            context="ctx").compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "ENUM_VALUE_ADDITION")
        self.assertEqual(finding.change_type.name, "MINOR")
        self.assertEqual(finding.location.proto_file_name, "test.proto")
        self.assertEqual(finding.location.source_code_line, 2)

    def test_name_change(self):
        EnumValueComparator(self.enum_foo,
                            self.enum_bar,
                            self.finding_container,
                            context="ctx").compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "ENUM_VALUE_NAME_CHANGE")
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.location.proto_file_name, "test_update.proto")
        self.assertEqual(finding.location.source_code_line, 2)

    def test_no_api_change(self):
        EnumValueComparator(self.enum_foo,
                            self.enum_foo,
                            self.finding_container,
                            context="ctx").compare()
        self.assertEqual(len(self.finding_container.get_all_findings()), 0)
class EnumComparatorTest(unittest.TestCase):
    def setUp(self):
        L = descriptor_pb2.SourceCodeInfo.Location
        # fmt: off
        locations = [
            L(path=(
                4,
                0,
            ), span=(1, 2, 3, 4)),
            # Enum will add (2, 1) for each EnumValue in the path.
            L(path=(4, 0, 2, 1), span=(2, 3, 4, 5)),
        ]
        self.enum_foo = make_enum(
            name="Foo",
            values=(("VALUE1", 1), ),
            proto_file_name="test.proto",
            locations=locations,
            path=(
                4,
                0,
            ),
        )
        self.enum_bar = make_enum(
            name="Bar",
            values=(
                ("VALUE1", 1),
                ("VALUE2", 2),
            ),
            proto_file_name="test_update.proto",
            locations=locations,
            path=(
                4,
                0,
            ),
        )
        self.finding_container = FindingContainer()
        # fmt: on

    def test_enum_removal(self):
        EnumComparator(self.enum_foo,
                       None,
                       self.finding_container,
                       context="ctx").compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "ENUM_REMOVAL")
        self.assertEqual(finding.change_type.name, "MAJOR")
        self.assertEqual(finding.location.proto_file_name, "test.proto")
        self.assertEqual(finding.location.source_code_line, 2)

    def test_enum_addition(self):
        EnumComparator(None,
                       self.enum_bar,
                       self.finding_container,
                       context="ctx").compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "ENUM_ADDITION")
        self.assertEqual(finding.change_type.name, "MINOR")
        self.assertEqual(finding.location.proto_file_name, "test_update.proto")
        self.assertEqual(finding.location.source_code_line, 2)

    def test_enum_value_change(self):
        EnumComparator(self.enum_foo,
                       self.enum_bar,
                       self.finding_container,
                       context="ctx").compare()
        finding = self.finding_container.get_all_findings()[0]
        self.assertEqual(finding.category.name, "ENUM_VALUE_ADDITION")
        self.assertEqual(finding.change_type.name, "MINOR")
        self.assertEqual(finding.location.proto_file_name, "test_update.proto")
        self.assertEqual(finding.location.source_code_line, 3)

    def test_no_api_change(self):
        EnumComparator(self.enum_foo,
                       self.enum_foo,
                       self.finding_container,
                       context="ctx").compare()
        self.assertEqual(len(self.finding_container.get_all_findings()), 0)