Files
roboflow--rf-detr/tests/models/test_validate_shape_dims.py
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:26:24 +08:00

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")