Ejemplo n.º 1
0
def test_directive_location_added_and_removed():
    one_location = schema("""
    directive @somedir on FIELD_DEFINITION
    type A {
        a: String
    }
    """)
    two_locations = schema("""
    directive @somedir on FIELD_DEFINITION | FIELD
    type A {
        a: String
    }
    """)
    diff = Schema(one_location, two_locations).diff()
    assert diff and len(diff) == 1
    expected_message = (
        "Directive locations of `@somedir` changed from `FIELD_DEFINITION` to `FIELD_DEFINITION | FIELD`"
    )
    assert diff[0].message == expected_message
    assert diff[0].path == '@somedir'
    assert diff[0].criticality == Criticality.safe()

    diff = Schema(two_locations, one_location).diff()
    assert diff and len(diff) == 1
    expected_message = (
        "Directive locations of `@somedir` changed from `FIELD_DEFINITION | FIELD` to `FIELD_DEFINITION`"
    )
    assert diff[0].message == expected_message
    assert diff[0].path == '@somedir'
    assert diff[0].criticality == Criticality.breaking(
        'Removing a directive location will break any instance of its usage. Be sure no one uses it before removing it'
    )
def test_input_field_added_field():
    a = schema("""
    input Recipe {
        ingredients: [String]
    }
    """)
    b = schema("""
    input Recipe {
        ingredients: [String]
        love: Float
    }
    """)
    diff = Schema(a, b).diff()
    assert diff and len(diff) == 1
    assert diff[0].message == (
        "Input Field `love: Float` was added to input type `Recipe`")
    assert diff[0].path == 'Recipe.love'
    assert diff[0].criticality == Criticality.safe()

    diff = Schema(b, a).diff()
    assert diff and len(diff) == 1
    assert diff[0].message == (
        "Input Field `love` removed from input type `Recipe`")
    assert diff[0].path == 'Recipe.love'
    assert diff[0].criticality == Criticality.breaking(
        'Removing an input field will break queries that use this input field.'
    )
Ejemplo n.º 3
0
def test_interface_field_added_and_removed():
    a = schema("""
    interface Person {
      name: String
      age: Int
    }
    """)
    b = schema("""
    interface Person {
      name: String
      age: Int
      favorite_number: Float
    }
    """)

    diff = Schema(a, b).diff()
    assert diff and len(diff) == 1
    assert diff[
        0].message == "Field `favorite_number` of type `Float` was added to interface `Person`"
    assert diff[0].path == 'Person.favorite_number'
    assert diff[0].criticality == Criticality.dangerous(
        'Adding an interface to an object type may break existing clients '
        'that were not programming defensively against a new possible type.')

    diff = Schema(b, a).diff()
    assert diff[
        0].message == "Field `favorite_number` was removed from interface `Person`"
    assert diff[0].path == 'Person.favorite_number'
    assert diff[0].criticality == Criticality.dangerous(
        'Removing an interface field can break existing queries that use this in a fragment spread.'
    )
Ejemplo n.º 4
0
def test_added_removed_directive():
    no_directive = schema("""
    type A {
        a: String
    }
    """)
    one_directive = schema("""
    directive @somedir on FIELD_DEFINITION
    type A {
        a: String
    }
    """)
    diff = Schema(no_directive, one_directive).diff()
    assert diff and len(diff) == 1
    assert diff[0].message == "Directive `@somedir` was added to use on `FIELD_DEFINITION`"
    assert diff[0].path == '@somedir'
    assert diff[0].criticality == Criticality.safe()

    diff = Schema(one_directive, no_directive).diff()
    assert diff and len(diff) == 1
    assert diff[0].message == "Directive `@somedir` was removed"
    assert diff[0].path == '@somedir'
    assert diff[0].criticality == Criticality.breaking('Removing a directive may break clients that depend on them.')

    two_locations = schema("""
    directive @somedir on FIELD_DEFINITION | QUERY
    type A {
        a: String
    }
    """)
    diff = Schema(no_directive, two_locations).diff()
    assert diff and len(diff) == 1
    assert diff[0].message == "Directive `@somedir` was added to use on `FIELD_DEFINITION | QUERY`"
    assert diff[0].path == '@somedir'
    assert diff[0].criticality == Criticality.safe()
