cli/validate: wrap errors in types, improve formatting

This commit is contained in:
2026-01-27 16:19:08 +09:00
parent 5cd25d1717
commit 86f68fdd9a

View File

@@ -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,