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