def test_object_type_added_field():
    a = schema("""
    type MyType{
        a: Int
    }
    """)
    b = schema("""
    type MyType{
        a: Int
        b: String!
    }
    """)
    diff = Schema(a, b).diff()
    assert diff and len(diff) == 1
    assert diff[0].message == "Field `b` was added to object type `MyType`"
    assert diff[0].path == 'MyType.b'
    assert diff[0].criticality == Criticality.safe()

    diff = Schema(b, a).diff()
    assert diff and len(diff) == 1
    assert diff[0].message == "Field `b` was removed from object type `MyType`"
    assert diff[0].path == 'MyType.b'
    assert diff[0].criticality == Criticality.breaking(
        'Removing a field is a breaking change. It is preferred to deprecate the field before removing it.'
    )
def test_add_type_to_union():
    two_types = schema("""
    type Query {
        c: Int
    }
    type Result {
        message: String
    }
    type Error {
        message: String
        details: String
    }
        type Unknown {
        message: String
        details: String
        traceback: String
    }

    union Outcome = Result | Error
    """)

    three_types = schema("""
    type Query {
        c: Int
    }
    type Result {
        message: String
    }
    type Error {
        message: String
        details: String
    }
    type Unknown {
        message: String
        details: String
        traceback: String
    }

    union Outcome = Result | Error | Unknown
    """)
    diff = Schema(two_types, three_types).diff()
    assert diff and len(diff) == 1
    assert diff[
        0].message == "Union member `Unknown` was added to `Outcome` Union type"
    assert diff[0].path == 'Outcome'
    assert diff[0].criticality == Criticality.dangerous(
        'Adding a possible type to Unions may break existing clients '
        'that were not programming defensively against a new possible type.')

    diff = Schema(three_types, two_types).diff()
    assert diff and len(diff) == 1
    assert diff[
        0].message == "Union member `Unknown` was removed from `Outcome` Union type"
    assert diff[0].path == 'Outcome'
    assert diff[0].criticality == Criticality.breaking(
        'Removing a union member from a union can break queries that use this union member in a fragment spread'
    )
Ejemplo n.º 7
0
def test_directive_argument_description_changed():
    no_desc = schema("""
    directive @limit(
        number: Int
    ) on FIELD_DEFINITION

    type A {
        a: String
    }
    """)
    a_desc = schema("""
    directive @limit(
        "number limit"
        number: Int
    ) on FIELD_DEFINITION

    type A {
        a: String
    }
    """)
    other_desc = schema("""
    directive @limit(
        "field limit"
        number: Int
    ) on FIELD_DEFINITION

    type A {
        a: String
    }
    """)

    diff = Schema(no_desc, a_desc).diff()
    assert diff and len(diff) == 1
    expected_message = (
        "Description for argument `number` on `@limit` directive changed from `None` to `number limit`"
    )
    assert diff[0].message == expected_message
    assert diff[0].path == '@limit'
    assert diff[0].criticality == Criticality.safe()

    diff = Schema(a_desc, other_desc).diff()
    assert diff and len(diff) == 1
    expected_message = (
        "Description for argument `number` on `@limit` directive changed from `number limit` to `field limit`"
    )
    assert diff[0].message == expected_message
    assert diff[0].path == '@limit'
    assert diff[0].criticality == Criticality.safe()

    diff = Schema(other_desc, no_desc).diff()
    assert diff and len(diff) == 1
    expected_message = (
        "Description for argument `number` on `@limit` directive changed from `field limit` to `None`"
    )
    assert diff[0].message == expected_message
    assert diff[0].path == '@limit'
    assert diff[0].criticality == Criticality.safe()
Ejemplo n.º 8
0
def test_schema_query_root_changed():
    old_schema = schema("""
    schema {
        query: Query
    }

    type Query {
        field: String!
    }
    """)
    new_schema = schema("""
    schema {
        query: ChangedQuery
    }

    type ChangedQuery {
        field: String!
    }
    """)
    diff = Schema(old_schema, new_schema).diff()
    print(diff)
    assert diff and len(diff) == 3
    assert {x.message
            for x in diff} == {
                'Type `ChangedQuery` was added', 'Type `Query` was removed',
                'Schema query root has changed from `Query` to `ChangedQuery`'
            }
Ejemplo n.º 9
0
def test_deprecated_reason_changed():
    a = schema("""
    type Query {
        a: Int
    }
    enum Letters {
        A
        B @deprecated(reason: "a reason")
    }
    """)
    b = schema("""
    type Query {
        a: Int
    }
    enum Letters {
        A
        B @deprecated(reason: "a new reason")
    }
    """)
    diff = Schema(a, b).diff()
    assert len(diff) == 1
    assert diff[0].message == (
        "Deprecation reason for enum value `B` changed from `a reason` to `a new reason`"
    )
    assert diff[0].path == 'Letters.B'
    assert diff[0].criticality == Criticality.safe(
        "A deprecated field can still be used by clients and will give them time to adapt their queries"
    )
