Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies = [
"pyyaml>=6.0.2",
"requests>=2.32.3",
"typer>=0.15.1",
"xxhash>=3.6.0",
]

[build-system]
Expand Down
23 changes: 21 additions & 2 deletions toolbox/commands/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from jsonschema import ValidationError, validate

from toolbox.utils.config import EventConfig, LabelsConfig, RegistrationConfig
from toolbox.utils.hash import hash_file
from toolbox.utils.tasks import find_tasks

app = typer.Typer()
Expand Down Expand Up @@ -89,6 +90,8 @@ def tasks(context: typer.Context):

valid_count = 0
invalid_count = 0
tasks_icons = {}
tasks_backgrounds = {}

rich.print("[dim]Validating tasks...")
for subdir_path in find_tasks(tasks_directory):
Expand Down Expand Up @@ -119,7 +122,7 @@ def tasks(context: typer.Context):
invalid_count += 1
continue

if not verify_pictures(subdir_path / "pictures"):
if not verify_pictures(subdir_path / "pictures", task_id, tasks_icons, tasks_backgrounds):
invalid_count += 1
continue

Expand All @@ -136,6 +139,13 @@ def tasks(context: typer.Context):
rich.print(f"\nFinished validating all tasks: {total_tasks} tasks processed.")
rich.print(f"[green]{valid_count} tasks are valid.")

for task_list in tasks_icons.values():
if len(task_list) > 1:
rich.print("[yellow]Following tasks have the same icons: " + ", ".join(task_list))
for task_list in tasks_backgrounds.values():
if len(task_list) > 1:
rich.print("[yellow]Following tasks have the same backgrounds: " + ", ".join(task_list))

if invalid_count > 0:
rich.print(f"[red]{invalid_count} tasks are invalid.")
raise Exit(code=1)
Expand Down Expand Up @@ -167,12 +177,21 @@ def verify_assets(yaml_data: dict, assets_path: Path, subdir_path: Path) -> bool
return True


def verify_pictures(subdir_path: Path) -> bool:
def verify_pictures(
subdir_path: Path, task_id: str, tasks_icons: dict[str, list[str]], tasks_backgrounds: dict[str, list[str]]
) -> bool:
required_pictures = ["background.png", "icon.png"]
for picture in required_pictures:
picture_path = subdir_path.joinpath(picture)
if not picture_path.is_file():
rich.print(f"[red]Missing file pictures/{picture} for {subdir_path}")
return False
picture_hash = hash_file(picture_path)
if picture == "icon.png":
tasks_icons[picture_hash] = tasks_icons.get(picture_hash, []) + [task_id]
elif picture == "background.png":
tasks_backgrounds[picture_hash] = tasks_backgrounds.get(picture_hash, []) + [task_id]
else:
rich.print("[red]Unsupported file type for duplicate detection: " + picture)

return True
57 changes: 54 additions & 3 deletions toolbox/tests/test_verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,43 @@ def test_verify_invalid_difficulty(
tasks(mock_context)


@patch("rich.print")
@patch("toolbox.commands.verify.hash_file")
@patch.object(Path, "iterdir")
@patch.object(Path, "is_dir")
@patch.object(Path, "is_file")
@patch.object(Path, "read_text")
def test_verify_duplicated_pictures(
mock_read_text,
mock_is_file,
mock_is_dir,
mock_iterdir,
mock_hash_file,
mock_rich_print,
mock_context,
valid_schema,
valid_task_config,
valid_labels_config,
):
second_task = valid_task_config.copy()
second_task["id"] = "second_task"
mock_iterdir.side_effect = [[Path("valid_task"), Path("second_task")], [], []]
mock_is_dir.return_value = True
mock_is_file.return_value = True
mock_read_text.side_effect = [
yaml.dump(valid_labels_config),
json.dumps(valid_schema),
yaml.dump(valid_task_config),
yaml.dump(second_task),
]
mock_hash_file.return_value = "duplicate_hash"

tasks(mock_context)

mock_rich_print.assert_any_call("[yellow]Following tasks have the same icons: valid_task, second_task")
mock_rich_print.assert_any_call("[yellow]Following tasks have the same backgrounds: valid_task, second_task")


@patch.object(Path, "iterdir")
@patch.object(Path, "is_file")
@patch.object(Path, "is_dir")
Expand Down Expand Up @@ -360,15 +397,29 @@ def test_labels_missing_icons(mock_iterdir, mock_read_text, mock_context, valid_
labels(mock_context)


@patch("toolbox.commands.verify.hash_file")
@patch.object(Path, "is_file")
def test_valid_verify_pictures(mock_is_file, _mock_hash_file):
mock_is_file.return_value = True

assert verify_pictures(Path("assets"), "", {}, {}) is True


@patch("toolbox.commands.verify.hash_file")
@patch.object(Path, "is_file")
def test_valid_verify_pictures(mock_is_file):
def test_valid_verify_pictures_duplicated(mock_is_file, mock_hash_file):
mock_is_file.return_value = True
mock_hash_file.return_value = "duplicate_hash"
tasks_icons = {"duplicate_hash": ["41"]}
tasks_backgrounds = {"duplicate_hash": ["UwU"]}

assert verify_pictures(Path("assets")) is True
assert verify_pictures(Path("assets"), "67", tasks_icons, tasks_backgrounds) is True
assert tasks_icons["duplicate_hash"] == ["41", "67"]
assert tasks_backgrounds["duplicate_hash"] == ["UwU", "67"]


@patch.object(Path, "is_file")
def test_missing_verify_pictures(mock_is_file):
mock_is_file.return_value = False

assert verify_pictures(Path("assets")) is False
assert verify_pictures(Path("assets"), "", {}, {}) is False
13 changes: 13 additions & 0 deletions toolbox/utils/hash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from pathlib import Path

from xxhash import xxh64


def hash_file(path: Path, chunk_size=8192):
buffer = xxh64()

with path.open("rb") as file:
while chunk := file.read(chunk_size):
buffer.update(chunk)

return buffer.hexdigest()
Loading