mirror of https://github.com/razor-ai/soup.git
183 lines
5.9 KiB
Python
183 lines
5.9 KiB
Python
"""Tests for soup runs CLI commands."""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from typer.testing import CliRunner
|
|
|
|
from soup_cli.cli import app
|
|
from soup_cli.experiment.tracker import ExperimentTracker
|
|
|
|
runner = CliRunner()
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _use_temp_db(tmp_path: Path, monkeypatch):
|
|
"""Redirect experiment DB to a temp directory for all tests."""
|
|
db_path = str(tmp_path / "test_experiments.db")
|
|
monkeypatch.setenv("SOUP_DB_PATH", db_path)
|
|
|
|
|
|
@pytest.fixture
|
|
def tracker(tmp_path: Path) -> ExperimentTracker:
|
|
"""Create tracker using the same temp DB."""
|
|
import os
|
|
|
|
db_path = Path(os.environ["SOUP_DB_PATH"])
|
|
return ExperimentTracker(db_path=db_path)
|
|
|
|
|
|
def test_runs_empty():
|
|
"""soup runs with no runs should show a message, not crash."""
|
|
result = runner.invoke(app, ["runs"])
|
|
assert result.exit_code == 0
|
|
assert "No runs found" in result.output
|
|
|
|
|
|
def test_runs_list_with_data(tracker):
|
|
"""soup runs should show a table when runs exist."""
|
|
run_id = tracker.start_run(
|
|
config_dict={"base": "test-model", "task": "sft"},
|
|
device="cpu",
|
|
device_name="CPU",
|
|
gpu_info={"memory_total": "N/A"},
|
|
experiment_name="test-exp",
|
|
)
|
|
result = runner.invoke(app, ["runs"])
|
|
assert result.exit_code == 0
|
|
assert "Training Runs" in result.output
|
|
# Run ID column has no_wrap so it should always be visible
|
|
assert run_id in result.output
|
|
|
|
|
|
def test_runs_show_not_found():
|
|
"""soup runs show nonexistent should fail."""
|
|
result = runner.invoke(app, ["runs", "show", "nonexistent_run_id"])
|
|
assert result.exit_code == 1
|
|
assert "not found" in result.output.lower()
|
|
|
|
|
|
def test_runs_show_with_data(tracker):
|
|
"""soup runs show should display run details."""
|
|
run_id = tracker.start_run(
|
|
config_dict={"base": "test-model", "task": "sft"},
|
|
device="cpu",
|
|
device_name="CPU",
|
|
gpu_info={"memory_total": "16 GB"},
|
|
)
|
|
tracker.finish_run(
|
|
run_id=run_id,
|
|
initial_loss=2.5,
|
|
final_loss=0.8,
|
|
total_steps=100,
|
|
duration_secs=600.0,
|
|
output_dir="/tmp/output",
|
|
)
|
|
result = runner.invoke(app, ["runs", "show", run_id, "--no-plot"])
|
|
assert result.exit_code == 0
|
|
assert run_id in result.output
|
|
assert "test-model" in result.output
|
|
|
|
|
|
def test_runs_compare_not_found():
|
|
"""soup runs compare with bad IDs should fail."""
|
|
result = runner.invoke(app, ["runs", "compare", "run_1", "run_2"])
|
|
assert result.exit_code == 1
|
|
|
|
|
|
def test_runs_compare_with_data(tracker):
|
|
"""soup runs compare should show side-by-side table."""
|
|
id1 = tracker.start_run(
|
|
config_dict={"base": "model-a", "task": "sft", "training": {"epochs": 3, "lr": 2e-5}},
|
|
device="cpu", device_name="CPU", gpu_info={},
|
|
experiment_name="exp-a",
|
|
)
|
|
id2 = tracker.start_run(
|
|
config_dict={"base": "model-b", "task": "dpo", "training": {"epochs": 5, "lr": 1e-5}},
|
|
device="cuda", device_name="RTX 4090", gpu_info={},
|
|
experiment_name="exp-b",
|
|
)
|
|
result = runner.invoke(app, ["runs", "compare", id1, id2])
|
|
assert result.exit_code == 0
|
|
assert "model-a" in result.output
|
|
assert "model-b" in result.output
|
|
|
|
|
|
def test_runs_delete_not_found():
|
|
"""soup runs delete nonexistent should fail."""
|
|
result = runner.invoke(app, ["runs", "delete", "nonexistent", "--force"])
|
|
assert result.exit_code == 1
|
|
|
|
|
|
def test_runs_delete_with_data(tracker):
|
|
"""soup runs delete should remove the run."""
|
|
run_id = tracker.start_run(
|
|
config_dict={}, device="cpu", device_name="CPU", gpu_info={},
|
|
)
|
|
result = runner.invoke(app, ["runs", "delete", run_id, "--force"])
|
|
assert result.exit_code == 0
|
|
assert "Deleted" in result.output
|
|
# Verify it's gone
|
|
assert tracker.get_run(run_id) is None
|
|
|
|
|
|
def test_runs_clean_not_found():
|
|
"""soup runs clean with bad IDs should fail."""
|
|
result = runner.invoke(app, ["runs", "clean", "nonexistent"])
|
|
assert result.exit_code == 1
|
|
|
|
|
|
def test_runs_clean_with_data(tracker, tmp_path):
|
|
"""soup runs clean should reclaim space natively."""
|
|
# Note: Using Path.cwd() / "test_output" to satisfy security check that paths stay under CWD
|
|
out_dir = Path.cwd() / "test_output_clean"
|
|
if out_dir.exists():
|
|
import shutil
|
|
shutil.rmtree(out_dir)
|
|
out_dir.mkdir(parents=True)
|
|
|
|
try:
|
|
# create some dummy checkpoints
|
|
ckpt1 = out_dir / "checkpoint-100"
|
|
ckpt1.mkdir()
|
|
(ckpt1 / "optimizer.pt").write_text("dummy")
|
|
(ckpt1 / "adapter_model.bin").write_text("model")
|
|
|
|
ckpt2 = out_dir / "checkpoint-200"
|
|
ckpt2.mkdir()
|
|
(ckpt2 / "optimizer.pt").write_text("dummy")
|
|
(ckpt2 / "adapter_model.bin").write_text("model")
|
|
|
|
run_id = tracker.start_run(
|
|
config_dict={}, device="cpu", device_name="CPU", gpu_info={},
|
|
)
|
|
tracker.finish_run(
|
|
run_id=run_id,
|
|
initial_loss=2.0,
|
|
final_loss=1.0, # let's say step 200 has lower loss
|
|
total_steps=200,
|
|
duration_secs=100.0,
|
|
output_dir=str(out_dir),
|
|
)
|
|
# mock metrics: step 200 is best
|
|
tracker.log_metrics(run_id, step=100, loss=2.0)
|
|
tracker.log_metrics(run_id, step=200, loss=1.0)
|
|
|
|
result = runner.invoke(app, ["runs", "clean", run_id, "--force"])
|
|
assert result.exit_code == 0
|
|
assert "Successfully reclaimed" in result.output
|
|
|
|
# verify optimizer in ckpt1 is gone, but in ckpt2 it's potentially kept or not
|
|
# based on keep-weights
|
|
assert not (ckpt1 / "optimizer.pt").exists()
|
|
assert (ckpt2 / "optimizer.pt").exists()
|
|
|
|
# test --all
|
|
result = runner.invoke(app, ["runs", "clean", "--all", "--force"])
|
|
assert "No disposable checkpoint files found" in result.output
|
|
finally:
|
|
import shutil
|
|
if out_dir.exists():
|
|
shutil.rmtree(out_dir)
|
|
|