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
270 lines
11 KiB
Python
270 lines
11 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 package metadata helpers and structural import paths."""
|
|
|
|
import subprocess
|
|
import sys
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from rfdetr.utilities.package import get_sha
|
|
|
|
|
|
def test_get_sha_marks_dirty_worktree_when_diff_command_returns_exit_code_1() -> None:
|
|
"""A diff exit code of 1 should report uncommitted changes, not unknown."""
|
|
|
|
def _fake_check_output(command: list[str], cwd: str | None = None) -> bytes:
|
|
if command[:3] == ["git", "rev-parse", "HEAD"]:
|
|
return b"abc123\n"
|
|
if command[:4] == ["git", "rev-parse", "--abbrev-ref", "HEAD"]:
|
|
return b"feature/test\n"
|
|
raise AssertionError(f"Unexpected command: {command!r}")
|
|
|
|
class _RunResult:
|
|
def __init__(self, returncode: int) -> None:
|
|
self.returncode = returncode
|
|
|
|
with (
|
|
patch("rfdetr.utilities.package.subprocess.check_output", side_effect=_fake_check_output),
|
|
patch("rfdetr.utilities.package.subprocess.run", return_value=_RunResult(returncode=1)),
|
|
):
|
|
sha = get_sha()
|
|
|
|
assert sha == "sha: abc123, status: has uncommitted changes, branch: feature/test"
|
|
|
|
|
|
def test_peft_not_imported_eagerly_on_backbone_import_characterization() -> None:
|
|
"""Importing backbone.backbone must NOT pull peft into sys.modules (peft is optional).
|
|
|
|
This characterization test captures the invariant introduced in PR 1 (chore/packaging-peft-lora): after the lazy-
|
|
import refactor, importing backbone at module-load time must not trigger a top-level ``from peft import PeftModel``.
|
|
"""
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-c",
|
|
(
|
|
"import sys; "
|
|
"import rfdetr.models.backbone.backbone; "
|
|
"assert 'peft' not in sys.modules, "
|
|
"'peft was eagerly imported by backbone.backbone'"
|
|
),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 0, (
|
|
"Subprocess for backbone import failed:\n"
|
|
f"return code: {result.returncode}\n"
|
|
f"stdout:\n{result.stdout}\n"
|
|
f"stderr:\n{result.stderr}"
|
|
)
|
|
|
|
|
|
class TestImportPaths:
|
|
"""Structural tests for the detr.py → inference.py + variants.py split (PR 6).
|
|
|
|
After the split:
|
|
- ``rfdetr.inference`` exports ``ModelContext`` and ``_build_model_context``
|
|
- ``rfdetr.variants`` exports all 15 concrete model classes
|
|
- ``rfdetr.detr`` re-exports both for backward compatibility
|
|
- ``rfdetr`` (top-level) continues to export public names unchanged
|
|
"""
|
|
|
|
def test_model_context_importable_from_inference(self) -> None:
|
|
"""ModelContext must be importable from the new rfdetr.inference module."""
|
|
from rfdetr.inference import ModelContext
|
|
|
|
assert ModelContext is not None
|
|
|
|
def test_build_model_context_importable_from_inference(self) -> None:
|
|
"""_build_model_context must be importable from rfdetr.inference."""
|
|
from rfdetr.inference import _build_model_context
|
|
|
|
assert callable(_build_model_context)
|
|
|
|
def test_rfdetr_large_importable_from_variants(self) -> None:
|
|
"""RFDETRLarge must be importable from the new rfdetr.variants module."""
|
|
from rfdetr.variants import RFDETRLarge
|
|
|
|
assert RFDETRLarge is not None
|
|
|
|
def test_variants_import_first_does_not_trigger_circular_import(self) -> None:
|
|
"""Importing variants before detr must still preserve shared class identity."""
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-c",
|
|
(
|
|
"from rfdetr.variants import RFDETRLarge; "
|
|
"from rfdetr.detr import RFDETRLarge as FromDetr; "
|
|
"assert RFDETRLarge is FromDetr"
|
|
),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 0, (
|
|
"Subprocess for variants-first import failed:\n"
|
|
f"return code: {result.returncode}\n"
|
|
f"stdout:\n{result.stdout}\n"
|
|
f"stderr:\n{result.stderr}"
|
|
)
|
|
|
|
@pytest.mark.parametrize(
|
|
"class_name",
|
|
[
|
|
pytest.param("RFDETRBase", id="base"),
|
|
pytest.param("RFDETRKeypointPreview", id="keypoint-preview"),
|
|
pytest.param("RFDETRNano", id="nano"),
|
|
pytest.param("RFDETRSmall", id="small"),
|
|
pytest.param("RFDETRMedium", id="medium"),
|
|
pytest.param("RFDETRLargeDeprecated", id="large-deprecated"),
|
|
pytest.param("RFDETRLarge", id="large"),
|
|
pytest.param("RFDETRSeg", id="seg-base"),
|
|
pytest.param("RFDETRSegPreview", id="seg-preview"),
|
|
pytest.param("RFDETRSegNano", id="seg-nano"),
|
|
pytest.param("RFDETRSegSmall", id="seg-small"),
|
|
pytest.param("RFDETRSegMedium", id="seg-medium"),
|
|
pytest.param("RFDETRSegLarge", id="seg-large"),
|
|
pytest.param("RFDETRSegXLarge", id="seg-xlarge"),
|
|
pytest.param("RFDETRSeg2XLarge", id="seg-2xlarge"),
|
|
],
|
|
)
|
|
def test_all_variant_classes_importable_from_variants(self, class_name: str) -> None:
|
|
"""Every concrete variant class must be importable from rfdetr.variants."""
|
|
import rfdetr.variants as variants_mod
|
|
|
|
cls = getattr(variants_mod, class_name, None)
|
|
assert cls is not None, f"{class_name} not found in rfdetr.variants"
|
|
|
|
def test_model_context_backward_compat_from_detr(self) -> None:
|
|
"""ModelContext must remain importable from rfdetr.detr (backward compat)."""
|
|
from rfdetr.detr import ModelContext
|
|
|
|
assert ModelContext is not None
|
|
|
|
def test_rfdetr_large_backward_compat_from_detr(self) -> None:
|
|
"""RFDETRLarge must remain importable from rfdetr.detr (backward compat)."""
|
|
from rfdetr.detr import RFDETRLarge
|
|
|
|
assert RFDETRLarge is not None
|
|
|
|
def test_rfdetr_large_importable_from_top_level(self) -> None:
|
|
"""RFDETRLarge must remain importable from rfdetr (top-level package)."""
|
|
from rfdetr import RFDETRLarge
|
|
|
|
assert RFDETRLarge is not None
|
|
|
|
def test_model_context_importable_from_top_level(self) -> None:
|
|
"""ModelContext must remain importable from rfdetr (top-level package)."""
|
|
from rfdetr import ModelContext
|
|
|
|
assert ModelContext is not None
|
|
|
|
def test_top_level_import_sets_numpy_complex_alias(self) -> None:
|
|
"""Verify rfdetr shim sets np.complex_ in a fresh interpreter with complex_ absent.
|
|
|
|
Runs in a subprocess so the shim code path is actually executed — importing rfdetr inside pytest is a no-op
|
|
because rfdetr is already cached in sys.modules.
|
|
"""
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-c",
|
|
(
|
|
"import numpy\n"
|
|
"if hasattr(numpy, 'complex_'):\n"
|
|
" del numpy.complex_\n"
|
|
"import rfdetr\n"
|
|
"assert hasattr(numpy, 'complex_'), 'shim did not set numpy.complex_'\n"
|
|
"assert numpy.complex_ is numpy.complex128, 'complex_ must alias complex128'\n"
|
|
),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 0, (
|
|
"Subprocess for NumPy complex_ shim failed:\n"
|
|
f"return code: {result.returncode}\n"
|
|
f"stdout:\n{result.stdout}\n"
|
|
f"stderr:\n{result.stderr}"
|
|
)
|
|
|
|
def test_identity_across_import_paths(self) -> None:
|
|
"""The same class object must be returned regardless of import path.
|
|
|
|
This ensures re-exports are true re-exports (not copies) so that isinstance() checks work across all import
|
|
paths.
|
|
"""
|
|
import rfdetr
|
|
from rfdetr.detr import ModelContext as FromDetr
|
|
from rfdetr.detr import RFDETRLarge as LargeFromDetr
|
|
from rfdetr.inference import ModelContext as FromInference
|
|
from rfdetr.variants import RFDETRLarge as LargeFromVariants
|
|
|
|
assert FromDetr is FromInference, (
|
|
"rfdetr.detr.ModelContext and rfdetr.inference.ModelContext must be the same object"
|
|
)
|
|
assert LargeFromDetr is LargeFromVariants, (
|
|
"rfdetr.detr.RFDETRLarge and rfdetr.variants.RFDETRLarge must be the same object"
|
|
)
|
|
assert rfdetr.RFDETRLarge is LargeFromVariants, (
|
|
"top-level rfdetr.RFDETRLarge and rfdetr.variants.RFDETRLarge must be the same object"
|
|
)
|
|
assert rfdetr.ModelContext is FromInference, (
|
|
"top-level rfdetr.ModelContext and rfdetr.inference.ModelContext must be the same object"
|
|
)
|
|
|
|
def test_build_model_context_backward_compat_from_detr(self) -> None:
|
|
"""_build_model_context must remain importable from rfdetr.detr (backward compat)."""
|
|
from rfdetr.detr import _build_model_context
|
|
|
|
assert callable(_build_model_context)
|
|
|
|
def test_getattr_raises_for_unknown_names(self) -> None:
|
|
"""Accessing an unknown name via rfdetr.detr must raise AttributeError."""
|
|
import rfdetr.detr as detr_mod
|
|
|
|
with pytest.raises(AttributeError, match="has no attribute"):
|
|
_ = detr_mod._this_name_does_not_exist_12345
|
|
|
|
def test_dir_includes_lazy_exports(self) -> None:
|
|
"""dir(rfdetr.detr) must include all lazy re-export names."""
|
|
import rfdetr.detr as detr_mod
|
|
|
|
names = dir(detr_mod)
|
|
assert "ModelContext" in names, "ModelContext missing from dir(rfdetr.detr)"
|
|
assert "RFDETRLarge" in names, "RFDETRLarge missing from dir(rfdetr.detr)"
|
|
assert "RFDETRBase" in names, "RFDETRBase missing from dir(rfdetr.detr)"
|
|
|
|
def test_detr_first_import_order_preserves_identity(self) -> None:
|
|
"""Importing detr before variants must preserve object identity for variant classes."""
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-c",
|
|
(
|
|
"from rfdetr.detr import RFDETRLarge; "
|
|
"from rfdetr.variants import RFDETRLarge as FromVariants; "
|
|
"assert RFDETRLarge is FromVariants"
|
|
),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 0, (
|
|
"Subprocess for detr-first import failed:\n"
|
|
f"return code: {result.returncode}\n"
|
|
f"stdout:\n{result.stdout}\n"
|
|
f"stderr:\n{result.stderr}"
|
|
)
|