Files
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

176 lines
6.5 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 the ``_safe_torch_load`` helper in ``rfdetr.util.io``.
Covers the three-stage safe-load strategy: strict weights_only, safe-globals fallback, and opt-in pickle fallback.
"""
from __future__ import annotations
import argparse
from pathlib import Path
from types import SimpleNamespace
import pytest
import torch
from rfdetr.util.io import _safe_torch_load
# ---------------------------------------------------------------------------
# Fixtures / helpers
# ---------------------------------------------------------------------------
def _write_tensor_only_checkpoint(path: Path) -> None:
"""Save a checkpoint containing only tensors and plain dicts to *path*."""
ckpt = {"model": {"weight": torch.tensor([1.0, 2.0]), "bias": torch.tensor([0.0])}}
torch.save(ckpt, path)
def _write_namespace_checkpoint(path: Path) -> None:
"""Save a checkpoint with an ``argparse.Namespace`` args value to *path*.
Legacy RF-DETR engine.py checkpoints embed a Namespace; strict ``weights_only=True`` (without safe globals) would
reject these.
"""
ckpt = {
"model": {"weight": torch.tensor([1.0])},
"args": argparse.Namespace(pretrain_weights="rf-detr-small.pth", num_classes=80),
}
torch.save(ckpt, path)
def _write_simple_namespace_checkpoint(path: Path) -> None:
"""Save a checkpoint with a ``types.SimpleNamespace`` to *path*."""
ckpt = {
"model": {"weight": torch.tensor([1.0])},
"args": SimpleNamespace(pretrain_weights="rf-detr-small.pth"),
}
torch.save(ckpt, path)
class _ArbitraryObject:
"""Module-level object that torch.save can pickle but weights_only=True rejects.
Must be defined at module scope so pickle can resolve its fully-qualified name during serialisation (local/nested
classes cannot be pickled by torch.save).
"""
value = 42
def _write_arbitrary_pickle_checkpoint(path: Path) -> None:
"""Save a checkpoint that embeds an arbitrary class (requires pickle)."""
ckpt = {"model": {"weight": torch.tensor([1.0])}, "extra": _ArbitraryObject()}
torch.save(ckpt, path)
# ---------------------------------------------------------------------------
# Safe path (weights_only=True)
# ---------------------------------------------------------------------------
class TestSafeTorchLoadSafePath:
"""Tensor-only checkpoints load without trust=True."""
def test_tensor_only_checkpoint_loads(self, tmp_path: Path) -> None:
"""Pure-tensor checkpoint succeeds on the first safe-load attempt."""
ckpt_path = tmp_path / "ckpt.pth"
_write_tensor_only_checkpoint(ckpt_path)
result = _safe_torch_load(ckpt_path)
assert "model" in result
assert torch.allclose(result["model"]["weight"], torch.tensor([1.0, 2.0]))
def test_accepts_pathlib_path(self, tmp_path: Path) -> None:
"""Helper accepts a :class:`pathlib.Path` argument without error."""
ckpt_path = tmp_path / "ckpt.pth"
_write_tensor_only_checkpoint(ckpt_path)
result = _safe_torch_load(ckpt_path)
assert "model" in result
def test_accepts_string_path(self, tmp_path: Path) -> None:
"""Helper accepts a :class:`str` path argument without error."""
ckpt_path = tmp_path / "ckpt.pth"
_write_tensor_only_checkpoint(ckpt_path)
result = _safe_torch_load(str(ckpt_path))
assert "model" in result
# ---------------------------------------------------------------------------
# Safe-globals fallback (legacy Namespace checkpoints)
# ---------------------------------------------------------------------------
class TestSafeTorchLoadSafeGlobals:
"""Checkpoints with argparse.Namespace / SimpleNamespace load without trust=True."""
def test_argparse_namespace_loads_without_trust(self, tmp_path: Path) -> None:
"""argparse.Namespace checkpoint succeeds via the safe-globals retry."""
ckpt_path = tmp_path / "ckpt.pth"
_write_namespace_checkpoint(ckpt_path)
result = _safe_torch_load(ckpt_path)
assert isinstance(result["args"], argparse.Namespace)
assert result["args"].num_classes == 80
def test_simple_namespace_loads_without_trust(self, tmp_path: Path) -> None:
"""SimpleNamespace checkpoint succeeds via the safe-globals retry."""
ckpt_path = tmp_path / "ckpt.pth"
_write_simple_namespace_checkpoint(ckpt_path)
result = _safe_torch_load(ckpt_path)
assert isinstance(result["args"], SimpleNamespace)
# ---------------------------------------------------------------------------
# Arbitrary pickle — trust=False must raise, trust=True must succeed
# ---------------------------------------------------------------------------
class TestSafeTorchLoadTrustGate:
"""Arbitrary-pickle checkpoints require explicit trust=True."""
def test_arbitrary_pickle_raises_without_trust(self, tmp_path: Path) -> None:
"""Checkpoint with unknown Python object raises RuntimeError when trust=False."""
ckpt_path = tmp_path / "ckpt.pth"
_write_arbitrary_pickle_checkpoint(ckpt_path)
with pytest.raises(RuntimeError, match="trust_checkpoint=True"):
_safe_torch_load(ckpt_path, trust=False)
def test_arbitrary_pickle_raises_by_default(self, tmp_path: Path) -> None:
"""Checkpoint with unknown Python object raises RuntimeError when trust omitted (default=False)."""
ckpt_path = tmp_path / "ckpt.pth"
_write_arbitrary_pickle_checkpoint(ckpt_path)
with pytest.raises(RuntimeError, match="trust_checkpoint=True"):
_safe_torch_load(ckpt_path)
def test_arbitrary_pickle_succeeds_with_trust(self, tmp_path: Path) -> None:
"""Checkpoint with unknown Python object loads when trust=True."""
ckpt_path = tmp_path / "ckpt.pth"
_write_arbitrary_pickle_checkpoint(ckpt_path)
result = _safe_torch_load(ckpt_path, trust=True)
assert "model" in result
def test_trust_true_emits_warning(self, tmp_path: Path) -> None:
"""Trust=True triggers a UserWarning about unsafe loading."""
ckpt_path = tmp_path / "ckpt.pth"
_write_arbitrary_pickle_checkpoint(ckpt_path)
with pytest.warns(UserWarning, match="weights_only=False"):
_safe_torch_load(ckpt_path, trust=True)