""" PerformanceBenchmarker 的單元測試。 測試策略: - BenchmarkConfig / BenchmarkResult 資料結構驗證 - calculate_speedup() 純計算邏輯 - run_sequential_benchmark() / run_parallel_benchmark() 透過注入的 inference_runner callable 進行 Mock,不需要實際硬體 - run_full_benchmark() 整合流程 """ import time import pytest from unittest.mock import MagicMock, patch from core.performance.benchmarker import ( BenchmarkConfig, BenchmarkResult, PerformanceBenchmarker, ) # --------------------------------------------------------------------------- # 輔助:建立測試用資料結構 # --------------------------------------------------------------------------- def make_config(**kwargs) -> BenchmarkConfig: """建立測試用 BenchmarkConfig,提供合理的預設值。""" defaults = dict( pipeline_config=[], test_duration_seconds=1.0, warmup_frames=2, test_input_source="test_video.mp4", ) defaults.update(kwargs) return BenchmarkConfig(**defaults) def make_result(mode: str = "sequential", fps: float = 30.0) -> BenchmarkResult: """建立測試用 BenchmarkResult。""" avg_latency_ms = (1000.0 / fps) if fps > 0 else 0.0 return BenchmarkResult( mode=mode, fps=fps, avg_latency_ms=avg_latency_ms, p95_latency_ms=avg_latency_ms * 1.5, total_frames=int(fps * 30), timestamp=time.time(), device_config={"KL520": 1}, ) # --------------------------------------------------------------------------- # 測試:BenchmarkConfig 資料結構 # --------------------------------------------------------------------------- class TestBenchmarkConfig: def should_have_default_duration_30_seconds(self): """test_duration_seconds 預設值應為 30.0。""" config = BenchmarkConfig( pipeline_config=[], test_input_source="video.mp4", ) assert config.test_duration_seconds == 30.0 def should_have_default_warmup_50_frames(self): """warmup_frames 預設值應為 50。""" config = BenchmarkConfig( pipeline_config=[], test_input_source="video.mp4", ) assert config.warmup_frames == 50 def should_allow_custom_duration(self): """應可自訂 test_duration_seconds。""" config = BenchmarkConfig( pipeline_config=[], test_input_source="video.mp4", test_duration_seconds=10.0, ) assert config.test_duration_seconds == 10.0 # --------------------------------------------------------------------------- # 測試:BenchmarkResult 資料結構 # --------------------------------------------------------------------------- class TestBenchmarkResult: def should_store_all_required_fields(self): """BenchmarkResult 應儲存所有規格要求的欄位。""" ts = time.time() result = BenchmarkResult( mode="parallel", fps=45.2, avg_latency_ms=22.1, p95_latency_ms=35.0, total_frames=1356, timestamp=ts, device_config={"KL720": 2}, ) assert result.mode == "parallel" assert result.fps == pytest.approx(45.2) assert result.avg_latency_ms == pytest.approx(22.1) assert result.p95_latency_ms == pytest.approx(35.0) assert result.total_frames == 1356 assert result.timestamp == pytest.approx(ts) assert result.device_config == {"KL720": 2} def should_accept_sequential_mode(self): """mode 欄位應接受 'sequential'。""" result = make_result(mode="sequential") assert result.mode == "sequential" def should_accept_parallel_mode(self): """mode 欄位應接受 'parallel'。""" result = make_result(mode="parallel") assert result.mode == "parallel" # --------------------------------------------------------------------------- # 測試:calculate_speedup(純計算,無外部依賴) # --------------------------------------------------------------------------- class TestCalculateSpeedup: def should_return_ratio_of_parallel_to_sequential_fps(self): """calculate_speedup 應回傳 par.fps / seq.fps。""" benchmarker = PerformanceBenchmarker() seq = make_result(mode="sequential", fps=20.0) par = make_result(mode="parallel", fps=60.0) speedup = benchmarker.calculate_speedup(seq, par) assert speedup == pytest.approx(3.0) def should_return_one_when_same_fps(self): """相同 FPS 時 speedup 應為 1.0。""" benchmarker = PerformanceBenchmarker() result = make_result(fps=30.0) speedup = benchmarker.calculate_speedup(result, result) assert speedup == pytest.approx(1.0) def should_raise_when_sequential_fps_is_zero(self): """seq.fps 為 0 時應引發 ValueError,避免除以零。""" benchmarker = PerformanceBenchmarker() seq = make_result(fps=0.0) par = make_result(fps=30.0) with pytest.raises(ValueError): benchmarker.calculate_speedup(seq, par) # --------------------------------------------------------------------------- # 測試:run_sequential_benchmark(Mock inference_runner) # --------------------------------------------------------------------------- class TestRunSequentialBenchmark: def should_return_benchmark_result_with_sequential_mode(self): """run_sequential_benchmark() 應回傳 mode='sequential' 的 BenchmarkResult。""" benchmarker = PerformanceBenchmarker() config = make_config(warmup_frames=1, test_duration_seconds=0.1) # Mock inference_runner:每次呼叫模擬 10ms 推論 def fake_runner(frame_data): time.sleep(0.01) return {"result": "ok"} result = benchmarker.run_sequential_benchmark(config, inference_runner=fake_runner) assert isinstance(result, BenchmarkResult) assert result.mode == "sequential" def should_report_positive_fps(self): """FPS 應大於 0。""" benchmarker = PerformanceBenchmarker() config = make_config(warmup_frames=1, test_duration_seconds=0.1) def fake_runner(frame_data): time.sleep(0.01) return {} result = benchmarker.run_sequential_benchmark(config, inference_runner=fake_runner) assert result.fps > 0 def should_report_positive_latency(self): """avg_latency_ms 和 p95_latency_ms 應大於 0。""" benchmarker = PerformanceBenchmarker() config = make_config(warmup_frames=1, test_duration_seconds=0.1) def fake_runner(frame_data): time.sleep(0.01) return {} result = benchmarker.run_sequential_benchmark(config, inference_runner=fake_runner) assert result.avg_latency_ms > 0 assert result.p95_latency_ms > 0 def should_count_frames_excluding_warmup(self): """total_frames 不應包含暖機幀數。""" benchmarker = PerformanceBenchmarker() call_times = [] def fake_runner(frame_data): call_times.append(time.time()) time.sleep(0.005) return {} config = make_config(warmup_frames=3, test_duration_seconds=0.1) result = benchmarker.run_sequential_benchmark(config, inference_runner=fake_runner) # warmup 幀不計入 total_frames assert result.total_frames < len(call_times) assert result.total_frames > 0 def should_use_device_config_from_benchmarker(self): """BenchmarkResult.device_config 應由 PerformanceBenchmarker 填寫。""" benchmarker = PerformanceBenchmarker(device_config={"KL520": 1}) config = make_config(warmup_frames=1, test_duration_seconds=0.05) def fake_runner(frame_data): return {} result = benchmarker.run_sequential_benchmark(config, inference_runner=fake_runner) assert result.device_config == {"KL520": 1} # --------------------------------------------------------------------------- # 測試:run_parallel_benchmark(Mock inference_runner) # --------------------------------------------------------------------------- class TestRunParallelBenchmark: def should_return_benchmark_result_with_parallel_mode(self): """run_parallel_benchmark() 應回傳 mode='parallel' 的 BenchmarkResult。""" benchmarker = PerformanceBenchmarker() config = make_config(warmup_frames=1, test_duration_seconds=0.1) def fake_runner(frame_data): time.sleep(0.01) return {} result = benchmarker.run_parallel_benchmark(config, inference_runner=fake_runner) assert isinstance(result, BenchmarkResult) assert result.mode == "parallel" # --------------------------------------------------------------------------- # 測試:run_full_benchmark # --------------------------------------------------------------------------- class TestRunFullBenchmark: def should_return_tuple_of_seq_par_speedup(self): """run_full_benchmark() 應回傳 (BenchmarkResult, BenchmarkResult, float)。""" benchmarker = PerformanceBenchmarker() config = make_config(warmup_frames=1, test_duration_seconds=0.05) def fast_runner(frame_data): time.sleep(0.005) return {} seq_result, par_result, speedup = benchmarker.run_full_benchmark( config, inference_runner=fast_runner ) assert isinstance(seq_result, BenchmarkResult) assert isinstance(par_result, BenchmarkResult) assert isinstance(speedup, float) assert seq_result.mode == "sequential" assert par_result.mode == "parallel" def should_calculate_speedup_consistently(self): """speedup 應與 calculate_speedup(seq, par) 的結果一致。""" benchmarker = PerformanceBenchmarker() config = make_config(warmup_frames=1, test_duration_seconds=0.05) def fake_runner(frame_data): time.sleep(0.005) return {} seq_result, par_result, speedup = benchmarker.run_full_benchmark( config, inference_runner=fake_runner ) expected_speedup = benchmarker.calculate_speedup(seq_result, par_result) assert speedup == pytest.approx(expected_speedup)