mirror of https://github.com/razor-ai/soup.git
109 lines
3.6 KiB
Python
109 lines
3.6 KiB
Python
"""Tests for v0.34.0 Part F — auto-profiling."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from soup_cli.utils.profiling import (
|
|
DEFAULT_ACTIVE_STEPS,
|
|
MAX_ACTIVE_STEPS,
|
|
ProfilerSchedule,
|
|
profile_training,
|
|
resolve_trace_path,
|
|
)
|
|
|
|
|
|
class TestSchedule:
|
|
def test_default(self):
|
|
schedule = ProfilerSchedule.default()
|
|
assert schedule.active == DEFAULT_ACTIVE_STEPS
|
|
schedule.validate() # should not raise
|
|
|
|
def test_zero_active_rejected(self):
|
|
with pytest.raises(ValueError, match="active"):
|
|
ProfilerSchedule(1, 1, 0, 1).validate()
|
|
|
|
def test_negative_rejected(self):
|
|
with pytest.raises(ValueError, match="non-negative"):
|
|
ProfilerSchedule(-1, 1, 5, 1).validate()
|
|
|
|
def test_active_cap(self):
|
|
with pytest.raises(ValueError, match="exceeds cap"):
|
|
ProfilerSchedule(1, 1, MAX_ACTIVE_STEPS + 1, 1).validate()
|
|
|
|
def test_frozen(self):
|
|
schedule = ProfilerSchedule.default()
|
|
with pytest.raises(Exception):
|
|
schedule.active = 99 # type: ignore[misc]
|
|
|
|
|
|
class TestResolvePath:
|
|
def test_returns_path(self, tmp_path, monkeypatch):
|
|
monkeypatch.chdir(tmp_path)
|
|
path = resolve_trace_path(tmp_path / "out", "run_123_abc")
|
|
assert path.name == "run_123_abc.trace.json"
|
|
assert path.parent.name == "profiles"
|
|
|
|
def test_dotdot_rejected(self, tmp_path, monkeypatch):
|
|
monkeypatch.chdir(tmp_path)
|
|
with pytest.raises(ValueError):
|
|
resolve_trace_path(tmp_path, "..")
|
|
with pytest.raises(ValueError):
|
|
resolve_trace_path(tmp_path, ".")
|
|
|
|
def test_backslash_rejected(self, tmp_path, monkeypatch):
|
|
monkeypatch.chdir(tmp_path)
|
|
with pytest.raises(ValueError):
|
|
resolve_trace_path(tmp_path, "run\\x")
|
|
|
|
def test_empty_rejected(self, tmp_path, monkeypatch):
|
|
monkeypatch.chdir(tmp_path)
|
|
with pytest.raises(ValueError):
|
|
resolve_trace_path(tmp_path, "")
|
|
|
|
def test_run_id_with_slash_rejected(self, tmp_path, monkeypatch):
|
|
monkeypatch.chdir(tmp_path)
|
|
with pytest.raises(ValueError, match="separator"):
|
|
resolve_trace_path(tmp_path, "run/escaped")
|
|
|
|
def test_run_id_null_byte_rejected(self, tmp_path, monkeypatch):
|
|
monkeypatch.chdir(tmp_path)
|
|
with pytest.raises(ValueError):
|
|
resolve_trace_path(tmp_path, "run\x00x")
|
|
|
|
def test_outside_cwd_rejected(self, tmp_path, monkeypatch):
|
|
monkeypatch.chdir(tmp_path)
|
|
outside = tmp_path.parent / "outside"
|
|
with pytest.raises(ValueError, match="not under cwd"):
|
|
resolve_trace_path(outside, "run_x")
|
|
|
|
|
|
class TestProfileContext:
|
|
def test_no_torch_yields_none(self, tmp_path, monkeypatch):
|
|
monkeypatch.chdir(tmp_path)
|
|
# Force the deferred import to raise ImportError.
|
|
import builtins
|
|
|
|
real_import = builtins.__import__
|
|
|
|
def fake_import(name, *args, **kwargs):
|
|
if name in ("torch.profiler", "torch"):
|
|
raise ImportError("forced")
|
|
return real_import(name, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(builtins, "__import__", fake_import)
|
|
with profile_training(
|
|
output_dir=tmp_path / "out", run_id="run_x"
|
|
) as profiler:
|
|
assert profiler is None
|
|
|
|
def test_invalid_schedule_propagates(self, tmp_path, monkeypatch):
|
|
monkeypatch.chdir(tmp_path)
|
|
with pytest.raises(ValueError):
|
|
with profile_training(
|
|
output_dir=tmp_path / "out",
|
|
run_id="run_x",
|
|
schedule=ProfilerSchedule(1, 1, 0, 1),
|
|
):
|
|
pass
|