Ejemplo n.º 10
0
def test_description_changed():
    a = schema('''
    type Query {
        a: Int
    }
    enum Letters {
        """My description"""
        A
    }
    ''')
    b = schema('''
    type Query {
        a: Int
    }
    enum Letters {
        """My new description"""
        A
    }
    ''')
    diff = Schema(a, b).diff()
    assert len(diff) == 1
    assert diff[0].message == (
        "Description for enum value `A` changed from `My description` to `My new description`"
    )
    assert diff[0].path == 'Letters.A'
    assert diff[0].criticality == Criticality.safe()
Ejemplo n.º 11
0
def test_enums_added():
    a = schema("""
    type Query {
        a: String
    }
    enum Letters {
        A
        B
    }
    """)
    b = schema("""
    type Query {
        a: String
    }
    enum Letters {
        A
        B
        C
        D
    }
    """)
    diff = Schema(a, b).diff()
    assert len(diff) == 2
    expected_diff = {
        "Enum value `C` was added to `Letters` enum",
        "Enum value `D` was added to `Letters` enum",
    }
    expected_paths = {'Letters.C', 'Letters.D'}
    for change in diff:
        assert change.message in expected_diff
        assert change.path in expected_paths
        assert change.criticality == Criticality.dangerous(
            "Adding an enum value may break existing clients that "
            "were not programming defensively against an added case when querying an enum."
        )
Ejemplo n.º 12
0
def test_schema_removed_type():
    old_schema = schema("""
    schema {
        query: Query
    }

    type Query {
        field: String!
    }

    type ToBeRemovedType {
        added: Int
    }

    """)
    new_schema = schema("""
    schema {
        query: Query
    }

    type Query {
        field: String!
    }
    """)
    diff = Schema(old_schema, new_schema).diff()
    assert diff and len(diff) == 1
    # Type Int was also removed but it is ignored because it's a primitive.
    assert diff[0].message == "Type `ToBeRemovedType` was removed"
    assert diff[0].criticality == Criticality.breaking(
        'Removing a type is a breaking change. It is preferred to '
        'deprecate and remove all references to this type first.')
Ejemplo n.º 13
0
def test_schema_added_type():
    old_schema = schema("""
    schema {
        query: Query
    }

    type Query {
        field: String!
    }
    """)
    new_schema = schema("""
    schema {
        query: Query
    }

    type Query {
        field: String!
    }

    type AddedType {
        added: Int
    }
    """)
    diff = Schema(old_schema, new_schema).diff()
    assert diff and len(diff) == 1
    # Type Int was also added but its ignored because its a primitive.
    assert diff[0].message == "Type `AddedType` was added"
    assert diff[0].criticality == Criticality.safe()
Ejemplo n.º 14
0
def test_schema_mutation_root_changed():
    old_schema = schema("""
    schema {
        query: Query
    }

    type Query {
        field: String!
    }
    """)
    new_schema = schema("""
    schema {
        query: Query
        mutation: Mutation
    }

    type Query {
        field: String!
    }

    type Mutation {
        my_mutation: Int
    }
    """)
    diff = Schema(old_schema, new_schema).diff()
    print(diff)
    assert diff and len(diff) == 2
    assert {x.message
            for x in diff} == {
                'Type `Mutation` was added',
                'Schema mutation root has changed from `None` to `Mutation`'
            }
Ejemplo n.º 15
0
def test_directive_argument_changes():
    name_arg = schema("""
    directive @somedir(name: String) on FIELD_DEFINITION
    type A {
        a: String
    }
    """)
    id_arg = schema("""
    directive @somedir(id: ID) on FIELD_DEFINITION
    type A {
        a: String
    }
    """)
    diff = Schema(name_arg, id_arg).diff()
    assert diff and len(diff) == 2
    expected_message = (
        'Removed argument `name: String` from `@somedir` directive'
        "Added argument `id: ID` to `@somedir` directive"
    )
    for change in diff:
        assert change.message in expected_message
        assert change.path == '@somedir'
        assert change.criticality == Criticality.breaking(
            'Removing a directive argument will break existing usages of the argument'
        ) if 'Removed' in change.message else Criticality.safe()
