Considerably expand test coverage

This commit is contained in:
Reid 'arrdem' McKenzie 2021-05-14 23:25:07 -06:00
parent 0246be6a14
commit b49582a5dc
2 changed files with 134 additions and 17 deletions

View file

@ -8,12 +8,110 @@ import pytest
@pytest.mark.parametrize('schema, obj', [ @pytest.mark.parametrize('schema, obj', [
({"type": "number"}, "---\n1.0"), ({"type": "number"},
({"type": "integer"}, "---\n3"), "---\n1.0"),
({"type": "string"}, "---\nfoo bar baz"), ({"type": "integer"},
({"type": "string", "maxLength": 15}, "---\nfoo bar baz"), "---\n3"),
({"type": "string", "minLength": 10}, "---\nfoo bar baz"), ({"type": "string"},
({"type": "string", "pattern": "^foo.*"}, "---\nfoo bar baz"), "---\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): def test_lint_document_ok(schema, obj):
assert not list(lint_buffer(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

View file

@ -45,7 +45,10 @@ class YamlLinter(object):
def dereference(self, schema): def dereference(self, schema):
"""Dereference a {"$ref": ""} form.""" """Dereference a {"$ref": ""} form."""
if ref := schema.get("$ref"): if schema in [True, False]:
return schema
elif ref := schema.get("$ref"):
assert ref.startswith("#/") assert ref.startswith("#/")
path = ref.lstrip("#/").split("/") path = ref.lstrip("#/").split("/")
schema = self._schema schema = self._schema
@ -85,10 +88,10 @@ class YamlLinter(object):
for k, v in node.value: for k, v in node.value:
if k.value in properties: 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: elif additional_type:
yield from self.lint_document(additional_type, v) yield from self.lint_document(v, additional_type)
else: else:
yield LintRecord( yield LintRecord(
@ -116,7 +119,7 @@ class YamlLinter(object):
subschema = schema.get("items") subschema = schema.get("items")
if subschema: if subschema:
for item in node.value: 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]: def lint_scalar(self, schema, node: Node) -> t.Iterable[str]:
"""FIXME. """FIXME.
@ -204,7 +207,7 @@ class YamlLinter(object):
def _lint_num_range(self, schema, node: Node, value) -> t.Iterable[str]: def _lint_num_range(self, schema, node: Node, value) -> t.Iterable[str]:
""""FIXME.""" """"FIXME."""
if base := schema.get("multipleOf"): if (base := schema.get("multipleOf")) is not None:
if value % base != 0: if value % base != 0:
yield LintRecord( yield LintRecord(
LintLevel.MISSMATCH, LintLevel.MISSMATCH,
@ -213,7 +216,7 @@ class YamlLinter(object):
f"Expected a multiple of {base}, got {value}" f"Expected a multiple of {base}, got {value}"
) )
if max := schema.get("exclusiveMaximum"): if (max := schema.get("exclusiveMaximum")) is not None:
if value >= max: if value >= max:
yield LintRecord( yield LintRecord(
LintLevel.MISSMATCH, LintLevel.MISSMATCH,
@ -222,7 +225,7 @@ class YamlLinter(object):
f"Expected a value less than {max}, got {value}" 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: if value > max:
yield LintRecord( yield LintRecord(
LintLevel.MISSMATCH, LintLevel.MISSMATCH,
@ -231,7 +234,7 @@ class YamlLinter(object):
f"Expected a value less than or equal to {max}, got {value}" 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: if value <= min:
yield LintRecord( yield LintRecord(
LintLevel.MISSMATCH, LintLevel.MISSMATCH,
@ -240,7 +243,7 @@ class YamlLinter(object):
f"Expected a value greater than {min}, got {value}" 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: if value < min:
yield LintRecord( yield LintRecord(
LintLevel.MISSMATCH, LintLevel.MISSMATCH,
@ -260,20 +263,36 @@ class YamlLinter(object):
schema = schema or self._schema # Fixing up the schema source schema = schema or self._schema # Fixing up the schema source
schema = self.dereference(schema) # And dereference it if needed schema = self.dereference(schema) # And dereference it if needed
# Special schemas
# These are schemas that accept everything.
if schema == True or schema == {}: if schema == True or schema == {}:
yield from [] 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): elif isinstance(node, MappingNode):
yield from self.lint_mapping(schema, node) yield from self.lint_mapping(schema, node)
elif isinstance(node, SequenceNode): elif isinstance(node, SequenceNode):
yield from self.lint_sequence(schema, node) yield from self.lint_sequence(schema, node)
elif isinstance(node, ScalarNode): elif isinstance(node, ScalarNode):
yield from self.lint_scalar(schema, node) yield from self.lint_scalar(schema, node)
else: else:
yield from [] raise RuntimeError(f"Unsupported PyYAML node {type(node)}")
def lint_node(schema, node, cls=YamlLinter): 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)) print(repr(node))
linter = cls(schema) linter = cls(schema)