From b49582a5dc1c3cc661d93da97b7c580e33dcaf3e Mon Sep 17 00:00:00 2001 From: Reid 'arrdem' McKenzie Date: Fri, 14 May 2021 23:25:07 -0600 Subject: [PATCH] Considerably expand test coverage --- projects/yamlschema/test_yamlschema.py | 110 +++++++++++++++++++++++-- projects/yamlschema/yamlschema.py | 41 ++++++--- 2 files changed, 134 insertions(+), 17 deletions(-) diff --git a/projects/yamlschema/test_yamlschema.py b/projects/yamlschema/test_yamlschema.py index c39b545..f67ce2e 100644 --- a/projects/yamlschema/test_yamlschema.py +++ b/projects/yamlschema/test_yamlschema.py @@ -8,12 +8,110 @@ import pytest @pytest.mark.parametrize('schema, obj', [ - ({"type": "number"}, "---\n1.0"), - ({"type": "integer"}, "---\n3"), - ({"type": "string"}, "---\nfoo bar baz"), - ({"type": "string", "maxLength": 15}, "---\nfoo bar baz"), - ({"type": "string", "minLength": 10}, "---\nfoo bar baz"), - ({"type": "string", "pattern": "^foo.*"}, "---\nfoo bar baz"), + ({"type": "number"}, + "---\n1.0"), + ({"type": "integer"}, + "---\n3"), + ({"type": "string"}, + "---\nfoo bar baz"), + ({"type": "string", + "maxLength": 15}, + "---\nfoo bar baz"), + ({"type": "string", + "minLength": 10}, + "---\nfoo bar baz"), + ({"type": "string", + "pattern": "^foo.*"}, + "---\nfoo bar baz"), + ({"type": "object", + "additionalProperties": True}, + "---\nfoo: bar\nbaz: qux"), + ({"type": "object", + "properties": {"foo": {"type": "string"}}}, + "---\nfoo: bar\nbaz: qux"), + ({"type": "object", + "properties": {"foo": {"type": "string"}}, + "additionalProperties": False}, + "---\nfoo: bar"), + ({"type": "object", + "properties": {"foo": {"type": "object"}}}, + "---\nfoo: {}"), + ({"type": "object", + "properties": {"foo": { + "type": "array", + "items": {"type": "object"}}}}, + "---\nfoo: [{}, {}, {foo: bar}]"), ]) def test_lint_document_ok(schema, obj): assert not list(lint_buffer(schema, obj)) + + +@pytest.mark.parametrize('msg, schema, obj', [ + # Numerics + ("Floats are not ints", + {"type": "integer"}, + "---\n1.0"), + ("Ints are not floats", + {"type": "number"}, + "---\n1"), + + # Numerics - range limits. Integer edition + ("1 is the limit of the range", + {"type": "integer", + "exclusiveMaximum": 1}, + "---\n1"), + ("1 is the limit of the range", + {"type": "integer", + "exclusiveMinimum": 1}, + "---\n1"), + ("1 is out of the range", + {"type": "integer", + "minimum": 2}, + "---\n1"), + ("1 is out of the range", + {"type": "integer", + "maximum": 0}, + "---\n1"), + ("1 is out of the range", + {"type": "integer", + "exclusiveMinimum": 1}, + "---\n1"), + + # Numerics - range limits. Number/Float edition + ("1 is the limit of the range", + {"type": "number", + "exclusiveMaximum": 1}, + "---\n1.0"), + ("1 is the limit of the range", + {"type": "number", + "exclusiveMinimum": 1}, + "---\n1.0"), + ("1 is out of the range", + {"type": "number", + "minimum": 2}, + "---\n1.0"), + ("1 is out of the range", + {"type": "number", + "maximum": 0}, + "---\n1.0"), + ("1 is out of the range", + {"type": "number", + "exclusiveMinimum": 1}, + "---\n1.0"), + + # String shit + ("String too short", + {"type": "string", "minLength": 1}, + "---\n''"), + ("String too long", + {"type": "string", "maxLength": 1}, + "---\nfoo"), + ("String does not match pattern", + {"type": "string", "pattern": "bar"}, + "---\nfoo"), + ("String does not fully match pattern", + {"type": "string", "pattern": "foo"}, + "---\nfooooooooo"), +]) +def test_lint_document_fails(msg, schema, obj): + assert list(lint_buffer(schema, obj)), msg diff --git a/projects/yamlschema/yamlschema.py b/projects/yamlschema/yamlschema.py index 41b1f1a..6351f11 100644 --- a/projects/yamlschema/yamlschema.py +++ b/projects/yamlschema/yamlschema.py @@ -45,7 +45,10 @@ class YamlLinter(object): def dereference(self, schema): """Dereference a {"$ref": ""} form.""" - if ref := schema.get("$ref"): + if schema in [True, False]: + return schema + + elif ref := schema.get("$ref"): assert ref.startswith("#/") path = ref.lstrip("#/").split("/") schema = self._schema @@ -85,10 +88,10 @@ class YamlLinter(object): for k, v in node.value: if k.value in properties: - yield from self.lint_document(properties.get(k.value), v) + yield from self.lint_document(v, properties.get(k.value)) elif additional_type: - yield from self.lint_document(additional_type, v) + yield from self.lint_document(v, additional_type) else: yield LintRecord( @@ -116,7 +119,7 @@ class YamlLinter(object): subschema = schema.get("items") if subschema: for item in node.value: - yield from self.lint_document(subschema, item) + yield from self.lint_document(item, subschema) def lint_scalar(self, schema, node: Node) -> t.Iterable[str]: """FIXME. @@ -204,7 +207,7 @@ class YamlLinter(object): def _lint_num_range(self, schema, node: Node, value) -> t.Iterable[str]: """"FIXME.""" - if base := schema.get("multipleOf"): + if (base := schema.get("multipleOf")) is not None: if value % base != 0: yield LintRecord( LintLevel.MISSMATCH, @@ -213,7 +216,7 @@ class YamlLinter(object): f"Expected a multiple of {base}, got {value}" ) - if max := schema.get("exclusiveMaximum"): + if (max := schema.get("exclusiveMaximum")) is not None: if value >= max: yield LintRecord( LintLevel.MISSMATCH, @@ -222,7 +225,7 @@ class YamlLinter(object): f"Expected a value less than {max}, got {value}" ) - if max := schema.get("maximum"): + if (max := schema.get("maximum")) is not None: if value > max: yield LintRecord( LintLevel.MISSMATCH, @@ -231,7 +234,7 @@ class YamlLinter(object): f"Expected a value less than or equal to {max}, got {value}" ) - if min := schema.get("exclusiveMinimum"): + if (min := schema.get("exclusiveMinimum")) is not None: if value <= min: yield LintRecord( LintLevel.MISSMATCH, @@ -240,7 +243,7 @@ class YamlLinter(object): f"Expected a value greater than {min}, got {value}" ) - if min := schema.get("minimum"): + if (min := schema.get("minimum")) is not None: if value < min: yield LintRecord( LintLevel.MISSMATCH, @@ -260,20 +263,36 @@ class YamlLinter(object): schema = schema or self._schema # Fixing up the schema source schema = self.dereference(schema) # And dereference it if needed + # Special schemas + # These are schemas that accept everything. if schema == True or schema == {}: yield from [] + + # This is the schema that rejects everything. + elif schema == False: + yield LintRecord( + LintLevel.UNEXPECTED, + node, + schema, + "Received an unexpected value" + ) + + # Walking the PyYAML node hierarchy elif isinstance(node, MappingNode): yield from self.lint_mapping(schema, node) + elif isinstance(node, SequenceNode): yield from self.lint_sequence(schema, node) + elif isinstance(node, ScalarNode): yield from self.lint_scalar(schema, node) + else: - yield from [] + raise RuntimeError(f"Unsupported PyYAML node {type(node)}") def lint_node(schema, node, cls=YamlLinter): - """Lint a document using a schema and linter.""" + """Lint a composed PyYAML AST node using a schema and linter.""" print(repr(node)) linter = cls(schema)