Ejemplo n.º 16
0
def test_enum_value_removed():
    a = schema("""
    type Query {
        a: String
    }
    enum Letters {
        A
        B
    }
    """)
    b = schema("""
    type Query {
        a: String
    }
    enum Letters {
        A
    }
    """)
    diff = Schema(a, b).diff()
    assert len(diff) == 1
    change = diff[0]
    assert change.message == "Enum value `B` was removed from `Letters` enum"
    assert change.path == 'Letters'
    assert change.criticality == Criticality.breaking(
        'Removing an enum value will break existing queries that use this enum value'
    )
def test_print_json_shows_difference(capsys):
    old_schema = SchemaLoader.from_file(TESTS_DATA / 'simple_schema.gql')
    new_schema = SchemaLoader.from_file(TESTS_DATA /
                                        'simple_schema_breaking_changes.gql')

    diff = Schema(old_schema, new_schema).diff()
    assert len(diff) == 1
    ret = print_json(diff)
    assert ret is None
    json_output = capsys.readouterr().out
    output_as_dict = json.loads(json_output)
    change_message = "Field `a` was removed from object type `Query`"
    assert output_as_dict == [{
        "message": change_message,
        "path": "Query.a",
        "is_safe_change": False,
        "criticality": {
            "level":
            "BREAKING",
            "reason":
            "Removing a field is a breaking change. "
            "It is preferred to deprecate the field before removing it."
        },
        "checksum": '5fba3d6ffc43c6769c6959ce5cb9b1c8'
    }]
Ejemplo n.º 18
0
def test_deprecated_enum_value():
    a = schema("""
    type Query {
        a: Int
    }
    enum Letters {
        A
        B
    }
    """)
    b = schema("""
    type Query {
        a: Int
    }
    enum Letters {
        A
        B @deprecated(reason: "Changed the alphabet")
    }
    """)
    diff = Schema(a, b).diff()
    assert len(diff) == 1
    assert diff[
        0].message == "Enum value `B` was deprecated with reason `Changed the alphabet`"
    assert diff[0].path == 'Letters.B'
    assert diff[0].criticality == Criticality.safe(
        "A deprecated field can still be used by clients and will give them time to adapt their queries"
    )
Ejemplo n.º 19
0
def test_directive_default_value_changed():
    default_100 = schema("""
    directive @limit(number: Int=100) on FIELD_DEFINITION
    type A {
        a: String
    }
    """)
    default_0 = schema("""
    directive @limit(number: Int=0) on FIELD_DEFINITION
    type A {
        a: String
    }
    """)

    diff = Schema(default_100, default_0).diff()
    assert diff and len(diff) == 1
    expected_message = (
        'Default value for argument `number` on `@limit` directive changed from `100` to `0`'
    )
    assert diff[0].message == expected_message
    assert diff[0].path == '@limit'
    assert diff[0].criticality == Criticality.dangerous(
        'Changing the default value for an argument may change '
        'the runtime behaviour of a field if it was never provided.'
    )
Ejemplo n.º 20
0
def test_schema_query_fields_type_has_changes():
    old_schema = schema("""
    schema {
        query: Query
    }

    type Query {
        field: String!
    }
    """)
    new_schema = schema("""
    schema {
        query: Query
    }

    type Query {
        field: Int!
    }
    """)
    diff = Schema(old_schema, new_schema).diff()
    assert diff and len(diff) == 1
    assert diff[
        0].message == "`Query.field` type changed from `String!` to `Int!`"
    assert diff[0].criticality == Criticality.breaking(
        'Changing a field type will break queries that assume its type')
Ejemplo n.º 21
0
def test_directive_description_changed():
    no_desc = schema("""
    directive @my_directive on FIELD
    type A {
        a: String
    }
    """)
    with_desc = schema('''
    """directive desc"""
    directive @my_directive on FIELD
    type A {
        a: String
    }
    ''')
    new_desc = schema('''
    """new description"""
    directive @my_directive on FIELD
    type A {
        a: String
    }
    ''')
    diff = Schema(no_desc, with_desc).diff()
    assert diff and len(diff) == 1
    expected_message = (
        'Description for directive `@my_directive` changed from `None` to `directive desc`'
    )
    assert diff[0].message == expected_message
    assert diff[0].path == '@my_directive'
    assert diff[0].criticality == Criticality.safe()

    diff = Schema(with_desc, new_desc).diff()
    assert diff and len(diff) == 1
    expected_message = (
        'Description for directive `@my_directive` changed from `directive desc` to `new description`'
    )
    assert diff[0].message == expected_message
    assert diff[0].path == '@my_directive'
    assert diff[0].criticality == Criticality.safe()

    diff = Schema(with_desc, no_desc).diff()
    assert diff and len(diff) == 1
    expected_message = (
        'Description for directive `@my_directive` changed from `directive desc` to `None`'
    )
    assert diff[0].message == expected_message
    assert diff[0].path == '@my_directive'
    assert diff[0].criticality == Criticality.safe()
