"""Tests for soup bench CLI command.""" from typer.testing import CliRunner from soup_cli.cli import app runner = CliRunner() def test_bench_model_not_found(): """soup bench with nonexistent model should fail gracefully.""" result = runner.invoke(app, ["bench", "nonexistent_model_path"]) assert result.exit_code == 1 assert "not found" in result.output.lower() def test_bench_custom_prompts(tmp_path, monkeypatch): """Test using custom prompts from a text file and JSONL.""" monkeypatch.chdir(tmp_path) dummy_model = tmp_path / "dummy_model" dummy_model.mkdir() # Text file prompts_txt = tmp_path / "prompts.txt" prompts_txt.write_text("Custom prompt 1\nCustom prompt 2\n") # JSONL file prompts_jsonl = tmp_path / "prompts.jsonl" prompts_jsonl.write_text('{"prompt": "JSON prompt 1"}\n{"prompt": "JSON prompt 2"}\n') # Path traversal outside_file = tmp_path.parent / "outside.txt" outside_file.write_text("Outside\n") from unittest.mock import patch with patch("soup_cli.commands.infer._load_model") as mock_load, \ patch("soup_cli.commands.infer._generate") as mock_generate: mock_load.return_value = ("mock_model", "mock_tokenizer") mock_generate.return_value = (None, 10) # Test 1: TXT -- verify exit, output, and that actual prompts were passed to _generate result = runner.invoke(app, ["bench", str(dummy_model), "--prompts-file", "prompts.txt"]) assert result.exit_code == 0, (result.output, repr(result.exception)) assert "Running 2 test inferences" in result.output used_contents = [ call.args[2][0]["content"] for call in mock_generate.call_args_list ] assert "Custom prompt 1" in used_contents assert "Custom prompt 2" in used_contents mock_generate.reset_mock() # Test 2: JSONL -- verify JSON prompt field was extracted and used result = runner.invoke(app, ["bench", str(dummy_model), "--prompts-file", "prompts.jsonl"]) assert result.exit_code == 0, (result.output, repr(result.exception)) assert "Running 2 test inferences" in result.output used_contents = [ call.args[2][0]["content"] for call in mock_generate.call_args_list ] assert "JSON prompt 1" in used_contents assert "JSON prompt 2" in used_contents # Test 3: Path outside CWD -- security check result = runner.invoke( app, ["bench", str(dummy_model), "--prompts-file", str(outside_file)] ) assert result.exit_code == 1 assert "Security Error" in result.output def test_bench_happy_path(tmp_path, monkeypatch): """Happy path on CUDA: verify panel, table, TPS, and VRAM rendering end-to-end.""" monkeypatch.chdir(tmp_path) dummy_model = tmp_path / "dummy_model" dummy_model.mkdir() from unittest.mock import patch with patch("soup_cli.commands.infer._load_model") as mock_load, \ patch("soup_cli.commands.infer._generate") as mock_generate, \ patch("torch.cuda.is_available") as mock_is_available, \ patch("torch.cuda.reset_peak_memory_stats"), \ patch("torch.cuda.max_memory_allocated") as mock_max_memory, \ patch("soup_cli.utils.gpu.detect_device") as mock_detect_device: mock_load.return_value = ("mock_model", "mock_tokenizer") mock_generate.return_value = ("mock response", 128) mock_is_available.return_value = True mock_max_memory.return_value = 4 * 1024**3 # 4 GB mock_detect_device.return_value = ("cuda", 0) result = runner.invoke(app, ["bench", str(dummy_model)]) assert result.exit_code == 0, (result.output, repr(result.exception)) # Panel rendered assert "Benchmarking Configuration" in result.output # Results table rendered with expected columns assert "Inference Benchmark Results" in result.output assert "TPS (Avg)" in result.output # Token count propagated from mocked _generate assert "128 tokens" in result.output # VRAM value derived from mocked max_memory_allocated (4 GB) assert "4.00 GB" in result.output # Warmup + main loop: mock_generate called (warmup + num_prompts=3) assert mock_generate.call_count == 1 + 3 def test_bench_cpu_warning(tmp_path, monkeypatch): """CPU path: warning shown and VRAM column falls back to N/A.""" monkeypatch.chdir(tmp_path) dummy_model = tmp_path / "dummy_model" dummy_model.mkdir() from unittest.mock import patch with patch("soup_cli.commands.infer._load_model") as mock_load, \ patch("soup_cli.commands.infer._generate") as mock_generate, \ patch("torch.cuda.is_available") as mock_is_available, \ patch("soup_cli.utils.gpu.detect_device") as mock_detect_device: mock_load.return_value = ("mock_model", "mock_tokenizer") mock_generate.return_value = ("mock response", 10) mock_is_available.return_value = False mock_detect_device.return_value = ("cpu", None) result = runner.invoke(app, ["bench", str(dummy_model)]) assert result.exit_code == 0, (result.output, repr(result.exception)) assert "Running on CPU" in result.output assert "Inference Benchmark Results" in result.output # Without CUDA, VRAM column shows N/A assert "N/A" in result.output