diff --git a/pyproject.toml b/pyproject.toml index 9655646..25d1fcd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ test = [ [tool.ruff] line-length = 101 +target-version = "py310" [tool.ruff.lint] exclude = ["tests/*", "docs/*"] diff --git a/raillabel_providerkit/validation/__init__.py b/raillabel_providerkit/validation/__init__.py index fd71faf..d33d953 100644 --- a/raillabel_providerkit/validation/__init__.py +++ b/raillabel_providerkit/validation/__init__.py @@ -2,7 +2,8 @@ # SPDX-License-Identifier: Apache-2.0 """Package for validating raillabel data regarding the format requirements.""" +from .issue import Issue, IssueIdentifiers, IssueType from .validate_onthology.validate_onthology import validate_onthology from .validate_schema import validate_schema -__all__ = ["validate_onthology", "validate_schema"] +__all__ = ["Issue", "IssueIdentifiers", "IssueType", "validate_onthology", "validate_schema"] diff --git a/raillabel_providerkit/validation/issue.py b/raillabel_providerkit/validation/issue.py new file mode 100644 index 0000000..3db171c --- /dev/null +++ b/raillabel_providerkit/validation/issue.py @@ -0,0 +1,33 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from dataclasses import dataclass +from enum import Enum +from uuid import UUID + + +class IssueType(Enum): + """General classification of the issue.""" + + SCHEMA = "SchemaIssue" + EMPTY_FRAMES = "EmptyFramesIssue" + RAIL_SIDE = "RailSide" + + +@dataclass +class IssueIdentifiers: + """Information for locating an issue.""" + + annotation: UUID | None = None + frame: int | None = None + object: UUID | None = None + sensor: str | None = None + + +@dataclass +class Issue: + """An error that was found inside the scene.""" + + type: IssueType + reason: str + identifiers: IssueIdentifiers | list[str | int] diff --git a/raillabel_providerkit/validation/validate.py b/raillabel_providerkit/validation/validate.py index 3b402f8..1b111f4 100644 --- a/raillabel_providerkit/validation/validate.py +++ b/raillabel_providerkit/validation/validate.py @@ -3,10 +3,12 @@ from __future__ import annotations +from raillabel_providerkit.validation import Issue + from . import validate_schema -def validate(scene_dict: dict) -> list[str]: +def validate(scene_dict: dict) -> list[Issue]: """Validate a scene based on the Deutsche Bahn Requirements. Parameters @@ -16,7 +18,7 @@ def validate(scene_dict: dict) -> list[str]: Returns ------- - list[str] + list[Issue] list of all requirement errors in the scene. If an empty list is returned, then there are no errors present and the scene is valid. diff --git a/raillabel_providerkit/validation/validate_empty_frames/validate_empty_frames.py b/raillabel_providerkit/validation/validate_empty_frames/validate_empty_frames.py index c7b70bf..2ba798e 100644 --- a/raillabel_providerkit/validation/validate_empty_frames/validate_empty_frames.py +++ b/raillabel_providerkit/validation/validate_empty_frames/validate_empty_frames.py @@ -5,27 +5,25 @@ import raillabel +from raillabel_providerkit.validation import Issue, IssueIdentifiers, IssueType -def validate_empty_frames(scene: raillabel.Scene) -> list[str]: - """Validate whether all frames of a scene have at least one annotation. - - Parameters - ---------- - scene : raillabel.Scene - Scene, that should be validated. - Returns - ------- - list[str] - list of all empty frame errors in the scene. If an empty list is returned, then there are no - errors present. +def validate_empty_frames(scene: raillabel.Scene) -> list[Issue]: + """Validate whether all frames of a scene have at least one annotation. + If an empty list is returned, then there are no errors present. """ - errors: list[str] = [] + errors = [] for frame_uid, frame in scene.frames.items(): if _is_frame_empty(frame): - errors.append("Frame " + str(frame_uid) + " has no annotations!") + errors.append( + Issue( + type=IssueType.EMPTY_FRAMES, + reason="This frame has no annotations.", + identifiers=IssueIdentifiers(frame=frame_uid), + ) + ) return errors diff --git a/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py b/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py index 0fbca8f..1b03af9 100644 --- a/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py +++ b/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py @@ -14,8 +14,10 @@ IncludeSensorTypeFilter, ) +from raillabel_providerkit.validation import Issue, IssueIdentifiers, IssueType -def validate_rail_side(scene: raillabel.Scene) -> list[str]: + +def validate_rail_side(scene: raillabel.Scene) -> list[Issue]: """Validate whether all tracks have <= one left and right rail, and that they have correct order. Parameters @@ -30,7 +32,7 @@ def validate_rail_side(scene: raillabel.Scene) -> list[str]: errors present. """ - errors: list[str] = [] + errors = [] camera_uids = list(scene.filter([IncludeSensorTypeFilter(["camera"])]).sensors.keys()) @@ -47,11 +49,11 @@ def validate_rail_side(scene: raillabel.Scene) -> list[str]: counts_per_track = _count_rails_per_track_in_frame(frame) for object_uid, (left_count, right_count) in counts_per_track.items(): - context = { - "frame_uid": frame_uid, - "object_uid": object_uid, - "camera_uid": camera_uid, - } + context = IssueIdentifiers( + frame=frame_uid, + sensor=camera_uid, + object=object_uid, + ) count_errors = _check_rail_counts(context, left_count, right_count) exactly_one_left_and_right_rail_exist = count_errors != [] @@ -64,24 +66,28 @@ def validate_rail_side(scene: raillabel.Scene) -> list[str]: if left_rail is None or right_rail is None: continue - errors.extend( - _check_rails_for_swap_or_intersection(left_rail, right_rail, frame_uid) - ) + errors.extend(_check_rails_for_swap_or_intersection(left_rail, right_rail, context)) return errors -def _check_rail_counts(context: dict, left_count: int, right_count: int) -> list[str]: +def _check_rail_counts(context: IssueIdentifiers, left_count: int, right_count: int) -> list[Issue]: errors = [] if left_count > 1: errors.append( - f"In sensor {context['camera_uid']} frame {context['frame_uid']}, the track with" - f" object_uid {context['object_uid']} has more than one ({left_count}) left rail." + Issue( + type=IssueType.RAIL_SIDE, + reason=f"This track has {left_count} left rails.", + identifiers=context, + ) ) if right_count > 1: errors.append( - f"In sensor {context['camera_uid']} frame {context['frame_uid']}, the track with" - f" object_uid {context['object_uid']} has more than one ({right_count}) right rail." + Issue( + type=IssueType.RAIL_SIDE, + reason=f"This track has {right_count} right rails.", + identifiers=context, + ) ) return errors @@ -89,8 +95,8 @@ def _check_rail_counts(context: dict, left_count: int, right_count: int) -> list def _check_rails_for_swap_or_intersection( left_rail: raillabel.format.Poly2d, right_rail: raillabel.format.Poly2d, - frame_uid: str | int = "unknown", -) -> list[str]: + context: IssueIdentifiers, +) -> list[Issue]: if left_rail.object_id != right_rail.object_id: return [] @@ -103,23 +109,22 @@ def _check_rails_for_swap_or_intersection( if left_x is None or right_x is None: return [] - object_uid = left_rail.object_id - sensor_uid = left_rail.sensor_id if left_rail.sensor_id is not None else "unknown" - if left_x >= right_x: return [ - f"In sensor {sensor_uid} frame {frame_uid}, the track with" - f" object_uid {object_uid} has its rails swapped." - f" At the maximum common y={max_common_y}, the left rail has x={left_x}" - f" while the right rail has x={right_x}." + Issue( + type=IssueType.RAIL_SIDE, + reason="The left and right rails of this track are swapped.", + identifiers=context, + ) ] - intersect_interval = _find_intersect_interval(left_rail, right_rail) - if intersect_interval is not None: + if _polylines_are_intersecting(left_rail, right_rail): return [ - f"In sensor {sensor_uid} frame {frame_uid}, the track with" - f" object_uid {object_uid} intersects with itself." - f" The left and right rail intersect in y interval {intersect_interval}." + Issue( + type=IssueType.RAIL_SIDE, + reason="The left and right rails of this track intersect.", + identifiers=context, + ) ] return [] @@ -162,9 +167,9 @@ def _filter_for_poly2ds( ] -def _find_intersect_interval( +def _polylines_are_intersecting( line1: raillabel.format.Poly2d, line2: raillabel.format.Poly2d -) -> tuple[float, float] | None: +) -> bool: """If the two polylines intersect anywhere, return the y interval where they intersect.""" y_values_with_points_in_either_polyline: list[float] = sorted( _get_y_of_all_points_of_poly2d(line1).union(_get_y_of_all_points_of_poly2d(line2)) @@ -181,18 +186,18 @@ def _find_intersect_interval( continue if x1 == x2: - return (y, y) + return True new_order = x1 < x2 order_has_flipped = order is not None and new_order != order and last_y is not None if order_has_flipped: - return (last_y, y) # type: ignore # noqa: PGH003 + return True order = new_order last_y = y - return None + return False def _find_max_y(poly2d: raillabel.format.Poly2d) -> float: diff --git a/raillabel_providerkit/validation/validate_schema/validate_schema.py b/raillabel_providerkit/validation/validate_schema/validate_schema.py index 01a9879..36b1a8e 100644 --- a/raillabel_providerkit/validation/validate_schema/validate_schema.py +++ b/raillabel_providerkit/validation/validate_schema/validate_schema.py @@ -8,8 +8,10 @@ from pydantic_core import ValidationError from raillabel.json_format import JSONScene +from raillabel_providerkit.validation import Issue, IssueType -def validate_schema(data: dict) -> list[str]: + +def validate_schema(data: dict) -> list[Issue]: """Validate a scene for adherence to the raillabel schema.""" try: JSONScene(**data) @@ -19,42 +21,44 @@ def validate_schema(data: dict) -> list[str]: return [] -def _make_errors_readable(errors: ValidationError) -> list[str]: # noqa: C901 +def _make_errors_readable(errors: ValidationError) -> list[Issue]: # noqa: C901 readable_errors = [] for error in json.loads(errors.json()): match error["type"]: case "missing": - readable_errors.append(_convert_missing_error_to_string(error)) + readable_errors.append(_convert_missing_error_to_issue(error)) case "extra_forbidden": - readable_errors.append(_convert_unexpected_field_error_to_string(error)) + readable_errors.append(_convert_unexpected_field_error_to_issue(error)) case "literal_error": - readable_errors.append(_convert_literal_error_to_string(error)) + readable_errors.append(_convert_literal_error_to_issue(error)) case "bool_type" | "bool_parsing": - readable_errors.append(_convert_false_type_error_to_string(error, "bool")) + readable_errors.append(_convert_false_type_error_to_issue(error, "bool")) case "int_type" | "int_parsing" | "int_from_float": - readable_errors.append(_convert_false_type_error_to_string(error, "int")) + readable_errors.append(_convert_false_type_error_to_issue(error, "int")) case "decimal_type" | "decimal_parsing": - readable_errors.append(_convert_false_type_error_to_string(error, "Decimal")) + readable_errors.append(_convert_false_type_error_to_issue(error, "Decimal")) case "string_type" | "string_parsing": - readable_errors.append(_convert_false_type_error_to_string(error, "str")) + readable_errors.append(_convert_false_type_error_to_issue(error, "str")) case "float_type" | "float_parsing": - readable_errors.append(_convert_false_type_error_to_string(error, "float")) + readable_errors.append(_convert_false_type_error_to_issue(error, "float")) case "uuid_type" | "uuid_parsing": - readable_errors.append(_convert_false_type_error_to_string(error, "UUID")) + readable_errors.append(_convert_false_type_error_to_issue(error, "UUID")) case "too_long": - readable_errors.append(_convert_too_long_error_to_string(error)) + readable_errors.append(_convert_too_long_error_to_issue(error)) case _: - readable_errors.append(str(error)) + readable_errors.append( + Issue(type=IssueType.SCHEMA, identifiers=[], reason=str(error)) + ) return readable_errors @@ -66,32 +70,49 @@ def _build_error_path(loc: list[str]) -> str: return path -def _convert_missing_error_to_string(error: dict) -> str: - return f"{_build_error_path(error['loc'][:-1])}: required field '{error['loc'][-1]}' is missing." +def _convert_missing_error_to_issue(error: dict) -> Issue: + return Issue( + type=IssueType.SCHEMA, + identifiers=error["loc"][:-1], + reason=f"Required field '{error['loc'][-1]}' is missing.", + ) -def _convert_unexpected_field_error_to_string(error: dict) -> str: - return f"{_build_error_path(error['loc'][:-1])}: found unexpected field '{error['loc'][-1]}'." +def _convert_unexpected_field_error_to_issue(error: dict) -> Issue: + return Issue( + type=IssueType.SCHEMA, + identifiers=error["loc"][:-1], + reason=f"Found unexpected field '{error['loc'][-1]}'.", + ) -def _convert_literal_error_to_string(error: dict) -> str: - return ( - f"{_build_error_path(error['loc'])}: value '{error['input']}' does not match allowed values " - f"({error['ctx']['expected']})." +def _convert_literal_error_to_issue(error: dict) -> Issue: + return Issue( + type=IssueType.SCHEMA, + identifiers=error["loc"], + reason=( + f"Value '{error['input']}' does not match allowed values" + f" ({error['ctx']['expected']})." + ), ) -def _convert_false_type_error_to_string(error: dict, target_type: str) -> str: - if "[key]" in error["loc"]: - error_path = _build_error_path(error["loc"][:-2]) - else: - error_path = _build_error_path(error["loc"]) +def _convert_false_type_error_to_issue(error: dict, target_type: str) -> Issue: + error_path = error["loc"][:-2] if "[key]" in error["loc"] else error["loc"] - return f"{error_path}: value '{error['input']}' could not be interpreted " f"as {target_type}." + return Issue( + type=IssueType.SCHEMA, + identifiers=error_path, + reason=f"Value '{error['input']}' could not be interpreted as {target_type}.", + ) -def _convert_too_long_error_to_string(error: dict) -> str: - return ( - f"{_build_error_path(error['loc'])}: should have length of {error['ctx']['actual_length']} " - f"but has length of {error['ctx']['max_length']}." +def _convert_too_long_error_to_issue(error: dict) -> Issue: + return Issue( + type=IssueType.SCHEMA, + identifiers=error["loc"], + reason=( + f"Should have length of {error['ctx']['actual_length']} but has length of " + f"{error['ctx']['max_length']}." + ), ) diff --git a/tests/test_raillabel_providerkit/validation/validate_empty_frame/test_validate_empty_frames.py b/tests/test_raillabel_providerkit/validation/validate_empty_frame/test_validate_empty_frames.py index 7f1eea1..d4c5763 100644 --- a/tests/test_raillabel_providerkit/validation/validate_empty_frame/test_validate_empty_frames.py +++ b/tests/test_raillabel_providerkit/validation/validate_empty_frame/test_validate_empty_frames.py @@ -7,6 +7,7 @@ _is_frame_empty, validate_empty_frames, ) +from raillabel_providerkit.validation import Issue, IssueIdentifiers, IssueType def test_is_frame_empty__true(empty_frame): @@ -59,10 +60,12 @@ def test_validate_empty_frames__error_message_contains_indentifying_info(empty_f 0: empty_frame, } - error_message = validate_empty_frames(scene)[0].lower() - assert "frame" in error_message - assert "0" in error_message - assert "empty" in error_message or "no annotations" in error_message + actual = validate_empty_frames(scene)[0] + assert actual == Issue( + type=IssueType.EMPTY_FRAMES, + reason="This frame has no annotations.", + identifiers=IssueIdentifiers(frame=0), + ) if __name__ == "__main__": diff --git a/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py b/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py index 32e61f2..5e7e369 100644 --- a/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py +++ b/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py @@ -1,6 +1,8 @@ # Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 +from uuid import UUID + import pytest from raillabel.format import Poly2d, Point2d from raillabel.scene_builder import SceneBuilder @@ -9,6 +11,7 @@ validate_rail_side, _count_rails_per_track_in_frame, ) +from raillabel_providerkit.validation import Issue, IssueIdentifiers, IssueType def test_count_rails_per_track_in_frame__empty(empty_frame): @@ -155,6 +158,7 @@ def test_validate_rail_side__no_errors(ignore_uuid): def test_validate_rail_side__rail_sides_switched(ignore_uuid): + SENSOR_ID = "rgb_center" scene = ( SceneBuilder.empty() .add_annotation( @@ -169,7 +173,7 @@ def test_validate_rail_side__rail_sides_switched(ignore_uuid): sensor_id="IGNORE_THIS", ), object_name="track_0001", - sensor_id="rgb_center", + sensor_id=SENSOR_ID, ) .add_annotation( annotation=Poly2d( @@ -183,16 +187,25 @@ def test_validate_rail_side__rail_sides_switched(ignore_uuid): sensor_id="IGNORE_THIS", ), object_name="track_0001", - sensor_id="rgb_center", + sensor_id=SENSOR_ID, ) .result ) actual = validate_rail_side(scene) - assert len(actual) == 1 + assert actual == [ + Issue( + type=IssueType.RAIL_SIDE, + reason="The left and right rails of this track are swapped.", + identifiers=IssueIdentifiers( + frame=1, sensor=SENSOR_ID, object=UUID("5c59aad4-0000-4000-0000-000000000000") + ), + ) + ] def test_validate_rail_side__rail_sides_intersect_at_top(ignore_uuid): + SENSOR_ID = "rgb_center" scene = ( SceneBuilder.empty() .add_annotation( @@ -209,7 +222,7 @@ def test_validate_rail_side__rail_sides_intersect_at_top(ignore_uuid): sensor_id="IGNORE_THIS", ), object_name="track_0001", - sensor_id="rgb_center", + sensor_id=SENSOR_ID, ) .add_annotation( annotation=Poly2d( @@ -225,13 +238,21 @@ def test_validate_rail_side__rail_sides_intersect_at_top(ignore_uuid): sensor_id="IGNORE_THIS", ), object_name="track_0001", - sensor_id="rgb_center", + sensor_id=SENSOR_ID, ) .result ) actual = validate_rail_side(scene) - assert len(actual) == 1 + assert actual == [ + Issue( + type=IssueType.RAIL_SIDE, + reason="The left and right rails of this track intersect.", + identifiers=IssueIdentifiers( + frame=1, sensor=SENSOR_ID, object=UUID("5c59aad4-0000-4000-0000-000000000000") + ), + ) + ] def test_validate_rail_side__rail_sides_correct_with_early_end_of_one_side(ignore_uuid): @@ -276,6 +297,7 @@ def test_validate_rail_side__rail_sides_correct_with_early_end_of_one_side(ignor def test_validate_rail_side__two_left_rails(ignore_uuid): + SENSOR_ID = "rgb_center" scene = ( SceneBuilder.empty() .add_annotation( @@ -290,7 +312,7 @@ def test_validate_rail_side__two_left_rails(ignore_uuid): sensor_id="IGNORE_THIS", ), object_name="track_0001", - sensor_id="rgb_center", + sensor_id=SENSOR_ID, ) .add_annotation( annotation=Poly2d( @@ -304,16 +326,25 @@ def test_validate_rail_side__two_left_rails(ignore_uuid): sensor_id="IGNORE_THIS", ), object_name="track_0001", - sensor_id="rgb_center", + sensor_id=SENSOR_ID, ) .result ) actual = validate_rail_side(scene) - assert len(actual) == 1 + assert actual == [ + Issue( + type=IssueType.RAIL_SIDE, + reason="This track has 2 left rails.", + identifiers=IssueIdentifiers( + frame=1, sensor=SENSOR_ID, object=UUID("5c59aad4-0000-4000-0000-000000000000") + ), + ) + ] def test_validate_rail_side__two_right_rails(ignore_uuid): + SENSOR_ID = "rgb_center" scene = ( SceneBuilder.empty() .add_annotation( @@ -325,10 +356,10 @@ def test_validate_rail_side__two_right_rails(ignore_uuid): closed=False, attributes={"railSide": "rightRail"}, object_id=ignore_uuid, - sensor_id="IGNORE_THIS", + sensor_id=SENSOR_ID, ), object_name="track_0001", - sensor_id="rgb_center", + sensor_id=SENSOR_ID, ) .add_annotation( annotation=Poly2d( @@ -339,16 +370,24 @@ def test_validate_rail_side__two_right_rails(ignore_uuid): closed=False, attributes={"railSide": "rightRail"}, object_id=ignore_uuid, - sensor_id="IGNORE_THIS", + sensor_id=SENSOR_ID, ), object_name="track_0001", - sensor_id="rgb_center", + sensor_id=SENSOR_ID, ) .result ) actual = validate_rail_side(scene) - assert len(actual) == 1 + assert actual == [ + Issue( + type=IssueType.RAIL_SIDE, + reason="This track has 2 right rails.", + identifiers=IssueIdentifiers( + frame=1, sensor=SENSOR_ID, object=UUID("5c59aad4-0000-4000-0000-000000000000") + ), + ) + ] def test_validate_rail_side__two_sensors_with_two_right_rails_each(ignore_uuid): @@ -460,4 +499,4 @@ def test_validate_rail_side__two_sensors_with_one_right_rail_each(ignore_uuid): if __name__ == "__main__": - pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) + pytest.main([__file__, "-vv"]) diff --git a/tests/test_raillabel_providerkit/validation/validate_schema/test_validate_schema.py b/tests/test_raillabel_providerkit/validation/validate_schema/test_validate_schema.py index 632cfc1..5580e7e 100644 --- a/tests/test_raillabel_providerkit/validation/validate_schema/test_validate_schema.py +++ b/tests/test_raillabel_providerkit/validation/validate_schema/test_validate_schema.py @@ -3,7 +3,7 @@ import pytest -from raillabel_providerkit.validation import validate_schema +from raillabel_providerkit.validation import validate_schema, Issue, IssueType def test_no_errors__empty(): @@ -14,35 +14,42 @@ def test_no_errors__empty(): def test_required_field_missing(): - data = {"openlabel": {"metadata": {}}} + data: dict = {"openlabel": {"metadata": {}}} actual = validate_schema(data) - assert len(actual) == 1 - assert "$.openlabel.metadata" in actual[0] - assert "required" in actual[0] - assert "schema_version" in actual[0] - assert "missing" in actual[0] + assert actual == [ + Issue( + type=IssueType.SCHEMA, + identifiers=["openlabel", "metadata"], + reason="Required field 'schema_version' is missing.", + ) + ] def test_unsupported_field(): data = {"openlabel": {"metadata": {"schema_version": "1.0.0"}, "UNSUPPORTED_FIELD": {}}} actual = validate_schema(data) - assert len(actual) == 1 - assert "$.openlabel" in actual[0] - assert "unexpected" in actual[0] - assert "UNSUPPORTED_FIELD" in actual[0] + assert actual == [ + Issue( + type=IssueType.SCHEMA, + identifiers=["openlabel"], + reason="Found unexpected field 'UNSUPPORTED_FIELD'.", + ) + ] def test_unexpected_value(): data = {"openlabel": {"metadata": {"schema_version": "SOMETHING UNSUPPORTED"}}} actual = validate_schema(data) - assert len(actual) == 1 - assert "$.openlabel.metadata.schema_version" in actual[0] - assert "value" in actual[0] - assert "SOMETHING UNSUPPORTED" in actual[0] - assert "'1.0.0'" in actual[0] + assert actual == [ + Issue( + type=IssueType.SCHEMA, + identifiers=["openlabel", "metadata", "schema_version"], + reason="Value 'SOMETHING UNSUPPORTED' does not match allowed values ('1.0.0').", + ) + ] def test_wrong_type_bool(): @@ -72,33 +79,56 @@ def test_wrong_type_bool(): } actual = validate_schema(data) - assert len(actual) == 1 - assert ( - "$.openlabel.frames.1.objects.113c2b35-0965-4c80-a212-08b262e94203.object_data.poly2d.0.closed:" - in actual[0] - ) - assert "bool" in actual[0] - assert "NOT A BOOLEAN" in actual[0] + assert actual == [ + Issue( + type=IssueType.SCHEMA, + identifiers=[ + "openlabel", + "frames", + "1", + "objects", + "113c2b35-0965-4c80-a212-08b262e94203", + "object_data", + "poly2d", + 0, + "closed", + ], + reason="Value 'NOT A BOOLEAN' could not be interpreted as bool.", + ) + ] def test_wrong_type_int(): data = {"openlabel": {"metadata": {"schema_version": "1.0.0"}, "frames": {"NOT AN INT": {}}}} actual = validate_schema(data) - assert len(actual) == 1 - assert "$.openlabel.frames:" in actual[0] - assert "int" in actual[0] - assert "NOT AN INT" in actual[0] + assert actual == [ + Issue( + type=IssueType.SCHEMA, + identifiers=[ + "openlabel", + "frames", + ], + reason="Value 'NOT AN INT' could not be interpreted as int.", + ) + ] def test_wrong_type_string(): data = {"openlabel": {"metadata": {"schema_version": "1.0.0", "comment": False}}} actual = validate_schema(data) - assert len(actual) == 1 - assert "$.openlabel.metadata.comment:" in actual[0] - assert "str" in actual[0] - assert "False" in actual[0] + assert actual == [ + Issue( + type=IssueType.SCHEMA, + identifiers=[ + "openlabel", + "metadata", + "comment", + ], + reason="Value 'False' could not be interpreted as str.", + ) + ] def test_wrong_type_float(): @@ -119,10 +149,20 @@ def test_wrong_type_float(): } actual = validate_schema(data) - assert len(actual) == 1 - assert "$.openlabel.coordinate_systems.rgb_middle.pose_wrt_parent.translation.0:" in actual[0] - assert "float" in actual[0] - assert "None" in actual[0] + assert actual == [ + Issue( + type=IssueType.SCHEMA, + identifiers=[ + "openlabel", + "coordinate_systems", + "rgb_middle", + "pose_wrt_parent", + "translation", + 0, + ], + reason="Value 'None' could not be interpreted as float.", + ) + ] def test_wrong_type_uuid(): @@ -139,10 +179,16 @@ def test_wrong_type_uuid(): } actual = validate_schema(data) - assert len(actual) == 1 - assert "$.openlabel.objects:" in actual[0] - assert "UUID" in actual[0] - assert "NOT A VALID UUID" in actual[0] + assert actual == [ + Issue( + type=IssueType.SCHEMA, + identifiers=[ + "openlabel", + "objects", + ], + reason="Value 'NOT A VALID UUID' could not be interpreted as UUID.", + ) + ] def test_tuple_too_long(): @@ -163,12 +209,20 @@ def test_tuple_too_long(): } actual = validate_schema(data) - assert len(actual) == 1 - assert "$.openlabel.coordinate_systems.rgb_middle.pose_wrt_parent.translation:" in actual[0] - assert "length" in actual[0] - assert "4" in actual[0] - assert "3" in actual[0] + assert actual == [ + Issue( + type=IssueType.SCHEMA, + identifiers=[ + "openlabel", + "coordinate_systems", + "rgb_middle", + "pose_wrt_parent", + "translation", + ], + reason="Should have length of 4 but has length of 3.", + ) + ] if __name__ == "__main__": - pytest.main([__file__, "-v"]) + pytest.main([__file__, "-vv"])