Ejemplo n.º 22
0
def diff_from_file(schema_file: str, other_schema_file: str):
    """Compare two graphql schema files highlighting dangerous and breaking changes.

    Returns:
        changes (List[Change]): List of differences between both schemas with details about each change
    """
    first = SchemaLoader.from_file(schema_file)
    second = SchemaLoader.from_file(other_schema_file)
    return Schema(first, second).diff()
Ejemplo n.º 23
0
def test_type_implements_new_interface():
    a = schema("""
    interface InterfaceA {
        a: Int
    }
    type MyType implements InterfaceA {
        a: Int
    }    
    """)
    b = schema("""
    interface InterfaceA {
        a: Int
    }
    interface InterfaceB {
        b: String
    }
    type MyType implements InterfaceA & InterfaceB {
        a: Int
        b: String
    }    
    """)
    diff = Schema(a, b).diff()
    assert diff and len(diff) == 3
    expected_diff = {
        "Field `b` was added to object type `MyType`",
        "Type `InterfaceB` was added",
        "`MyType` implements new interface `InterfaceB`"
    }
    expected_paths = {'InterfaceB', 'MyType', 'MyType.b'}
    for change in diff:
        assert change.message in expected_diff
        assert change.path in expected_paths

    diff = Schema(b, a).diff()
    assert diff and len(diff) == 3
    expected_diff = {
        "Field `b` was removed from object type `MyType`",
        "Type `InterfaceB` was removed",
        "`MyType` no longer implements interface `InterfaceB`"
    }
    expected_paths = {'InterfaceB', 'MyType', 'MyType.b'}
    for change in diff:
        assert change.message in expected_diff
        assert change.path in expected_paths
Ejemplo n.º 24
0
def test_schema_no_diff():
    a_schema = schema("""
    schema {
        query: Query
    }
    type Query {
        a: String!
    }
    """)
    diff = Schema(a_schema, a_schema).diff()
    assert diff == []
def test_print_diff_shows_difference(capsys):
    old_schema = SchemaLoader.from_file(TESTS_DATA / 'simple_schema.gql')
    new_schema = SchemaLoader.from_file(TESTS_DATA /
                                        'simple_schema_breaking_changes.gql')

    diff = Schema(old_schema, new_schema).diff()
    assert len(diff) == 1
    ret = print_diff(diff)
    assert ret is None
    assert capsys.readouterr().out == (
        '❌ Field `a` was removed from object type `Query`\n')
Ejemplo n.º 26
0
def diff(old_schema: Union[SDL, GQLSchema],
         new_schema: Union[SDL, GQLSchema]) -> List[Change]:
    """Compare two graphql schemas highlighting dangerous and breaking changes.

    Returns:
        changes (List[Change]): List of differences between both schemas with details about each change
    """
    first = SchemaLoader.from_sdl(
        old_schema) if not is_schema(old_schema) else old_schema
    second = SchemaLoader.from_sdl(
        new_schema) if not is_schema(new_schema) else new_schema
    return Schema(first, second).diff()
def test_field_type_change_is_safe():
    non_nullable_schema = schema("""
    type Query {
        a: Int!
    }
    """)
    nullable_schema = schema("""
    type Query {
        a: Int
    }
    """)
    # Dropping the non-null constraint is a breaking change because clients may have assumed it could never be null
    diff = Schema(non_nullable_schema, nullable_schema).diff()
    assert len(diff) == 1
    assert diff[0].criticality == Criticality.breaking(
        'Changing a field type will break queries that assume its type')

    # But if it was already nullable, they already had to handle that case.
    diff = Schema(nullable_schema, non_nullable_schema).diff()
    assert len(diff) == 1
    assert diff[0].criticality == Criticality.safe()
