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

230 lines
8.8 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 TensorRT export helpers."""
import argparse
import subprocess
import pytest
from rfdetr.export import _tensorrt as tensorrt_export
def test_run_command_shell_dry_run_handles_missing_cuda_visible_devices(monkeypatch) -> None:
"""Dry-run logging should not crash when CUDA_VISIBLE_DEVICES is unset."""
monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising=False)
logged_messages = []
monkeypatch.setattr(tensorrt_export.logger, "info", logged_messages.append)
result = tensorrt_export.run_command_shell(["trtexec", "--help"], dry_run=True)
assert result.returncode == 0
assert any("CUDA_VISIBLE_DEVICES=" in message for message in logged_messages)
def test_run_command_shell_uses_list_not_string(monkeypatch) -> None:
"""subprocess.run must be called with a list (shell=False) to prevent injection."""
captured = {}
def _fake_run(command, shell, capture_output, text, check):
captured["command"] = command
captured["shell"] = shell
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
monkeypatch.setattr(tensorrt_export.subprocess, "run", _fake_run)
tensorrt_export.run_command_shell(["trtexec", "--onnx=/some/model.onnx"], dry_run=False)
assert isinstance(captured["command"], list), "command must be a list, not a string"
assert captured["shell"] is False, "shell=False is required to prevent injection"
def test_run_command_shell_dry_run_does_not_invoke_subprocess(monkeypatch) -> None:
"""Dry-run must return early without calling subprocess.run."""
was_called = []
def _should_not_run(*args, **kwargs):
was_called.append(True)
return subprocess.CompletedProcess([], 0)
monkeypatch.setattr(tensorrt_export.subprocess, "run", _should_not_run)
monkeypatch.setattr(tensorrt_export.logger, "info", lambda _: None)
result = tensorrt_export.run_command_shell(["trtexec", "--help"], dry_run=True)
assert not was_called, "subprocess.run must not be called during dry_run"
assert result.returncode == 0
def test_trtexec_returns_engine_path(monkeypatch) -> None:
"""Trtexec() must return the .engine path, not None."""
captured_argv = []
def _fake_run(command, **kwargs):
captured_argv.extend(command)
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
monkeypatch.setattr(tensorrt_export.subprocess, "run", _fake_run)
monkeypatch.setattr(tensorrt_export, "parse_trtexec_output", lambda _: {})
args = argparse.Namespace(profile=False, verbose=False, dry_run=False)
result = tensorrt_export.trtexec("/tmp/model.onnx", args)
assert result == "/tmp/model.engine"
def test_trtexec_dry_run_returns_engine_path(monkeypatch) -> None:
"""Trtexec() with dry_run=True must still return the engine path."""
monkeypatch.setattr(tensorrt_export.logger, "info", lambda _: None)
monkeypatch.setattr(tensorrt_export, "parse_trtexec_output", lambda _: {})
args = argparse.Namespace(profile=False, verbose=False, dry_run=True)
result = tensorrt_export.trtexec("/tmp/model.onnx", args)
assert result == "/tmp/model.engine"
@pytest.mark.parametrize(
("onnx_path", "expected_engine"),
[
pytest.param("/output/rfdetr.onnx", "/output/rfdetr.engine", id="plain-path"),
pytest.param("/path with spaces/model.onnx", "/path with spaces/model.engine", id="path-with-spaces"),
pytest.param("/model;rm -rf /.onnx", "/model;rm -rf /.engine", id="shell-metachar"),
],
)
def test_trtexec_argv_contains_no_shell_string(monkeypatch, onnx_path: str, expected_engine: str) -> None:
"""Trtexec builds an argv list; no shell string concatenation of user paths."""
captured = {}
def _fake_run(command, shell, **kwargs):
captured["command"] = command
captured["shell"] = shell
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
monkeypatch.setattr(tensorrt_export.subprocess, "run", _fake_run)
monkeypatch.setattr(tensorrt_export, "parse_trtexec_output", lambda _: {})
args = argparse.Namespace(profile=False, verbose=False, dry_run=False)
result = tensorrt_export.trtexec(onnx_path, args)
assert result == expected_engine
assert isinstance(captured["command"], list), "argv must be a list"
assert captured["shell"] is False, "shell=False required"
# Verify the ONNX path appears as a standalone argument element (not shell-expanded)
assert any(onnx_path in arg for arg in captured["command"])
# ---------------------------------------------------------------------------
# parse_trtexec_output (#1)
# ---------------------------------------------------------------------------
_FULL_TRTEXEC_STDOUT = """\
[I] GPU Compute Time: min = 1.23 ms, max = 4.56 ms, mean = 2.34 ms, median = 2.10 ms
[I] Host to Device Transfer Time: min = 0.10 ms, max = 0.20 ms, mean = 0.15 ms
[I] Device to Host Transfer Time: min = 0.05 ms, max = 0.08 ms, mean = 0.06 ms
[I] Latency: min = 1.40 ms, max = 4.80 ms, mean = 2.55 ms
[I] Throughput: 391.22 qps
"""
_PARTIAL_TRTEXEC_STDOUT = """\
[I] GPU Compute Time: min = 1.00 ms, max = 2.00 ms, mean = 1.50 ms, median = 1.45 ms
[I] Throughput: 100.00 qps
"""
@pytest.mark.parametrize(
("output_text", "expected"),
[
pytest.param(
_FULL_TRTEXEC_STDOUT,
{
"compute_min_ms": 1.23,
"compute_max_ms": 4.56,
"compute_mean_ms": 2.34,
"compute_median_ms": 2.10,
"h2d_min_ms": 0.10,
"h2d_max_ms": 0.20,
"h2d_mean_ms": 0.15,
"d2h_min_ms": 0.05,
"d2h_max_ms": 0.08,
"d2h_mean_ms": 0.06,
"latency_min_ms": 1.40,
"latency_max_ms": 4.80,
"latency_mean_ms": 2.55,
"throughput_qps": 391.22,
},
id="all-5-patterns",
),
pytest.param(
"",
{},
id="empty-stdout",
),
pytest.param(
_PARTIAL_TRTEXEC_STDOUT,
{
"compute_min_ms": 1.00,
"compute_max_ms": 2.00,
"compute_mean_ms": 1.50,
"compute_median_ms": 1.45,
"throughput_qps": 100.00,
},
id="partial-stdout",
),
],
)
def test_parse_trtexec_output(output_text: str, expected: dict) -> None:
"""parse_trtexec_output extracts timing statistics from trtexec stdout."""
result = tensorrt_export.parse_trtexec_output(output_text)
assert result == pytest.approx(expected, abs=1e-6)
# ---------------------------------------------------------------------------
# CalledProcessError logging path (#15)
# ---------------------------------------------------------------------------
def test_run_command_shell_called_process_error_is_reraised(monkeypatch) -> None:
"""CalledProcessError from subprocess.run is re-raised after logging."""
error_messages = []
def _fake_run(command, **kwargs):
raise subprocess.CalledProcessError(returncode=1, cmd=["trtexec"], stderr="engine build failed")
monkeypatch.setattr(tensorrt_export.subprocess, "run", _fake_run)
monkeypatch.setattr(tensorrt_export.logger, "error", error_messages.append)
with pytest.raises(subprocess.CalledProcessError):
tensorrt_export.run_command_shell(["trtexec", "--onnx=/tmp/model.onnx"], dry_run=False)
assert error_messages, "logger.error must be called when CalledProcessError is raised"
# ---------------------------------------------------------------------------
# profile=True argv path (#17)
# ---------------------------------------------------------------------------
def test_trtexec_profile_true_wraps_with_nsys(monkeypatch) -> None:
"""Profile=True wraps trtexec with 'nsys profile …' and the output flag is present."""
captured_argv: list[str] = []
def _fake_run(command, **kwargs):
captured_argv.extend(command)
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
monkeypatch.setattr(tensorrt_export.subprocess, "run", _fake_run)
monkeypatch.setattr(tensorrt_export, "parse_trtexec_output", lambda _: {})
monkeypatch.setattr(tensorrt_export.logger, "info", lambda _: None)
args = argparse.Namespace(profile=True, verbose=False, dry_run=False)
tensorrt_export.trtexec("/tmp/model.onnx", args)
assert captured_argv[0] == "nsys", "profile=True must wrap with nsys as argv[0]"
argv_str = " ".join(captured_argv)
assert "--output=" in argv_str, "nsys profile must include --output= flag"