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
190 lines
8.3 KiB
Python
190 lines
8.3 KiB
Python
# ------------------------------------------------------------------------
|
|
# RF-DETR
|
|
# Copyright (c) 2025 Roboflow. All Rights Reserved.
|
|
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
|
|
# ------------------------------------------------------------------------
|
|
"""Regression tests for _namespace_from_configs() config forwarding."""
|
|
|
|
import sys
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from rfdetr._namespace import _namespace_from_configs
|
|
from rfdetr.config import RFDETRBaseConfig, RFDETRSegNanoConfig, SegmentationTrainConfig, TrainConfig
|
|
from rfdetr.models._types import BuilderArgs
|
|
|
|
|
|
class TestNamespaceForwarding:
|
|
"""Verify that _namespace_from_configs() forwards TrainConfig fields that were previously hardcoded to wrong
|
|
defaults."""
|
|
|
|
def _make_ns(self: "TestNamespaceForwarding", **tc_kwargs: Any) -> Any:
|
|
"""Build a namespace for tests with minimal default TrainConfig values."""
|
|
mc = RFDETRBaseConfig(num_classes=80)
|
|
tc_kwargs.setdefault("dataset_dir", "/tmp")
|
|
tc = TrainConfig(**tc_kwargs)
|
|
return _namespace_from_configs(mc, tc)
|
|
|
|
def test_aug_config_forwarded_when_set(self: "TestNamespaceForwarding") -> None:
|
|
aug = {"hsv_h": 0.015, "hsv_s": 0.7}
|
|
ns = self._make_ns(aug_config=aug)
|
|
assert ns.aug_config == aug
|
|
|
|
def test_aug_config_none_by_default(self: "TestNamespaceForwarding") -> None:
|
|
ns = self._make_ns()
|
|
assert ns.aug_config is None
|
|
|
|
def test_use_ema_forwarded_true(self: "TestNamespaceForwarding") -> None:
|
|
ns = self._make_ns(use_ema=True)
|
|
assert ns.use_ema is True
|
|
|
|
def test_use_ema_forwarded_false(self: "TestNamespaceForwarding") -> None:
|
|
ns = self._make_ns(use_ema=False)
|
|
assert ns.use_ema is False
|
|
|
|
def test_early_stopping_use_ema_forwarded_true(self: "TestNamespaceForwarding") -> None:
|
|
ns = self._make_ns(early_stopping_use_ema=True)
|
|
assert ns.early_stopping_use_ema is True
|
|
|
|
def test_early_stopping_use_ema_forwarded_false(self: "TestNamespaceForwarding") -> None:
|
|
ns = self._make_ns(early_stopping_use_ema=False)
|
|
assert ns.early_stopping_use_ema is False
|
|
|
|
|
|
class TestNamespaceProtocol:
|
|
"""_namespace_from_configs() output must satisfy the BuilderArgs Protocol."""
|
|
|
|
def _make_ns(self, mc=None, tc=None):
|
|
mc = mc or RFDETRBaseConfig(num_classes=80)
|
|
tc = tc or TrainConfig(dataset_dir="/tmp")
|
|
return _namespace_from_configs(mc, tc)
|
|
|
|
@pytest.mark.skipif(
|
|
sys.version_info < (3, 12),
|
|
reason="Runtime Protocol attribute checks require Python 3.12+",
|
|
)
|
|
def test_namespace_satisfies_builderargs_protocol_py312(self) -> None:
|
|
"""On Python 3.12+, isinstance() verifies data-attribute presence."""
|
|
ns = self._make_ns()
|
|
assert isinstance(ns, BuilderArgs)
|
|
|
|
def test_namespace_is_builderargs_instance(self) -> None:
|
|
"""Isinstance() check passes on all supported Python versions.
|
|
|
|
On Python 3.10/3.11 this is a structural no-op (no method members to check). On 3.12+ it verifies attribute
|
|
presence. The test documents the intent regardless of Python version.
|
|
"""
|
|
ns = self._make_ns()
|
|
assert isinstance(ns, BuilderArgs)
|
|
|
|
|
|
class TestNamespaceFieldOwnership:
|
|
"""Verify that the namespace reads each field from the authoritative owner."""
|
|
|
|
def _make_ns(self, mc=None, tc=None):
|
|
mc = mc or RFDETRBaseConfig(num_classes=80)
|
|
tc = tc or TrainConfig(dataset_dir="/tmp")
|
|
return _namespace_from_configs(mc, tc)
|
|
|
|
# --- cls_loss_coef must come from TrainConfig ---
|
|
|
|
def test_cls_loss_coef_from_train_config(self) -> None:
|
|
"""ns.cls_loss_coef must reflect TrainConfig.cls_loss_coef, not ModelConfig."""
|
|
mc = RFDETRBaseConfig(num_classes=80) # cls_loss_coef=1.0 (ModelConfig default)
|
|
tc = TrainConfig(dataset_dir="/tmp", cls_loss_coef=2.5)
|
|
ns = _namespace_from_configs(mc, tc)
|
|
assert ns.cls_loss_coef == pytest.approx(2.5)
|
|
|
|
def test_cls_loss_coef_segmentation_default_matches_pre_1_7_effective_value(self) -> None:
|
|
"""SegmentationTrainConfig default must preserve the pre-1.7 effective loss_ce weight."""
|
|
mc = RFDETRSegNanoConfig()
|
|
tc = SegmentationTrainConfig(dataset_dir="/tmp")
|
|
ns = _namespace_from_configs(mc, tc)
|
|
assert ns.cls_loss_coef == pytest.approx(1.0)
|
|
|
|
def test_cls_loss_coef_segmentation_explicit_train_config_value_wins(self) -> None:
|
|
"""Explicit SegmentationTrainConfig.cls_loss_coef values must propagate to namespace."""
|
|
mc = RFDETRSegNanoConfig()
|
|
tc = SegmentationTrainConfig(dataset_dir="/tmp", cls_loss_coef=5.0)
|
|
ns = _namespace_from_configs(mc, tc)
|
|
assert ns.cls_loss_coef == pytest.approx(5.0)
|
|
|
|
def test_cls_loss_coef_train_config_wins_over_explicit_model_config(self) -> None:
|
|
"""When both are explicitly set, TrainConfig.cls_loss_coef takes precedence."""
|
|
with pytest.warns(DeprecationWarning, match="ModelConfig\\.cls_loss_coef is deprecated"):
|
|
mc = RFDETRBaseConfig(num_classes=80, cls_loss_coef=0.5)
|
|
tc = TrainConfig(dataset_dir="/tmp", cls_loss_coef=3.0)
|
|
ns = _namespace_from_configs(mc, tc)
|
|
assert ns.cls_loss_coef == pytest.approx(3.0)
|
|
|
|
def test_cls_loss_coef_model_config_explicit_is_preserved_during_deprecation(self) -> None:
|
|
"""Explicit ModelConfig.cls_loss_coef remains effective until removal."""
|
|
with pytest.warns(DeprecationWarning, match="ModelConfig\\.cls_loss_coef is deprecated"):
|
|
mc = RFDETRBaseConfig(num_classes=80, cls_loss_coef=2.5)
|
|
tc = TrainConfig(dataset_dir="/tmp")
|
|
ns = _namespace_from_configs(mc, tc)
|
|
assert ns.cls_loss_coef == pytest.approx(2.5)
|
|
|
|
# --- num_select must come from ModelConfig unconditionally ---
|
|
|
|
def test_num_select_from_model_config(self) -> None:
|
|
"""ns.num_select must equal mc.num_select regardless of tc.num_select."""
|
|
mc = RFDETRSegNanoConfig() # num_select=100
|
|
tc = TrainConfig(dataset_dir="/tmp") # num_select=300 (default — was the bug)
|
|
ns = _namespace_from_configs(mc, tc)
|
|
assert ns.num_select == 100
|
|
|
|
@pytest.mark.parametrize(
|
|
"config_class, expected_num_select",
|
|
[
|
|
pytest.param(RFDETRSegNanoConfig, 100, id="seg_nano"),
|
|
pytest.param(RFDETRBaseConfig, 300, id="base"),
|
|
],
|
|
)
|
|
def test_num_select_matches_model_config_variant(self, config_class, expected_num_select) -> None:
|
|
"""ns.num_select must equal the model config's num_select for each variant."""
|
|
mc = config_class()
|
|
tc = TrainConfig(dataset_dir="/tmp")
|
|
ns = _namespace_from_configs(mc, tc)
|
|
assert ns.num_select == expected_num_select
|
|
|
|
|
|
class TestBuildNamespaceDeprecated:
|
|
"""build_namespace() is a deprecated shim — verify the warning fires."""
|
|
|
|
def test_emits_deprecation_warning(self, reset_build_namespace_warning_state) -> None:
|
|
"""Every call to build_namespace() must emit a DeprecationWarning."""
|
|
from rfdetr._namespace import build_namespace
|
|
|
|
mc = RFDETRBaseConfig(num_classes=80)
|
|
tc = TrainConfig(dataset_dir="/tmp")
|
|
|
|
with pytest.warns(FutureWarning, match="build_namespace"):
|
|
build_namespace(mc, tc)
|
|
|
|
def test_result_identical_to_namespace_from_configs(self, reset_build_namespace_warning_state) -> None:
|
|
"""build_namespace output must equal _namespace_from_configs output."""
|
|
from rfdetr._namespace import build_namespace
|
|
from rfdetr.models._defaults import MODEL_DEFAULTS
|
|
|
|
mc = RFDETRBaseConfig(num_classes=80)
|
|
tc = TrainConfig(dataset_dir="/tmp")
|
|
|
|
with pytest.warns(FutureWarning):
|
|
ns_legacy = build_namespace(mc, tc)
|
|
ns_new = _namespace_from_configs(mc, tc, MODEL_DEFAULTS)
|
|
|
|
legacy_attrs = vars(ns_legacy)
|
|
new_attrs = vars(ns_new)
|
|
|
|
assert set(legacy_attrs.keys()) == set(new_attrs.keys()), (
|
|
f"Key mismatch: "
|
|
f"legacy_only={set(legacy_attrs) - set(new_attrs)}, "
|
|
f"new_only={set(new_attrs) - set(legacy_attrs)}"
|
|
)
|
|
for key in sorted(legacy_attrs):
|
|
assert legacy_attrs[key] == new_attrs[key], (
|
|
f"Value mismatch for '{key}': legacy={legacy_attrs[key]!r}, new={new_attrs[key]!r}"
|
|
)
|