From 86f68fdd9af13e1ad7ddca93b98812427afa4356 Mon Sep 17 00:00:00 2001 From: h7x4 Date: Tue, 27 Jan 2026 16:19:08 +0900 Subject: [PATCH] cli/validate: wrap errors in types, improve formatting --- src/cli/validate.py | 233 ++++++++++++++++++++++++++++++-------------- 1 file changed, 159 insertions(+), 74 deletions(-) diff --git a/src/cli/validate.py b/src/cli/validate.py index 69e5272..b90ea20 100644 --- a/src/cli/validate.py +++ b/src/cli/validate.py @@ -1,16 +1,22 @@ +from dataclasses import dataclass +from textwrap import indent +from typing import Any, Callable + +from lib_marker import MarkerSet, Point, Track from marker_sets import WORLDS -from lib_marker import Point, Track def main() -> None: result_is_ok = True for test_name, test_f in _tests.items(): - results = test_f() + results = test_f(WORLDS) if len(results) > 0: result_is_ok = False print(f"[X] {test_name}") + print() for result in results: - print(f" {result}") + print(indent(result.pretty_print(), " ")) + print() else: print(f"[✓] {test_name}") @@ -18,126 +24,200 @@ def main() -> None: exit(1) -def validate_no_non_included_files() -> list: +class ValidationError: + error_type: str + world_name: str | None + marker_set_name: str | None + marker_name: str | None + note: str | None + details: dict[str, Any] + + def __init__( + self, + error_type: str, + world_name: str | None = None, + marker_set_name: str | None = None, + marker_name: str | None = None, + note: str | None = None, + **details, + ): + self.error_type = error_type + self.world_name = world_name + self.marker_set_name = marker_set_name + self.marker_name = marker_name + self.note = note + self.details = details + + def pretty_print(self) -> str: + result = [] + + location_parts = [ + x + for x in (self.world_name, self.marker_set_name, self.marker_name) + if x is not None + ] + if len(location_parts) > 0: + result.append(f"[{'/'.join(location_parts)}] ") + else: + result.append("[]") + + result.append(indent(f"type: {self.error_type}", " ")) + + if self.note: + result.append(indent(f"note: {self.note.removesuffix('.')}", " ")) + + for key, value in self.details.items(): + result.append(indent(f"{key}: {value}", " ")) + + return "\n".join(result) + + +@dataclass +class ValidationErrorPoint: + x: int + y: int + z: int + + def __init__(self, x: int, y: int, z: int) -> None: + self.x = x + self.y = y + self.z = z + + def __str__(self) -> str: + return f"({self.x}, {self.y}, {self.z})" + + +def validate_no_non_included_files( + worlds: dict[str, list[MarkerSet]], +) -> list[ValidationError]: # TODO: Implement this function return [] -def validate_no_invalid_y_values() -> list: +def validate_no_invalid_y_values( + worlds: dict[str, list[MarkerSet]], +) -> list[ValidationError]: result = [] - for world_name, marker_sets in WORLDS.items(): + for world_name, marker_sets in worlds.items(): for marker_set in marker_sets: for marker in marker_set.markers: if isinstance(marker, Point): if marker.y < -64 or marker.y > 255: result.append( - { - "error_type": "invalid_y_value", - "full_marker_name": ( - world_name, - marker_set.name, - marker.name, - ), - "y_value": marker.y, - } + ValidationError( + error_type="invalid_y_value", + world_name=world_name, + marker_set_name=marker_set.name, + marker_name=marker.name, + y_value=marker.y, + note="Point has coordinate above 255 or below -64.", + ) ) elif isinstance(marker, Track): for i, point in enumerate(marker.points): _, y, _ = point if y < -64 or y > 255: result.append( - { - "error_type": "invalid_y_value", - "full_marker_name": ( - world_name, - marker_set.name, - marker.name, - ), - "index": i, - "y_value": y, - } + ValidationError( + error_type="invalid_y_value", + world_name=world_name, + marker_set_name=marker_set.name, + marker_name=marker.name, + index=i, + y_value=y, + note="Track point has coordinate above 255 or below -64.", + ) ) return result -def validate_no_duplicate_points_in_tracks() -> list: +def validate_no_consecutive_duplicate_points_in_tracks( + worlds: dict[str, list[MarkerSet]], +) -> list[ValidationError]: result = [] - for world_name, marker_sets in WORLDS.items(): + for world_name, marker_sets in worlds.items(): for marker_set in marker_sets: for marker in marker_set.markers: if isinstance(marker, Track): for p1, p2 in zip(marker.points, marker.points[1:]): if p1 == p2: result.append( - { - "error_type": "duplicate_point", - "point_a": ( - world_name, - marker_set.name, - marker.name, - ), - "point_b": ( - world_name, - marker_set.name, - marker.name, - ), - "coordinates": p1, - } + ValidationError( + error_type="duplicate_point_in_track", + world_name=world_name, + marker_set_name=marker_set.name, + marker_name=marker.name, + point=ValidationErrorPoint(*p1), + note="Consecutive points in a track must not be identical, this is likely a copy-paste error.", + ) ) return result -def validate_no_duplicate_marker_names() -> list: +def validate_no_duplicate_marker_names( + worlds: dict[str, list[MarkerSet]], +) -> list[ValidationError]: result = [] marker_names = set() - for world_name, marker_sets in WORLDS.items(): + for world_name, marker_sets in worlds.items(): for marker_set in marker_sets: for marker in marker_set.markers: full_name = (world_name, marker_set.name, marker.name) if full_name in marker_names: result.append( - { - "error_type": "duplicate_marker_name", - "full_marker_name": full_name, - } + ValidationError( + error_type="duplicate_marker_name", + world_name=world_name, + marker_set_name=marker_set.name, + marker_name=marker.name, + note="Marker names must be unique within a marker set.", + ) ) marker_names.add(full_name) return result -def validate_no_duplicate_marker_set_names() -> list: +def validate_no_duplicate_marker_set_names( + worlds: dict[str, list[MarkerSet]], +) -> list[ValidationError]: result = [] marker_set_names = set() - for world_name, marker_sets in WORLDS.items(): + for world_name, marker_sets in worlds.items(): for marker_set in marker_sets: if (world_name, marker_set.name) in marker_set_names: result.append( - { - "error_type": "duplicate_marker_set_name", - "full_marker_set_name": (world_name, marker_set.name), - } + ValidationError( + error_type="duplicate_marker_set_name", + world_name=world_name, + marker_set_name=marker_set.name, + note="Marker set names must be unique within a world.", + ) ) marker_set_names.add((world_name, marker_set.name)) return result -def validate_no_unused_icons() -> list: +def validate_no_unused_icons( + worlds: dict[str, list[MarkerSet]], +) -> list[ValidationError]: # TODO: Implement this function return [] -def validate_no_duplicate_points() -> list: +def validate_no_duplicate_points( + worlds: dict[str, list[MarkerSet]], +) -> list[ValidationError]: result = [] - for world_name, marker_sets in WORLDS.items(): + for world_name, marker_sets in worlds.items(): points = {} for marker_set in marker_sets: for marker in marker_set.markers: @@ -145,43 +225,48 @@ def validate_no_duplicate_points() -> list: point = (marker.x, marker.y, marker.z) if point in points: result.append( - { - "error_type": "duplicate_point", - "point_a": points[point], - "point_b": (world_name, marker_set.name, marker.name), - "coordinates": point, - } + ValidationError( + error_type="duplicate_point", + world_name=world_name, + marker_set_name=points[point][1], + marker_name=points[point][2], + other_marker_set_name=marker_set.name, + other_marker_name=marker.name, + location=ValidationErrorPoint(*point), + note="Two markers should not share the same coordinates.", + ) ) points[point] = (world_name, marker_set.name, marker.name) return result -def validate_all_tracks_have_at_least_two_points() -> list: +def validate_all_tracks_have_at_least_two_points( + worlds: dict[str, list[MarkerSet]], +) -> list[ValidationError]: result = [] - for world_name, marker_sets in WORLDS.items(): + for world_name, marker_sets in worlds.items(): for marker_set in marker_sets: for marker in marker_set.markers: if isinstance(marker, Track) and len(marker.points) < 2: result.append( - { - "error_type": "track_too_short", - "full_marker_name": ( - world_name, - marker_set.name, - marker.name, - ), - } + ValidationError( + error_type="track_too_short", + world_name=world_name, + marker_set_name=marker_set.name, + marker_name=marker.name, + note="Tracks must have at least two points.", + ) ) return result -_tests = { +_tests: dict[str, Callable[[dict[str, list[MarkerSet]]], list[ValidationError]]] = { "No non-included files": validate_no_non_included_files, "No invalid y coordinates": validate_no_invalid_y_values, - "No duplicate points in tracks": validate_no_duplicate_points_in_tracks, + "No consecutive duplicate points in tracks": validate_no_consecutive_duplicate_points_in_tracks, "No duplicate marker names": validate_no_duplicate_marker_names, "No duplicate marker-set names": validate_no_duplicate_marker_set_names, "No unused icons": validate_no_unused_icons,