def test_compare_from_schema_string_sdl():
    old_schema = SchemaLoader.from_file(TESTS_DATA / 'old_schema.gql')
    new_schema = SchemaLoader.from_file(TESTS_DATA / 'new_schema.gql')

    diff = Schema(old_schema, new_schema).diff()
    assert len(diff) == 38

    expected_changes = {
         'Argument `arg: Int` added to `CType.a`',
         "Default value for argument `anotherArg` on `@yolo` directive changed from `Undefined` to `'Test'`",
         'Default value for argument `arg` on field `CType.d` changed from `Undefined` to `10`',
         'Default value for argument `arg` on field `WithArguments.b` changed from `1` to `2`',
         "Default value for input field `AInput.a` changed from `'1'` to `1`",
         'Deprecation reason for enum value `F` changed from `Old` to `New`',
         'Deprecation reason on field `CType.a` changed from `whynot` to `cuz`',
         'Description for Input field `AInput.a` changed from `a` to `None`',
         'Description for argument `a` on field `WithArguments.a` changed from `Meh` to `None`',
         'Description for argument `someArg` on `@yolo` directive changed from `Included when true.` to `None`',
         'Description for directive `@yolo` changed from `Old` to `None`',
         'Description for type `Query` changed from `The Query Root of this schema` to `None`',
         'Directive `@willBeRemoved` was removed',
         'Directive `@yolo2` was added to use on `FIELD`',
         (
            'Directive locations of `@yolo` changed from `FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT` '
            'to `FIELD | FIELD_DEFINITION`'
         ),
         'Enum value `C` was removed from `Options` enum',
         'Enum value `D` was added to `Options` enum',
         'Enum value `E` was deprecated with reason `No longer supported`',
         'Field `anotherInterfaceField` was removed from interface `AnotherInterface`',
         'Field `b` of type `Int` was added to interface `AnotherInterface`',
         'Field `b` was added to object type `CType`',
         'Field `c` was removed from object type `CType`',
         'Input Field `b` removed from input type `AInput`',
         'Input Field `c: String!` was added to input type `AInput`',
         'Removed argument `anArg` from `Query.a`',
         'Removed argument `willBeRemoved: Boolean!` from `@yolo` directive',
         'Type `DType` was added',
         'Type `WillBeRemoved` was removed',
         'Type for argument `b` on field `WithArguments.a` changed from `String` to `String!`',
         'Type for argument `someArg` on `@yolo` directive changed from `Boolean!` to `String!`',
         'Union member `BType` was removed from `MyUnion` Union type',
         'Union member `DType` was added to `MyUnion` Union type',
         '`AInput.a` type changed from `String` to `Int`',
         '`BType` kind changed from `OBJECT` to `INPUT OBJECT`',
         '`CType` implements new interface `AnInterface`',
         '`Query.a` description changed from `Just a simple string` to `None`',
         '`Query.b` type changed from `BType` to `Int!`',
         '`WithInterfaces` no longer implements interface `AnotherInterface`'
    }
    messages = [change.message for change in diff]
    for message in messages:
        assert message in expected_changes
def test_input_field_type_nullability_change_on_lists_of_the_same_underlying_types(
):
    a = schema("""
    input Params {
        arg: [ID!]!
    }
    """)
    b = schema("""
    input Params {
        arg: [ID!]
    }
    """)
    c = schema("""
    input Params {
        arg: [ID]
    }
    """)
    d = schema("""
    input Params {
        arg: ID
    }
    """)
    diff = Schema(a, b).diff()
    assert diff and len(diff) == 1
    assert diff[
        0].message == "`Params.arg` type changed from `[ID!]!` to `[ID!]`"
    assert diff[0].path == 'Params.arg'
    assert diff[0].criticality == Criticality.safe(
    )  # Because dropping the non-null constraint will not break anything

    diff = Schema(a, c).diff()
    assert diff[
        0].message == "`Params.arg` type changed from `[ID!]!` to `[ID]`"
    assert diff[0].path == 'Params.arg'
    assert diff[0].criticality == Criticality.breaking(ERROR)

    diff = Schema(a, d).diff()
    assert diff[0].message == "`Params.arg` type changed from `[ID!]!` to `ID`"
    assert diff[0].path == 'Params.arg'
    assert diff[0].criticality == Criticality.breaking(ERROR)
def test_no_field_diff():
    a = schema("""
    type Query {
        b: Int!
    }
    """)
    b = schema("""
    type Query {
        b: Int!
    }
    """)
    diff = Schema(a, b).diff()
    assert not diff