""" Tests for benchmarks/comparison/evaluator.py Tests cover: - compare_configurations function - _calculate_average_metrics function - _evaluate_single_configuration function structure - Visualization creation functions """ from unittest.mock import patch import pytest class TestCompareConfigurationsValidation: """Tests for compare_configurations input validation.""" @patch("local_deep_research.benchmarks.comparison.evaluator.os.makedirs") def test_returns_error_for_empty_configurations( self, mock_makedirs, tmp_path ): """Test that empty configurations list returns error.""" from local_deep_research.benchmarks.comparison.evaluator import ( compare_configurations, ) result = compare_configurations( query="test query", configurations=[], output_dir=str(tmp_path), ) assert "error" in result assert result["error"] == "No configurations provided" @patch("local_deep_research.benchmarks.comparison.evaluator.os.makedirs") def test_creates_output_directory(self, mock_makedirs): """Test that output directory is created.""" from local_deep_research.benchmarks.comparison.evaluator import ( compare_configurations, ) # Empty configs will return early with error but still create dir compare_configurations( query="test", configurations=[], output_dir="/tmp/test_output", ) mock_makedirs.assert_called_with("/tmp/test_output", exist_ok=True) class TestCalculateAverageMetrics: """Tests for _calculate_average_metrics function.""" def test_calculates_average_quality_metrics(self): """Test averaging of quality metrics.""" from local_deep_research.benchmarks.comparison.evaluator import ( _calculate_average_metrics, ) results = [ {"quality_metrics": {"accuracy": 0.8, "source_count": 10}}, {"quality_metrics": {"accuracy": 0.6, "source_count": 8}}, ] avg = _calculate_average_metrics(results) assert avg["quality_metrics"]["accuracy"] == pytest.approx(0.7) assert avg["quality_metrics"]["source_count"] == pytest.approx(9.0) def test_calculates_average_speed_metrics(self): """Test averaging of speed metrics.""" from local_deep_research.benchmarks.comparison.evaluator import ( _calculate_average_metrics, ) results = [ { "speed_metrics": { "total_duration": 100, "duration_per_question": 10, } }, { "speed_metrics": { "total_duration": 80, "duration_per_question": 8, } }, ] avg = _calculate_average_metrics(results) assert avg["speed_metrics"]["total_duration"] == pytest.approx(90.0) assert avg["speed_metrics"]["duration_per_question"] == pytest.approx( 9.0 ) def test_calculates_average_resource_metrics(self): """Test averaging of resource metrics.""" from local_deep_research.benchmarks.comparison.evaluator import ( _calculate_average_metrics, ) results = [ {"resource_metrics": {"memory_mb": 100}}, {"resource_metrics": {"memory_mb": 200}}, ] avg = _calculate_average_metrics(results) assert avg["resource_metrics"]["memory_mb"] == pytest.approx(150.0) def test_handles_empty_results(self): """Test handling of empty results list.""" from local_deep_research.benchmarks.comparison.evaluator import ( _calculate_average_metrics, ) avg = _calculate_average_metrics([]) assert avg == {} def test_handles_missing_metrics_categories(self): """Test handling of results with missing metric categories.""" from local_deep_research.benchmarks.comparison.evaluator import ( _calculate_average_metrics, ) results = [ {"quality_metrics": {"accuracy": 0.8}}, {}, # No metrics ] avg = _calculate_average_metrics(results) # Should still have quality_metrics assert "quality_metrics" in avg assert avg["quality_metrics"]["accuracy"] == pytest.approx(0.8) def test_handles_none_values(self): """Test handling of None values in metrics.""" from local_deep_research.benchmarks.comparison.evaluator import ( _calculate_average_metrics, ) results = [ {"quality_metrics": {"accuracy": 0.8, "optional": None}}, {"quality_metrics": {"accuracy": 0.6, "optional": 0.5}}, ] avg = _calculate_average_metrics(results) # None values should be filtered out assert avg["quality_metrics"]["accuracy"] == pytest.approx(0.7) # Optional should only have one valid value assert avg["quality_metrics"]["optional"] == pytest.approx(0.5) def test_handles_mixed_metric_keys(self): """Test handling when results have different metric keys.""" from local_deep_research.benchmarks.comparison.evaluator import ( _calculate_average_metrics, ) results = [ {"quality_metrics": {"metric_a": 0.8}}, {"quality_metrics": {"metric_b": 0.6}}, ] avg = _calculate_average_metrics(results) # Both metrics should be present assert avg["quality_metrics"]["metric_a"] == pytest.approx(0.8) assert avg["quality_metrics"]["metric_b"] == pytest.approx(0.6) class TestMetricWeightsDefault: """Tests for default metric weights.""" @patch("local_deep_research.benchmarks.comparison.evaluator.os.makedirs") def test_default_metric_weights(self, mock_makedirs): """Test that default metric weights are applied.""" from local_deep_research.benchmarks.comparison.evaluator import ( compare_configurations, ) compare_configurations( query="test", configurations=[], output_dir="/tmp/test", ) # Empty configs returns early, but we can verify the function signature assert callable(compare_configurations) class TestVisualizationFunctions: """Tests for visualization helper functions.""" def test_create_metric_comparison_chart_exists(self): """Test that _create_metric_comparison_chart function exists.""" from local_deep_research.benchmarks.comparison.evaluator import ( _create_metric_comparison_chart, ) assert callable(_create_metric_comparison_chart) def test_create_spider_chart_exists(self): """Test that _create_spider_chart function exists.""" from local_deep_research.benchmarks.comparison.evaluator import ( _create_spider_chart, ) assert callable(_create_spider_chart) def test_create_pareto_chart_exists(self): """Test that _create_pareto_chart function exists.""" from local_deep_research.benchmarks.comparison.evaluator import ( _create_pareto_chart, ) assert callable(_create_pareto_chart) def test_create_comparison_visualizations_exists(self): """Test that _create_comparison_visualizations function exists.""" from local_deep_research.benchmarks.comparison.evaluator import ( _create_comparison_visualizations, ) assert callable(_create_comparison_visualizations) class TestEvaluateSingleConfiguration: """Tests for _evaluate_single_configuration function.""" def test_evaluate_single_configuration_exists(self): """Test that _evaluate_single_configuration function exists.""" from local_deep_research.benchmarks.comparison.evaluator import ( _evaluate_single_configuration, ) assert callable(_evaluate_single_configuration) def test_function_signature(self): """Test function has expected parameters.""" from local_deep_research.benchmarks.comparison.evaluator import ( _evaluate_single_configuration, ) import inspect sig = inspect.signature(_evaluate_single_configuration) params = list(sig.parameters.keys()) assert "query" in params assert "config" in params class TestConfigurationResultStructure: """Tests for expected result structure.""" @patch("local_deep_research.benchmarks.comparison.evaluator.os.makedirs") def test_compare_returns_dict(self, mock_makedirs): """Test that compare_configurations returns a dictionary.""" from local_deep_research.benchmarks.comparison.evaluator import ( compare_configurations, ) result = compare_configurations( query="test", configurations=[], output_dir="/tmp/test", ) assert isinstance(result, dict) @patch("local_deep_research.benchmarks.comparison.evaluator.os.makedirs") def test_error_result_has_error_key(self, mock_makedirs): """Test that error results have 'error' key.""" from local_deep_research.benchmarks.comparison.evaluator import ( compare_configurations, ) result = compare_configurations( query="test", configurations=[], output_dir="/tmp/test", ) assert "error" in result class TestCompareConfigurationsWithMocks: """Tests for compare_configurations with full mocking.""" @patch( "local_deep_research.benchmarks.comparison.evaluator._evaluate_single_configuration" ) @patch( "local_deep_research.benchmarks.comparison.evaluator._create_comparison_visualizations" ) @patch( "local_deep_research.benchmarks.comparison.evaluator.calculate_combined_score" ) @patch( "local_deep_research.security.file_write_verifier.write_json_verified" ) @patch("local_deep_research.benchmarks.comparison.evaluator.os.makedirs") def test_compare_single_configuration( self, mock_makedirs, mock_write, mock_combined, mock_viz, mock_eval ): """Test comparing a single configuration.""" from local_deep_research.benchmarks.comparison.evaluator import ( compare_configurations, ) mock_combined.return_value = 0.8 mock_eval.return_value = { "success": True, "quality_metrics": {"overall_quality": 0.8}, "speed_metrics": {"total_duration": 10.0}, "resource_metrics": {}, } result = compare_configurations( query="test query", configurations=[{"name": "Config 1", "iterations": 2}], output_dir="/tmp/test", repetitions=1, ) assert result["configurations_tested"] == 1 assert result["successful_configurations"] == 1 assert len(result["results"]) == 1 @patch( "local_deep_research.benchmarks.comparison.evaluator._evaluate_single_configuration" ) @patch( "local_deep_research.benchmarks.comparison.evaluator._create_comparison_visualizations" ) @patch( "local_deep_research.benchmarks.comparison.evaluator.calculate_combined_score" ) @patch( "local_deep_research.security.file_write_verifier.write_json_verified" ) @patch("local_deep_research.benchmarks.comparison.evaluator.os.makedirs") def test_compare_multiple_configurations( self, mock_makedirs, mock_write, mock_combined, mock_viz, mock_eval ): """Test comparing multiple configurations.""" from local_deep_research.benchmarks.comparison.evaluator import ( compare_configurations, ) mock_combined.return_value = 0.8 mock_eval.side_effect = [ { "success": True, "quality_metrics": {"overall_quality": 0.8}, "speed_metrics": {"total_duration": 10.0}, "resource_metrics": {}, }, { "success": True, "quality_metrics": {"overall_quality": 0.7}, "speed_metrics": {"total_duration": 15.0}, "resource_metrics": {}, }, ] result = compare_configurations( query="test", configurations=[ {"name": "Config 1"}, {"name": "Config 2"}, ], output_dir="/tmp/test", ) assert result["configurations_tested"] == 2 assert result["successful_configurations"] == 2 @patch( "local_deep_research.benchmarks.comparison.evaluator._evaluate_single_configuration" ) @patch( "local_deep_research.benchmarks.comparison.evaluator._create_comparison_visualizations" ) @patch( "local_deep_research.benchmarks.comparison.evaluator.calculate_combined_score" ) @patch( "local_deep_research.security.file_write_verifier.write_json_verified" ) @patch("local_deep_research.benchmarks.comparison.evaluator.os.makedirs") def test_compare_handles_failed_configuration( self, mock_makedirs, mock_write, mock_combined, mock_viz, mock_eval ): """Test handling of failed configuration.""" from local_deep_research.benchmarks.comparison.evaluator import ( compare_configurations, ) mock_combined.return_value = 0.0 mock_eval.return_value = { "success": False, "error": "Config failed", } result = compare_configurations( query="test", configurations=[{"name": "Failing Config"}], output_dir="/tmp/test", ) assert result["failed_configurations"] == 1 assert result["successful_configurations"] == 0 @patch( "local_deep_research.benchmarks.comparison.evaluator._evaluate_single_configuration" ) @patch( "local_deep_research.benchmarks.comparison.evaluator._create_comparison_visualizations" ) @patch( "local_deep_research.benchmarks.comparison.evaluator.calculate_combined_score" ) @patch( "local_deep_research.security.file_write_verifier.write_json_verified" ) @patch("local_deep_research.benchmarks.comparison.evaluator.os.makedirs") def test_compare_with_multiple_repetitions( self, mock_makedirs, mock_write, mock_combined, mock_viz, mock_eval ): """Test compare with multiple repetitions per configuration.""" from local_deep_research.benchmarks.comparison.evaluator import ( compare_configurations, ) mock_combined.return_value = 0.8 # Three repetitions for one config mock_eval.side_effect = [ { "success": True, "quality_metrics": {"overall_quality": 0.8}, "speed_metrics": {"total_duration": 10.0}, "resource_metrics": {}, }, { "success": True, "quality_metrics": {"overall_quality": 0.85}, "speed_metrics": {"total_duration": 9.0}, "resource_metrics": {}, }, { "success": True, "quality_metrics": {"overall_quality": 0.75}, "speed_metrics": {"total_duration": 11.0}, "resource_metrics": {}, }, ] result = compare_configurations( query="test", configurations=[{"name": "Config 1"}], output_dir="/tmp/test", repetitions=3, ) assert result["repetitions"] == 3 assert result["results"][0]["runs_completed"] == 3 @patch( "local_deep_research.benchmarks.comparison.evaluator._evaluate_single_configuration" ) @patch( "local_deep_research.benchmarks.comparison.evaluator._create_comparison_visualizations" ) @patch( "local_deep_research.benchmarks.comparison.evaluator.calculate_combined_score" ) @patch( "local_deep_research.security.file_write_verifier.write_json_verified" ) @patch("local_deep_research.benchmarks.comparison.evaluator.os.makedirs") def test_compare_custom_metric_weights( self, mock_makedirs, mock_write, mock_combined, mock_viz, mock_eval ): """Test compare with custom metric weights.""" from local_deep_research.benchmarks.comparison.evaluator import ( compare_configurations, ) mock_combined.return_value = 0.8 mock_eval.return_value = { "success": True, "quality_metrics": {"overall_quality": 0.8}, "speed_metrics": {"total_duration": 10.0}, "resource_metrics": {}, } custom_weights = {"quality": 0.8, "speed": 0.2, "resource": 0.0} result = compare_configurations( query="test", configurations=[{"name": "Config 1"}], output_dir="/tmp/test", metric_weights=custom_weights, ) assert result["metric_weights"] == custom_weights @patch( "local_deep_research.benchmarks.comparison.evaluator._evaluate_single_configuration" ) @patch( "local_deep_research.benchmarks.comparison.evaluator._create_comparison_visualizations" ) @patch( "local_deep_research.benchmarks.comparison.evaluator.calculate_combined_score" ) @patch( "local_deep_research.security.file_write_verifier.write_json_verified" ) @patch("local_deep_research.benchmarks.comparison.evaluator.os.makedirs") def test_results_sorted_by_score_descending( self, mock_makedirs, mock_write, mock_combined, mock_viz, mock_eval ): """Test that results are sorted by score in descending order.""" from local_deep_research.benchmarks.comparison.evaluator import ( compare_configurations, ) mock_combined.side_effect = [0.5, 0.9, 0.7] mock_eval.side_effect = [ { "success": True, "quality_metrics": {"overall_quality": 0.5}, "speed_metrics": {"total_duration": 10.0}, "resource_metrics": {}, }, { "success": True, "quality_metrics": {"overall_quality": 0.9}, "speed_metrics": {"total_duration": 10.0}, "resource_metrics": {}, }, { "success": True, "quality_metrics": {"overall_quality": 0.7}, "speed_metrics": {"total_duration": 10.0}, "resource_metrics": {}, }, ] result = compare_configurations( query="test", configurations=[ {"name": "Low"}, {"name": "High"}, {"name": "Mid"}, ], output_dir="/tmp/test", ) # Successful results should be sorted by score descending successful = [r for r in result["results"] if r.get("success")] scores = [r.get("overall_score", 0) for r in successful] assert scores == sorted(scores, reverse=True) class TestEvaluateSingleConfigurationFull: """Full tests for _evaluate_single_configuration function.""" from unittest.mock import Mock @patch("local_deep_research.benchmarks.comparison.evaluator.get_llm") @patch("local_deep_research.benchmarks.comparison.evaluator.get_search") @patch( "local_deep_research.benchmarks.comparison.evaluator.AdvancedSearchSystem" ) @patch("local_deep_research.benchmarks.comparison.evaluator.SpeedProfiler") @patch( "local_deep_research.benchmarks.comparison.evaluator.ResourceMonitor" ) @patch( "local_deep_research.benchmarks.comparison.evaluator.calculate_quality_metrics" ) @patch( "local_deep_research.benchmarks.comparison.evaluator.calculate_speed_metrics" ) @patch( "local_deep_research.benchmarks.comparison.evaluator.calculate_resource_metrics" ) def test_successful_evaluation( self, mock_res_metrics, mock_speed_metrics, mock_quality_metrics, mock_res_monitor, mock_profiler, mock_search_system, mock_get_search, mock_get_llm, ): """Test successful configuration evaluation.""" from local_deep_research.benchmarks.comparison.evaluator import ( _evaluate_single_configuration, ) from unittest.mock import Mock # Setup mocks mock_llm = Mock() mock_get_llm.return_value = mock_llm mock_search = Mock() mock_get_search.return_value = mock_search mock_system = Mock() mock_system.analyze_topic.return_value = { "findings": [{"phase": 1, "content": "test"}], "current_knowledge": "Test knowledge", } mock_system.all_links_of_system = ["http://example.com"] mock_search_system.return_value = mock_system mock_profiler_instance = Mock() mock_profiler_instance.timer.return_value.__enter__ = Mock() mock_profiler_instance.timer.return_value.__exit__ = Mock( return_value=False ) mock_profiler_instance.get_summary.return_value = {} mock_profiler_instance.get_timings.return_value = {} mock_profiler.return_value = mock_profiler_instance mock_res_monitor_instance = Mock() mock_res_monitor_instance.get_combined_stats.return_value = {} mock_res_monitor.return_value = mock_res_monitor_instance mock_quality_metrics.return_value = {"overall_quality": 0.8} mock_speed_metrics.return_value = {"total_duration": 10.0} mock_res_metrics.return_value = {} config = {"iterations": 2, "search_strategy": "iterdrag"} result = _evaluate_single_configuration( query="test query", config=config, ) assert result["success"] is True assert "quality_metrics" in result assert "speed_metrics" in result @patch("local_deep_research.benchmarks.comparison.evaluator.get_llm") @patch("local_deep_research.benchmarks.comparison.evaluator.SpeedProfiler") @patch( "local_deep_research.benchmarks.comparison.evaluator.ResourceMonitor" ) def test_evaluation_handles_llm_error( self, mock_res_monitor, mock_profiler, mock_get_llm, ): """Test that evaluation handles LLM initialization errors.""" from local_deep_research.benchmarks.comparison.evaluator import ( _evaluate_single_configuration, ) from unittest.mock import Mock mock_get_llm.side_effect = Exception("LLM init failed") mock_profiler_instance = Mock() mock_profiler_instance.timer.return_value.__enter__ = Mock() mock_profiler_instance.timer.return_value.__exit__ = Mock( return_value=False ) mock_profiler_instance.get_timings.return_value = {} mock_profiler.return_value = mock_profiler_instance mock_res_monitor_instance = Mock() mock_res_monitor_instance.get_combined_stats.return_value = {} mock_res_monitor.return_value = mock_res_monitor_instance config = {"iterations": 2} result = _evaluate_single_configuration( query="test", config=config, ) assert result["success"] is False assert "error" in result @patch("local_deep_research.benchmarks.comparison.evaluator.get_llm") @patch("local_deep_research.benchmarks.comparison.evaluator.get_search") @patch( "local_deep_research.benchmarks.comparison.evaluator.AdvancedSearchSystem" ) @patch("local_deep_research.benchmarks.comparison.evaluator.SpeedProfiler") @patch( "local_deep_research.benchmarks.comparison.evaluator.ResourceMonitor" ) @patch( "local_deep_research.benchmarks.comparison.evaluator.calculate_quality_metrics" ) @patch( "local_deep_research.benchmarks.comparison.evaluator.calculate_speed_metrics" ) @patch( "local_deep_research.benchmarks.comparison.evaluator.calculate_resource_metrics" ) def test_evaluation_uses_config_parameters( self, mock_res_metrics, mock_speed_metrics, mock_quality_metrics, mock_res_monitor, mock_profiler, mock_search_system, mock_get_search, mock_get_llm, ): """Test that configuration parameters are applied correctly.""" from local_deep_research.benchmarks.comparison.evaluator import ( _evaluate_single_configuration, ) from unittest.mock import Mock mock_llm = Mock() mock_get_llm.return_value = mock_llm mock_search = Mock() mock_get_search.return_value = mock_search mock_system = Mock() mock_system.analyze_topic.return_value = { "findings": [], "current_knowledge": "", } mock_search_system.return_value = mock_system mock_profiler_instance = Mock() mock_profiler_instance.timer.return_value.__enter__ = Mock() mock_profiler_instance.timer.return_value.__exit__ = Mock( return_value=False ) mock_profiler_instance.get_summary.return_value = {} mock_profiler_instance.get_timings.return_value = {} mock_profiler.return_value = mock_profiler_instance mock_res_monitor_instance = Mock() mock_res_monitor_instance.get_combined_stats.return_value = {} mock_res_monitor.return_value = mock_res_monitor_instance mock_quality_metrics.return_value = {} mock_speed_metrics.return_value = {} mock_res_metrics.return_value = {} config = { "iterations": 5, "questions_per_iteration": 4, "search_strategy": "focused_iteration", } _evaluate_single_configuration( query="test", config=config, ) # Verify system was configured with our parameters call_kwargs = mock_search_system.call_args[1] assert call_kwargs["max_iterations"] == 5 assert call_kwargs["questions_per_iteration"] == 4 assert call_kwargs["strategy_name"] == "focused_iteration" class TestVisualizationCreation: """Tests for visualization creation functions.""" from unittest.mock import Mock @patch("local_deep_research.benchmarks.comparison.evaluator.plt") def test_create_comparison_visualizations_no_successful(self, mock_plt): """Test visualization with no successful results.""" from local_deep_research.benchmarks.comparison.evaluator import ( _create_comparison_visualizations, ) report = {"results": [{"success": False}]} # Should not raise _create_comparison_visualizations( report, output_dir="/tmp/test", timestamp="20240101" ) @patch("local_deep_research.benchmarks.comparison.evaluator.plt") def test_create_metric_comparison_chart_single_metric(self, mock_plt): """Test metric comparison chart with single metric.""" from local_deep_research.benchmarks.comparison.evaluator import ( _create_metric_comparison_chart, ) from unittest.mock import Mock, MagicMock mock_fig = Mock() mock_ax = MagicMock() mock_plt.subplots.return_value = (mock_fig, mock_ax) # barh returns a BarContainer which is iterable; mock it with a list # of mock bars that support get_width/get_y/get_height mock_bar = Mock() mock_bar.get_width.return_value = 0.8 mock_bar.get_y.return_value = 0.0 mock_bar.get_height.return_value = 0.5 mock_ax.barh.return_value = [mock_bar] results = [ { "name": "Config 1", "avg_metrics": {"quality_metrics": {"overall_quality": 0.8}}, } ] _create_metric_comparison_chart( results, ["Config 1"], ["overall_quality"], "quality_metrics", "Test", "/tmp/test.png", ) mock_plt.savefig.assert_called() @patch("local_deep_research.benchmarks.comparison.evaluator.plt") def test_create_pareto_chart_with_data(self, mock_plt): """Test pareto chart creation with data.""" from local_deep_research.benchmarks.comparison.evaluator import ( _create_pareto_chart, ) from unittest.mock import Mock mock_fig = Mock() mock_ax = Mock() mock_plt.subplots.return_value = (mock_fig, mock_ax) mock_plt.figure.return_value = mock_fig results = [ { "name": "Config 1", "avg_metrics": { "quality_metrics": {"overall_quality": 0.8}, "speed_metrics": {"total_duration": 10.0}, }, }, { "name": "Config 2", "avg_metrics": { "quality_metrics": {"overall_quality": 0.6}, "speed_metrics": {"total_duration": 5.0}, }, }, ] _create_pareto_chart(results, "/tmp/pareto.png") mock_plt.savefig.assert_called() @patch("local_deep_research.benchmarks.comparison.evaluator.plt") def test_create_comparison_visualizations_creates_files(self, mock_plt): """Test that visualizations create output files.""" from local_deep_research.benchmarks.comparison.evaluator import ( _create_comparison_visualizations, ) from unittest.mock import Mock, MagicMock def _make_mock_ax(): """Create a mock ax that supports barh returning iterable bars.""" ax = MagicMock() bar = Mock() bar.get_width.return_value = 0.5 bar.get_y.return_value = 0.0 bar.get_height.return_value = 0.5 ax.barh.return_value = [bar] return ax mock_fig = Mock() def _subplots_side_effect(*args, **kwargs): """Return (fig, axes) with correct number of axes.""" nrows = args[0] if args else kwargs.get("nrows", 1) if nrows == 1: return (mock_fig, _make_mock_ax()) return (mock_fig, [_make_mock_ax() for _ in range(nrows)]) mock_plt.subplots.side_effect = _subplots_side_effect mock_plt.figure.return_value = mock_fig report = { "results": [ { "name": "Config 1", "success": True, "overall_score": 0.8, "avg_metrics": { "quality_metrics": {"overall_quality": 0.8}, "speed_metrics": {"total_duration": 10.0}, "resource_metrics": {}, }, } ] } _create_comparison_visualizations( report, output_dir="/tmp/test", timestamp="20240101" ) # Should have called savefig multiple times assert mock_plt.savefig.called