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
153 lines
7.1 KiB
Python
153 lines
7.1 KiB
Python
# ------------------------------------------------------------------------
|
|
# RF-DETR
|
|
# Copyright (c) 2025 Roboflow. All Rights Reserved.
|
|
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
|
|
# ------------------------------------------------------------------------
|
|
"""Unit tests for :func:`rfdetr.detr._validate_shape_dims` and :func:`rfdetr.detr._resolve_patch_size`.
|
|
|
|
Tests call each helper directly so each validation path has a single focused test without the export/predict scaffolding
|
|
overhead.
|
|
"""
|
|
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from rfdetr.detr import _resolve_patch_size, _validate_shape_dims
|
|
|
|
|
|
class TestValidateShapeDimsHappyPath:
|
|
"""_validate_shape_dims returns normalised (height, width) for valid inputs."""
|
|
|
|
def test_exact_plain_ints(self) -> None:
|
|
"""Plain int dims divisible by block_size are returned unchanged."""
|
|
assert _validate_shape_dims((56, 112), 14, 14, 1) == (56, 112)
|
|
|
|
def test_returns_plain_int_tuple(self) -> None:
|
|
"""Return type is always a tuple of plain Python int."""
|
|
h, w = _validate_shape_dims((56, 56), 14, 14, 1)
|
|
assert type(h) is int and type(w) is int
|
|
|
|
def test_numpy_int_accepted(self) -> None:
|
|
"""numpy.int64 dims are accepted via operator.index and normalised."""
|
|
import numpy as np
|
|
|
|
h, w = _validate_shape_dims((np.int64(56), np.int64(112)), 14, 14, 1)
|
|
assert (h, w) == (56, 112)
|
|
assert type(h) is int and type(w) is int
|
|
|
|
def test_non_square_shape(self) -> None:
|
|
"""Non-square (H != W) shapes are returned correctly."""
|
|
assert _validate_shape_dims((112, 224), 14, 14, 1) == (112, 224)
|
|
|
|
def test_block_size_from_num_windows(self) -> None:
|
|
"""block_size = patch_size * num_windows; both dims divisible by it."""
|
|
# patch_size=16, num_windows=2 → block_size=32
|
|
assert _validate_shape_dims((64, 128), 32, 16, 2) == (64, 128)
|
|
|
|
|
|
class TestValidateShapeDimsArityErrors:
|
|
"""_validate_shape_dims raises ValueError for non-two-element shapes."""
|
|
|
|
def test_one_element_raises(self) -> None:
|
|
"""Single-element tuple must raise ValueError."""
|
|
with pytest.raises(ValueError, match="shape must be a sequence"):
|
|
_validate_shape_dims((56,), 14, 14, 1)
|
|
|
|
def test_three_element_raises(self) -> None:
|
|
"""Three-element tuple must raise ValueError."""
|
|
with pytest.raises(ValueError, match="shape must be a sequence"):
|
|
_validate_shape_dims((56, 56, 3), 14, 14, 1)
|
|
|
|
def test_scalar_raises(self) -> None:
|
|
"""Bare scalar (not a sequence) must raise ValueError."""
|
|
with pytest.raises(ValueError, match="shape must be a sequence"):
|
|
_validate_shape_dims(56, 14, 14, 1) # type: ignore[arg-type]
|
|
|
|
|
|
class TestValidateShapeDimsInvalidDim:
|
|
"""_validate_shape_dims rejects bool, float, and non-positive dimension values."""
|
|
|
|
@pytest.mark.parametrize("shape,match", [((True, 56), "height"), ((56, False), "width")])
|
|
def test_bool_dim_raises(self, shape: tuple, match: str) -> None:
|
|
"""Bool dims must raise ValueError even though bool is an int subtype."""
|
|
with pytest.raises(ValueError, match=f"{match} must be an integer"):
|
|
_validate_shape_dims(shape, 14, 14, 1) # type: ignore[arg-type]
|
|
|
|
@pytest.mark.parametrize("shape", [(56.0, 56.0), (56.0, 56), (56, 56.0)])
|
|
def test_float_dim_raises(self, shape: tuple) -> None:
|
|
"""Float dims must raise ValueError (operator.index rejects them)."""
|
|
with pytest.raises(ValueError, match="must be an integer"):
|
|
_validate_shape_dims(shape, 14, 14, 1)
|
|
|
|
@pytest.mark.parametrize("shape", [(0, 56), (56, 0), (-14, 56), (56, -14)])
|
|
def test_non_positive_dim_raises(self, shape: tuple[int, int]) -> None:
|
|
"""Zero or negative dims must raise ValueError."""
|
|
with pytest.raises(ValueError, match="positive integers"):
|
|
_validate_shape_dims(shape, 14, 14, 1)
|
|
|
|
|
|
class TestValidateShapeDimsDivisibilityCheck:
|
|
"""_validate_shape_dims enforces divisibility by block_size."""
|
|
|
|
@pytest.mark.parametrize(
|
|
"shape, block_size",
|
|
[
|
|
pytest.param((55, 56), 14, id="height_not_divisible"),
|
|
pytest.param((56, 55), 14, id="width_not_divisible"),
|
|
pytest.param((48, 64), 32, id="height_not_divisible_large_block"),
|
|
],
|
|
)
|
|
def test_indivisible_shape_raises(self, shape: tuple[int, int], block_size: int) -> None:
|
|
"""Shapes not divisible by block_size must raise ValueError."""
|
|
with pytest.raises(ValueError, match=f"divisible by {block_size}"):
|
|
_validate_shape_dims(shape, block_size, 14, 1)
|
|
|
|
def test_error_message_includes_patch_size_and_num_windows(self) -> None:
|
|
"""Error message must name patch_size and num_windows for debuggability."""
|
|
with pytest.raises(ValueError, match="patch_size=16") as exc_info:
|
|
_validate_shape_dims((48, 64), 32, 16, 2)
|
|
assert "num_windows=2" in str(exc_info.value)
|
|
|
|
|
|
class TestResolvePatchSize:
|
|
"""_resolve_patch_size resolves and validates patch_size for export()/predict()."""
|
|
|
|
def _cfg(self, patch_size: int) -> SimpleNamespace:
|
|
"""Return a minimal model_config stub with the given patch_size."""
|
|
return SimpleNamespace(patch_size=patch_size)
|
|
|
|
def test_none_reads_from_model_config(self) -> None:
|
|
"""patch_size=None resolves to model_config.patch_size."""
|
|
assert _resolve_patch_size(None, self._cfg(16), "export") == 16
|
|
|
|
def test_none_falls_back_to_14_when_config_missing(self) -> None:
|
|
"""patch_size=None falls back to 14 when model_config has no patch_size."""
|
|
assert _resolve_patch_size(None, SimpleNamespace(), "export") == 14
|
|
|
|
def test_explicit_matching_config_accepted(self) -> None:
|
|
"""Providing patch_size equal to model_config.patch_size succeeds."""
|
|
assert _resolve_patch_size(14, self._cfg(14), "export") == 14
|
|
|
|
def test_explicit_mismatch_raises(self) -> None:
|
|
"""Providing patch_size != model_config.patch_size must raise ValueError."""
|
|
with pytest.raises(ValueError, match="does not match"):
|
|
_resolve_patch_size(16, self._cfg(14), "export")
|
|
|
|
def test_mismatch_error_includes_caller_name(self) -> None:
|
|
"""Mismatch error message includes the caller name for context."""
|
|
with pytest.raises(ValueError, match="predict"):
|
|
_resolve_patch_size(16, self._cfg(14), "predict")
|
|
|
|
@pytest.mark.parametrize("bad", [0, -1, True, False])
|
|
def test_invalid_explicit_patch_size_raises(self, bad: int) -> None:
|
|
"""Non-positive-int patch_size must raise ValueError before the mismatch check."""
|
|
cfg = SimpleNamespace(patch_size=bad)
|
|
with pytest.raises(ValueError, match="patch_size must be a positive integer"):
|
|
_resolve_patch_size(bad, cfg, "export")
|
|
|
|
def test_invalid_config_patch_size_raises(self) -> None:
|
|
"""Bad patch_size in model_config (when caller passes None) must raise ValueError."""
|
|
with pytest.raises(ValueError, match="patch_size must be a positive integer"):
|
|
_resolve_patch_size(None, SimpleNamespace(patch_size=0), "export")
|