16031aae96
CPU tests Workflow / Testing (ubuntu-latest, 3.12) (push) Failing after 1s
CPU tests Workflow / Testing (ubuntu-latest, 3.13) (push) Failing after 0s
Mypy Type Check / Type Check (push) Failing after 0s
Docs/Test WorkFlow / Test docs build (push) Failing after 1s
PR Conflict Labeler / labeling (push) Failing after 1s
Dependency resolution / Resolve [tflite] extra — Python 3.12 (push) Failing after 0s
Smoke Tests / try-all-models (ubuntu-latest, 3.10) (push) Failing after 0s
Smoke Tests / try-all-models (ubuntu-latest, 3.13) (push) Failing after 1s
CPU tests Workflow / build-pkg (push) Failing after 1s
CPU tests Workflow / Testing (ubuntu-latest, 3.10) (push) Failing after 0s
CPU tests Workflow / Testing (ubuntu-latest, 3.11) (push) Failing after 0s
Smoke Tests / try-all-models (macos-latest, 3.10) (push) Has been cancelled
Smoke Tests / try-all-models (macos-latest, 3.13) (push) Has been cancelled
Smoke Tests / try-all-models (windows-latest, 3.10) (push) Has been cancelled
Smoke Tests / try-all-models (windows-latest, 3.13) (push) Has been cancelled
CPU tests Workflow / Testing (macos-latest, 3.10) (push) Has been cancelled
CPU tests Workflow / Testing (macos-latest, 3.13) (push) Has been cancelled
CPU tests Workflow / Testing (windows-latest, 3.10) (push) Has been cancelled
CPU tests Workflow / Testing (windows-latest, 3.13) (push) Has been cancelled
CPU tests Workflow / testing-guardian (push) Has been cancelled
GPU tests Workflow / Testing (push) Has been cancelled
119 lines
4.5 KiB
Python
119 lines
4.5 KiB
Python
# ------------------------------------------------------------------------
|
|
# RF-DETR
|
|
# Copyright (c) 2025 Roboflow. All Rights Reserved.
|
|
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
|
|
# ------------------------------------------------------------------------
|
|
"""Tests for private developer download helpers."""
|
|
|
|
import io
|
|
import zipfile
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from rfdetr.datasets._develop import (
|
|
_coco_val_images_complete,
|
|
_download_and_extract,
|
|
_download_lock,
|
|
_nonempty_file_exists,
|
|
)
|
|
|
|
|
|
class TestCocoValImagesComplete:
|
|
"""Regression coverage for interrupted COCO val2017 image downloads."""
|
|
|
|
def test_missing_directory_is_incomplete(self, tmp_path: Path) -> None:
|
|
"""A missing image directory must trigger a download."""
|
|
assert not _coco_val_images_complete(tmp_path / "val2017")
|
|
|
|
def test_empty_existing_directory_is_incomplete(self, tmp_path: Path) -> None:
|
|
"""An empty ``val2017`` directory must not skip the image download."""
|
|
images_root = tmp_path / "val2017"
|
|
images_root.mkdir()
|
|
|
|
assert not _coco_val_images_complete(images_root)
|
|
|
|
@pytest.mark.parametrize(
|
|
"file_count,expected",
|
|
[
|
|
pytest.param(1, False, id="below_threshold_is_incomplete"),
|
|
pytest.param(2, True, id="at_threshold_is_complete"),
|
|
pytest.param(3, True, id="above_threshold_is_complete"),
|
|
],
|
|
)
|
|
def test_file_count_threshold(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, file_count: int, expected: bool
|
|
) -> None:
|
|
"""Directory completeness reflects the >= threshold semantics."""
|
|
import rfdetr.datasets._develop as _develop_mod
|
|
|
|
monkeypatch.setattr(_develop_mod, "_COCO_VAL_IMAGE_COUNT", 2)
|
|
images_root = tmp_path / "val2017"
|
|
images_root.mkdir()
|
|
for i in range(file_count):
|
|
(images_root / f"{i:012d}.jpg").write_bytes(b"jpeg")
|
|
|
|
assert _coco_val_images_complete(images_root) is expected
|
|
|
|
|
|
class TestNonemptyFileExists:
|
|
"""Regression coverage for annotation file integrity checks in benchmark downloads."""
|
|
|
|
def test_missing_file_is_incomplete(self, tmp_path: Path) -> None:
|
|
"""A missing annotation file must trigger a download."""
|
|
annotations_path = tmp_path / "instances_val2017.json"
|
|
|
|
assert not _nonempty_file_exists(annotations_path)
|
|
|
|
def test_empty_file_is_incomplete(self, tmp_path: Path) -> None:
|
|
"""An empty annotation file must trigger a re-download."""
|
|
annotations_path = tmp_path / "instances_val2017.json"
|
|
annotations_path.write_bytes(b"")
|
|
|
|
assert not _nonempty_file_exists(annotations_path)
|
|
|
|
def test_nonempty_file_is_complete(self, tmp_path: Path) -> None:
|
|
"""A non-empty annotation file is accepted without re-download."""
|
|
annotations_path = tmp_path / "instances_val2017.json"
|
|
annotations_path.write_bytes(b"{}")
|
|
|
|
assert _nonempty_file_exists(annotations_path)
|
|
|
|
|
|
class TestDownloadLock:
|
|
"""Coverage for the cross-process file-lock context manager."""
|
|
|
|
def test_timeout_raises_when_lock_held(self, tmp_path: Path) -> None:
|
|
"""TimeoutError is raised immediately when the lock file already exists and timeout_s=0."""
|
|
lock_path = tmp_path / "test.lock"
|
|
lock_path.touch()
|
|
|
|
with pytest.raises(TimeoutError):
|
|
with _download_lock(lock_path, timeout_s=0, poll_s=0):
|
|
pass
|
|
|
|
|
|
class TestDownloadAndExtract:
|
|
"""Coverage for the ZIP download-and-extract helper."""
|
|
|
|
def _make_zip(self, members: dict) -> bytes:
|
|
"""Build an in-memory ZIP archive from a mapping of filename→content."""
|
|
buf = io.BytesIO()
|
|
with zipfile.ZipFile(buf, "w") as zf:
|
|
for name, content in members.items():
|
|
zf.writestr(name, content)
|
|
return buf.getvalue()
|
|
|
|
def test_path_traversal_raises_runtime_error(self, tmp_path: Path) -> None:
|
|
"""A ZIP entry escaping dest_dir must raise RuntimeError (path-traversal guard)."""
|
|
zip_bytes = self._make_zip({"../evil.txt": "malicious"})
|
|
url = "http://example.com/test.zip"
|
|
|
|
def fake_urlretrieve(url: str, dest: str) -> None:
|
|
Path(dest).write_bytes(zip_bytes)
|
|
|
|
with patch("rfdetr.datasets._develop.urlretrieve", side_effect=fake_urlretrieve):
|
|
with pytest.raises(RuntimeError, match="Unsafe path detected"):
|
|
_download_and_extract(url, tmp_path)
|