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' }]
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()
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')
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_json_dump_changes(): old = SchemaLoad.from_file(TESTS_DATA / 'simple_schema.gql') new = SchemaLoad.from_file(TESTS_DATA / 'simple_schema_dangerous_and_breaking.gql') changes = Schema(old, new).diff() expected_changes = [ { 'criticality': { 'level': 'NON_BREAKING', 'reason': "This change won't break any preexisting query" }, 'is_safe_change': True, 'message': 'Field `c` was added to object type `Query`', 'path': 'Query.c', 'checksum': '1e3b776bda2dd8b11804e7341bb8b2d1', }, { 'criticality': { 'level': 'BREAKING', 'reason': ( 'Removing a field is a breaking change. It is preferred to deprecate the field before removing it.' ) }, 'is_safe_change': False, 'message': 'Field `a` was removed from object type `Query`', 'path': 'Query.a', 'checksum': '5fba3d6ffc43c6769c6959ce5cb9b1c8', }, { 'criticality': { 'level': 'DANGEROUS', 'reason': ( 'Changing the default value for an argument may ' 'change the runtime behaviour of a field if it was never provided.' ) }, 'is_safe_change': False, 'message': 'Default value for argument `x` on field `Field.calculus` changed from `0` to `1`', 'path': 'Field.calculus', 'checksum': '8890b911d44b2ead0dceeae066d98617', } ] changes_result = changes_to_dict(changes) by_path = itemgetter('path') assert sorted(changes_result, key=by_path) == sorted(expected_changes, key=by_path) # Assert the json dump has a 32-char checksum and the changes attributes as expected json_changes = json.loads(json_dump_changes(changes)) assert all(len(change['checksum']) == 32 for change in json_changes) assert sorted(json_changes, key=by_path) == sorted(expected_changes, key=by_path)
def test_load_invalid_schema(): with pytest.raises(TypeError, match="Unknown type 'InvalidType'"): SchemaLoader.from_file(TESTS_DATA / 'invalid_schema.gql')
def test_load_from_file(): schema = SchemaLoader.from_file(TESTS_DATA / 'simple_schema.gql') assert is_schema(schema) assert len(schema.query_type.fields) == 2