def should_detect_if_a_type_was_removed_or_not(): old_schema = build_schema(""" type Type1 type Type2 """) new_schema = build_schema(""" type Type2 """) assert find_breaking_changes(old_schema, new_schema) == [ (BreakingChangeType.TYPE_REMOVED, "Type1 was removed.") ] assert find_breaking_changes(old_schema, old_schema) == []
def should_detect_if_a_field_argument_was_removed(): old_schema = build_schema(""" interface Interface1 { field1(arg1: Boolean, objectArg: String): String } type Type1 { field1(name: String): String } """) new_schema = build_schema(""" interface Interface1 { field1: String } type Type1 { field1: String } """) assert find_breaking_changes(old_schema, new_schema) == [ (BreakingChangeType.ARG_REMOVED, "Interface1.field1 arg arg1 was removed."), ( BreakingChangeType.ARG_REMOVED, "Interface1.field1 arg objectArg was removed.", ), (BreakingChangeType.ARG_REMOVED, "Type1.field1 arg name was removed."), ]
def should_not_flag_args_with_the_same_type_signature_as_breaking(): old_schema = build_schema( """ input InputType1 { field1: String } type Type1 { field1(arg1: Int!, arg2: InputType1): Int } """ ) new_schema = build_schema( """ input InputType1 { field1: String } type Type1 { field1(arg1: Int!, arg2: InputType1): Int } """ ) assert find_breaking_changes(old_schema, new_schema) == []
def should_detect_if_a_required_field_argument_was_added(): old_schema = build_schema( """ type Type1 { field1(arg1: String): String } """ ) new_schema = build_schema( """ type Type1 { field1( arg1: String, newRequiredArg: String! newOptionalArg1: Int newOptionalArg2: Int! = 0 ): String } """ ) assert find_breaking_changes(old_schema, new_schema) == [ ( BreakingChangeType.REQUIRED_ARG_ADDED, "A required arg newRequiredArg on Type1.field1 was added.", ) ]
def should_detect_if_a_standard_scalar_was_removed(): old_schema = build_schema( """ type Query { foo: Float } """ ) new_schema = build_schema( """ type Query { foo: String } """ ) assert find_breaking_changes(old_schema, new_schema) == [ ( BreakingChangeType.TYPE_REMOVED, "Standard scalar Float was removed" " because it is not referenced anymore.", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "Query.foo changed type from Float to String.", ), ]
def should_detect_if_a_value_was_removed_from_an_enum_type(): old_schema = build_schema( """ enum EnumType1 { VALUE0 VALUE1 VALUE2 } """ ) new_schema = build_schema( """ enum EnumType1 { VALUE0 VALUE2 VALUE3 } """ ) assert find_breaking_changes(old_schema, new_schema) == [ ( BreakingChangeType.VALUE_REMOVED_FROM_ENUM, "VALUE1 was removed from enum type EnumType1.", ) ]
def should_detect_if_a_type_was_removed_from_a_union_type(): old_schema = build_schema( """ type Type1 type Type2 type Type3 union UnionType1 = Type1 | Type2 """ ) new_schema = build_schema( """ type Type1 type Type2 type Type3 union UnionType1 = Type1 | Type3 """ ) assert find_breaking_changes(old_schema, new_schema) == [ ( BreakingChangeType.TYPE_REMOVED_FROM_UNION, "Type2 was removed from union type UnionType1.", ) ]
def should_detect_if_a_required_field_is_added_to_an_input_type(): old_schema = build_schema( """ input InputType1 { field1: String } """ ) new_schema = build_schema( """ input InputType1 { field1: String requiredField: Int! optionalField1: Boolean optionalField2: Boolean! = false } """ ) assert find_breaking_changes(old_schema, new_schema) == [ ( BreakingChangeType.REQUIRED_INPUT_FIELD_ADDED, "A required field requiredField on input type InputType1 was added.", ) ]
def should_detect_if_a_type_changed_its_type(): old_schema = build_schema(""" scalar TypeWasScalarBecomesEnum interface TypeWasInterfaceBecomesUnion type TypeWasObjectBecomesInputObject """) new_schema = build_schema(""" enum TypeWasScalarBecomesEnum union TypeWasInterfaceBecomesUnion input TypeWasObjectBecomesInputObject """) assert find_breaking_changes(old_schema, new_schema) == [ ( BreakingChangeType.TYPE_CHANGED_KIND, "TypeWasScalarBecomesEnum changed from a Scalar type to an Enum type.", ), ( BreakingChangeType.TYPE_CHANGED_KIND, "TypeWasInterfaceBecomesUnion changed" " from an Interface type to a Union type.", ), ( BreakingChangeType.TYPE_CHANGED_KIND, "TypeWasObjectBecomesInputObject changed" " from an Object type to an Input type.", ), ]
def should_detect_if_a_directive_was_implicitly_removed(): old_schema = GraphQLSchema() new_schema = GraphQLSchema( directives=[GraphQLSkipDirective, GraphQLIncludeDirective]) assert find_breaking_changes(old_schema, new_schema) == [( BreakingChangeType.DIRECTIVE_REMOVED, f"{GraphQLDeprecatedDirective.name} was removed.", )]
def should_detect_if_a_directive_argument_was_removed(): old_schema = build_schema(""" directive @DirectiveWithArg(arg1: String) on FIELD_DEFINITION """) new_schema = build_schema(""" directive @DirectiveWithArg on FIELD_DEFINITION """) assert find_breaking_changes(old_schema, new_schema) == [( BreakingChangeType.DIRECTIVE_ARG_REMOVED, "arg1 was removed from DirectiveWithArg.", )]
def should_detect_locations_removed_from_a_directive(): old_schema = build_schema(""" directive @DirectiveName on FIELD_DEFINITION | QUERY """) new_schema = build_schema(""" directive @DirectiveName on FIELD_DEFINITION """) assert find_breaking_changes(old_schema, new_schema) == [( BreakingChangeType.DIRECTIVE_LOCATION_REMOVED, "QUERY was removed from DirectiveName.", )]
def should_detect_if_a_directive_was_explicitly_removed(): old_schema = build_schema(""" directive @DirectiveThatIsRemoved on FIELD_DEFINITION directive @DirectiveThatStays on FIELD_DEFINITION """) new_schema = build_schema(""" directive @DirectiveThatStays on FIELD_DEFINITION """) assert find_breaking_changes(old_schema, new_schema) == [( BreakingChangeType.DIRECTIVE_REMOVED, "DirectiveThatIsRemoved was removed.", )]
def test_graphql_breaking_changes(schema_library: str) -> None: graphql_schema_versions = "graphql_schema_versions" zip_filepath = os.path.join(graphql_schema_versions, "old_schemas.zip") schema = compile_schema_library(schema_library) schemas = extract_zip(zip_filepath) minimal_version = get_minimal_supported_version(graphql_schema_versions) for ver, schema_str in schemas.items(): if LooseVersion(ver) < LooseVersion(minimal_version): continue schema_str = schema_str.decode("utf-8") old_schema = build_ast_schema(parse(schema_str)) breaking_changes = find_breaking_changes(old_schema, schema) assert len(breaking_changes) == 0, ERR_MSG_FORMAT.format( ver, breaking_changes)
def should_consider_args_that_move_away_from_non_null_as_non_breaking(): old_schema = build_schema(""" type Type1 { field1(name: String!): String } """) new_schema = build_schema(""" type Type1 { field1(name: String): String } """) assert find_breaking_changes(old_schema, new_schema) == []
def should_ignore_changes_in_order_of_interfaces(): old_schema = build_schema(""" interface FirstInterface interface SecondInterface type Type1 implements FirstInterface & SecondInterface """) new_schema = build_schema(""" interface FirstInterface interface SecondInterface type Type1 implements SecondInterface & FirstInterface """) assert find_breaking_changes(old_schema, new_schema) == []
def should_detect_interfaces_removed_from_types(): old_schema = build_schema(""" interface Interface1 type Type1 implements Interface1 """) new_schema = build_schema(""" interface Interface1 type Type1 """) assert find_breaking_changes(old_schema, new_schema) == [( BreakingChangeType.INTERFACE_REMOVED_FROM_OBJECT, "Type1 no longer implements interface Interface1.", )]
def should_detect_intrefaces_removed_from_interfaces(): old_schema = build_schema(""" interface Interface1 interface Interface2 implements Interface1 """) new_schema = build_schema(""" interface Interface1 interface Interface2 """) assert find_breaking_changes(old_schema, new_schema) == [( BreakingChangeType.IMPLEMENTED_INTERFACE_REMOVED, "Interface2 no longer implements interface Interface1.", )]
def should_detect_if_an_optional_directive_argument_was_added(): old_schema = build_schema(""" directive @DirectiveName on FIELD_DEFINITION """) new_schema = build_schema(""" directive @DirectiveName( newRequiredArg: String! newOptionalArg1: Int newOptionalArg2: Int! = 0 ) on FIELD_DEFINITION """) assert find_breaking_changes(old_schema, new_schema) == [( BreakingChangeType.REQUIRED_DIRECTIVE_ARG_ADDED, "A required arg newRequiredArg on directive DirectiveName was added.", )]
def should_detect_removal_of_repeatable_flag(): old_schema = build_schema( """ directive @DirectiveName repeatable on OBJECT """ ) new_schema = build_schema( """ directive @DirectiveName on OBJECT """ ) assert find_breaking_changes(old_schema, new_schema) == [ ( BreakingChangeType.DIRECTIVE_REPEATABLE_REMOVED, "Repeatable flag was removed from DirectiveName.", ) ]
def test_graphql_breaking_changes(self, mock_popen, mock_path_exists) -> None: schema_versions_library = pkg_resources.resource_filename( __name__, "schema_versions") schema_library = pkg_resources.resource_filename(__name__, "schema") zip_filepath = os.path.join(schema_versions_library, "old_schemas.zip") schema = compile_schema_library(schema_library) schemas = extract_zip(zip_filepath) minimal_version = get_minimal_supported_version( schema_versions_library) for ver, schema_str in schemas.items(): if LooseVersion(ver) < LooseVersion(minimal_version): continue schema_str = schema_str.decode("utf-8") old_schema = build_ast_schema(parse(schema_str)) breaking_changes = find_breaking_changes(old_schema, schema) assert len(breaking_changes) == 0, ERR_MSG_FORMAT.format( ver, breaking_changes)
def test_graphql_breaking_changes(self, mock_popen, mock_path_exists): schema_versions_library = pkg_resources.resource_filename( __name__, "schema_versions") schema_filepath = pkg_resources.resource_filename( __name__, "schema/symphony.graphql") zip_filepath = os.path.join(schema_versions_library, "old_schemas.zip") minimal_version_filepath = os.path.join( schema_versions_library, "minimal_supported_version") with open(schema_filepath) as schema_file: schema = build_ast_schema(parse(schema_file.read())) schemas = extract_zip(zip_filepath) with open(minimal_version_filepath) as minimal_version_file: minimal_version = minimal_version_file.read().strip() for ver, schema_str in schemas.items(): if LooseVersion(ver) < LooseVersion(minimal_version): continue schema_str = schema_str.decode("utf-8") old_schema = build_ast_schema(parse(schema_str)) breaking_changes = find_breaking_changes(old_schema, schema) assert len(breaking_changes) == 0, \ ERR_MSG_FORMAT.format(ver, breaking_changes)
def should_detect_if_a_field_on_type_was_deleted_or_changed_type(): old_schema = build_schema(""" type TypeA type TypeB interface Type1 { field1: TypeA field2: String field3: String field4: TypeA field6: String field7: [String] field8: Int field9: Int! field10: [Int]! field11: Int field12: [Int] field13: [Int!] field14: [Int] field15: [[Int]] field16: Int! field17: [Int] field18: [[Int!]!] } """) new_schema = build_schema(""" type TypeA type TypeB interface Type1 { field1: TypeA field3: Boolean field4: TypeB field5: String field6: [String] field7: String field8: Int! field9: Int field10: [Int] field11: [Int]! field12: [Int!] field13: [Int] field14: [[Int]] field15: [Int] field16: [Int]! field17: [Int]! field18: [[Int!]] } """) assert find_breaking_changes(old_schema, new_schema) == [ (BreakingChangeType.FIELD_REMOVED, "Type1.field2 was removed."), ( BreakingChangeType.FIELD_CHANGED_KIND, "Type1.field3 changed type from String to Boolean.", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "Type1.field4 changed type from TypeA to TypeB.", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "Type1.field6 changed type from String to [String].", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "Type1.field7 changed type from [String] to String.", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "Type1.field9 changed type from Int! to Int.", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "Type1.field10 changed type from [Int]! to [Int].", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "Type1.field11 changed type from Int to [Int]!.", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "Type1.field13 changed type from [Int!] to [Int].", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "Type1.field14 changed type from [Int] to [[Int]].", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "Type1.field15 changed type from [[Int]] to [Int].", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "Type1.field16 changed type from Int! to [Int]!.", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "Type1.field18 changed type from [[Int!]!] to [[Int!]].", ), ]
def should_detect_all_breaking_changes(): old_schema = build_schema(""" directive @DirectiveThatIsRemoved on FIELD_DEFINITION directive @DirectiveThatRemovesArg(arg1: String) on FIELD_DEFINITION directive @NonNullDirectiveAdded on FIELD_DEFINITION directive @DirectiveName on FIELD_DEFINITION | QUERY type ArgThatChanges { field1(id: Int): String } enum EnumTypeThatLosesAValue { VALUE0 VALUE1 VALUE2 } interface Interface1 type TypeThatLoosesInterface1 implements Interface1 type TypeInUnion1 type TypeInUnion2 union UnionTypeThatLosesAType = TypeInUnion1 | TypeInUnion2 type TypeThatChangesType type TypeThatGetsRemoved interface TypeThatHasBreakingFieldChanges { field1: String field2: String } """) new_schema = build_schema(""" directive @DirectiveThatRemovesArg on FIELD_DEFINITION directive @NonNullDirectiveAdded(arg1: Boolean!) on FIELD_DEFINITION directive @DirectiveName on FIELD_DEFINITION type ArgThatChanges { field1(id: String): String } enum EnumTypeThatLosesAValue { VALUE1 VALUE2 } interface Interface1 type TypeThatLoosesInterface1 type TypeInUnion1 type TypeInUnion2 union UnionTypeThatLosesAType = TypeInUnion1 interface TypeThatChangesType interface TypeThatHasBreakingFieldChanges { field2: Boolean } """) assert find_breaking_changes(old_schema, new_schema) == [ (BreakingChangeType.TYPE_REMOVED, "Int was removed."), (BreakingChangeType.TYPE_REMOVED, "TypeThatGetsRemoved was removed."), ( BreakingChangeType.ARG_CHANGED_KIND, "ArgThatChanges.field1 arg id has changed type from Int to String.", ), ( BreakingChangeType.VALUE_REMOVED_FROM_ENUM, "VALUE0 was removed from enum type EnumTypeThatLosesAValue.", ), ( BreakingChangeType.INTERFACE_REMOVED_FROM_OBJECT, "TypeThatLoosesInterface1 no longer implements interface Interface1.", ), ( BreakingChangeType.TYPE_REMOVED_FROM_UNION, "TypeInUnion2 was removed from union type UnionTypeThatLosesAType.", ), ( BreakingChangeType.TYPE_CHANGED_KIND, "TypeThatChangesType changed from an Object type to an" " Interface type.", ), ( BreakingChangeType.FIELD_REMOVED, "TypeThatHasBreakingFieldChanges.field1 was removed.", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "TypeThatHasBreakingFieldChanges.field2 changed type" " from String to Boolean.", ), ( BreakingChangeType.DIRECTIVE_REMOVED, "DirectiveThatIsRemoved was removed.", ), ( BreakingChangeType.DIRECTIVE_ARG_REMOVED, "arg1 was removed from DirectiveThatRemovesArg.", ), ( BreakingChangeType.REQUIRED_DIRECTIVE_ARG_ADDED, "A required arg arg1 on directive NonNullDirectiveAdded was added.", ), ( BreakingChangeType.DIRECTIVE_LOCATION_REMOVED, "QUERY was removed from DirectiveName.", ), ]
def should_detect_if_a_field_argument_has_changed_type(): old_schema = build_schema(""" type Type1 { field1( arg1: String arg2: String arg3: [String] arg4: String arg5: String! arg6: String! arg7: [Int]! arg8: Int arg9: [Int] arg10: [Int!] arg11: [Int] arg12: [[Int]] arg13: Int! arg14: [[Int]!] arg15: [[Int]!] ): String } """) new_schema = build_schema(""" type Type1 { field1( arg1: Int arg2: [String] arg3: String arg4: String! arg5: Int arg6: Int! arg7: [Int] arg8: [Int]! arg9: [Int!] arg10: [Int] arg11: [[Int]] arg12: [Int] arg13: [Int]! arg14: [[Int]] arg15: [[Int!]!] ): String } """) assert find_breaking_changes(old_schema, new_schema) == [ ( BreakingChangeType.ARG_CHANGED_KIND, "Type1.field1 arg arg1 has changed type from String to Int.", ), ( BreakingChangeType.ARG_CHANGED_KIND, "Type1.field1 arg arg2 has changed type from String to [String].", ), ( BreakingChangeType.ARG_CHANGED_KIND, "Type1.field1 arg arg3 has changed type from [String] to String.", ), ( BreakingChangeType.ARG_CHANGED_KIND, "Type1.field1 arg arg4 has changed type from String to String!.", ), ( BreakingChangeType.ARG_CHANGED_KIND, "Type1.field1 arg arg5 has changed type from String! to Int.", ), ( BreakingChangeType.ARG_CHANGED_KIND, "Type1.field1 arg arg6 has changed type from String! to Int!.", ), ( BreakingChangeType.ARG_CHANGED_KIND, "Type1.field1 arg arg8 has changed type from Int to [Int]!.", ), ( BreakingChangeType.ARG_CHANGED_KIND, "Type1.field1 arg arg9 has changed type from [Int] to [Int!].", ), ( BreakingChangeType.ARG_CHANGED_KIND, "Type1.field1 arg arg11 has changed type from [Int] to [[Int]].", ), ( BreakingChangeType.ARG_CHANGED_KIND, "Type1.field1 arg arg12 has changed type from [[Int]] to [Int].", ), ( BreakingChangeType.ARG_CHANGED_KIND, "Type1.field1 arg arg13 has changed type from Int! to [Int]!.", ), ( BreakingChangeType.ARG_CHANGED_KIND, "Type1.field1 arg arg15 has changed type from [[Int]!] to [[Int!]!].", ), ]
def should_detect_if_fields_on_input_types_changed_kind_or_were_removed(): old_schema = build_schema(""" input InputType1 { field1: String field2: Boolean field3: [String] field4: String! field5: String field6: [Int] field7: [Int]! field8: Int field9: [Int] field10: [Int!] field11: [Int] field12: [[Int]] field13: Int! field14: [[Int]!] field15: [[Int]!] } """) new_schema = build_schema(""" input InputType1 { field1: Int field3: String field4: String field5: String! field6: [Int]! field7: [Int] field8: [Int]! field9: [Int!] field10: [Int] field11: [[Int]] field12: [Int] field13: [Int]! field14: [[Int]] field15: [[Int!]!] } """) assert find_breaking_changes(old_schema, new_schema) == [ (BreakingChangeType.FIELD_REMOVED, "InputType1.field2 was removed."), ( BreakingChangeType.FIELD_CHANGED_KIND, "InputType1.field1 changed type from String to Int.", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "InputType1.field3 changed type from [String] to String.", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "InputType1.field5 changed type from String to String!.", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "InputType1.field6 changed type from [Int] to [Int]!.", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "InputType1.field8 changed type from Int to [Int]!.", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "InputType1.field9 changed type from [Int] to [Int!].", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "InputType1.field11 changed type from [Int] to [[Int]].", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "InputType1.field12 changed type from [[Int]] to [Int].", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "InputType1.field13 changed type from Int! to [Int]!.", ), ( BreakingChangeType.FIELD_CHANGED_KIND, "InputType1.field15 changed type from [[Int]!] to [[Int!]!].", ), ]
def should_detect_all_breaking_changes(): old_schema = build_schema(""" directive @DirectiveThatIsRemoved on FIELD_DEFINITION directive @DirectiveThatRemovesArg(arg1: String) on FIELD_DEFINITION directive @NonNullDirectiveAdded on FIELD_DEFINITION directive @DirectiveName on FIELD_DEFINITION | QUERY type ArgThatChanges { field1(id: Int): String } enum EnumTypeThatLosesAValue { VALUE0 VALUE1 VALUE2 } interface Interface1 { field1: String } type TypeThatGainsInterface1 implements Interface1 { field1: String } type TypeInUnion1 { field1: String } type TypeInUnion2 { field1: String } union UnionTypeThatLosesAType = TypeInUnion1 | TypeInUnion2 type TypeThatChangesType { field1: String } type TypeThatGetsRemoved { field1: String } interface TypeThatHasBreakingFieldChanges { field1: String field2: String } type Query { field1: String } """) # noqa new_schema = build_schema(""" directive @DirectiveThatRemovesArg on FIELD_DEFINITION directive @NonNullDirectiveAdded(arg1: Boolean!) on FIELD_DEFINITION directive @DirectiveName on FIELD_DEFINITION type ArgThatChanges { field1(id: String): String } enum EnumTypeThatLosesAValue { VALUE1 VALUE2 } interface Interface1 { field1: String } type TypeInUnion1 { field1: String } union UnionTypeThatLosesAType = TypeInUnion1 interface TypeThatChangesType { field1: String } type TypeThatGainsInterface1 { field1: String } interface TypeThatHasBreakingFieldChanges { field2: Boolean } type Query { field1: String } """) # noqa expected_breaking_changes = [ (BreakingChangeType.TYPE_REMOVED, 'Int was removed.'), (BreakingChangeType.TYPE_REMOVED, 'TypeInUnion2 was removed.'), (BreakingChangeType.TYPE_REMOVED, 'TypeThatGetsRemoved was removed.'), (BreakingChangeType.TYPE_CHANGED_KIND, 'TypeThatChangesType changed from an Object type to an' ' Interface type.'), (BreakingChangeType.FIELD_REMOVED, 'TypeThatHasBreakingFieldChanges.field1 was removed.'), (BreakingChangeType.FIELD_CHANGED_KIND, 'TypeThatHasBreakingFieldChanges.field2 changed type' ' from String to Boolean.'), (BreakingChangeType.TYPE_REMOVED_FROM_UNION, 'TypeInUnion2 was removed from union type' ' UnionTypeThatLosesAType.'), (BreakingChangeType.VALUE_REMOVED_FROM_ENUM, 'VALUE0 was removed from enum type EnumTypeThatLosesAValue.'), (BreakingChangeType.ARG_CHANGED_KIND, 'ArgThatChanges.field1 arg id has changed' ' type from Int to String'), (BreakingChangeType.INTERFACE_REMOVED_FROM_OBJECT, 'TypeThatGainsInterface1 no longer implements' ' interface Interface1.'), (BreakingChangeType.DIRECTIVE_REMOVED, 'DirectiveThatIsRemoved was removed'), (BreakingChangeType.DIRECTIVE_ARG_REMOVED, 'arg1 was removed from DirectiveThatRemovesArg'), (BreakingChangeType.NON_NULL_DIRECTIVE_ARG_ADDED, 'A non-null arg arg1 on directive' ' NonNullDirectiveAdded was added'), (BreakingChangeType.DIRECTIVE_LOCATION_REMOVED, 'QUERY was removed from DirectiveName') ] assert find_breaking_changes(old_schema, new_schema) == expected_breaking_changes