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
176 lines
6.5 KiB
Python
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)
|