chore: import upstream snapshot with attribution
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,404 @@
|
||||
"""
|
||||
Branch-coverage tests for benchmarks/optimization/optuna_optimizer.py.
|
||||
|
||||
Targets branches not fully exercised by the existing test files:
|
||||
- _get_default_param_space structure and types
|
||||
- optimize() raising KeyboardInterrupt
|
||||
- progress_callback invocation during optimize()
|
||||
- _save_results: joblib.dump called for study
|
||||
- _create_visualizations with PLOTTING_AVAILABLE=False
|
||||
- metric_weights normalisation when sum != 1.0
|
||||
"""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
MODULE = "local_deep_research.benchmarks.optimization.optuna_optimizer"
|
||||
|
||||
|
||||
def _make_optimizer(**kwargs):
|
||||
from local_deep_research.benchmarks.optimization.optuna_optimizer import (
|
||||
OptunaOptimizer,
|
||||
)
|
||||
|
||||
defaults = {"base_query": "branches coverage query"}
|
||||
defaults.update(kwargs)
|
||||
return OptunaOptimizer(**defaults)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _get_default_param_space
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetDefaultParamSpace:
|
||||
# test_get_default_param_space_iterations_is_int_type is defined first so it
|
||||
# runs first and warms up the expensive module import within the pytest-timeout
|
||||
# window before the other tests (including the bare test_get_default_param_space)
|
||||
# are collected.
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_get_default_param_space_iterations_is_int_type(
|
||||
self, mock_evaluator
|
||||
):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
space = optimizer._get_default_param_space()
|
||||
assert space["iterations"]["type"] == "int"
|
||||
assert space["iterations"]["low"] >= 1
|
||||
assert space["iterations"]["high"] >= space["iterations"]["low"]
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_get_default_param_space(self, mock_evaluator):
|
||||
"""_get_default_param_space returns a dict with the four expected keys."""
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
space = optimizer._get_default_param_space()
|
||||
assert isinstance(space, dict)
|
||||
assert "iterations" in space
|
||||
assert "questions_per_iteration" in space
|
||||
assert "search_strategy" in space
|
||||
assert "max_results" in space
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_get_default_param_space_search_strategy_is_categorical(
|
||||
self, mock_evaluator
|
||||
):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
space = optimizer._get_default_param_space()
|
||||
assert space["search_strategy"]["type"] == "categorical"
|
||||
choices = space["search_strategy"]["choices"]
|
||||
assert isinstance(choices, list)
|
||||
assert len(choices) > 0
|
||||
assert "source-based" in choices
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_get_default_param_space_max_results_step(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
space = optimizer._get_default_param_space()
|
||||
mr = space["max_results"]
|
||||
assert mr["type"] == "int"
|
||||
assert mr["low"] > 0
|
||||
assert mr["high"] > mr["low"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# optimize() – KeyboardInterrupt
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestOptimizeKeyboardInterrupt:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.optuna")
|
||||
def test_optimize_keyboard_interrupt(self, mock_optuna, mock_evaluator):
|
||||
"""When study.optimize raises KeyboardInterrupt, best_params and value are still returned."""
|
||||
mock_evaluator.return_value = Mock()
|
||||
mock_study = Mock()
|
||||
mock_study.best_params = {"iterations": 2, "max_results": 50}
|
||||
mock_study.best_value = 0.55
|
||||
mock_study.trials = [Mock(), Mock()]
|
||||
mock_study.optimize.side_effect = KeyboardInterrupt()
|
||||
mock_optuna.create_study.return_value = mock_study
|
||||
mock_optuna.samplers.TPESampler.return_value = Mock()
|
||||
|
||||
optimizer = _make_optimizer(n_trials=10)
|
||||
with (
|
||||
patch.object(optimizer, "_save_results") as mock_save,
|
||||
patch.object(optimizer, "_create_visualizations") as mock_viz,
|
||||
):
|
||||
best_params, best_value = optimizer.optimize()
|
||||
|
||||
assert best_params == {"iterations": 2, "max_results": 50}
|
||||
assert best_value == 0.55
|
||||
mock_save.assert_called_once()
|
||||
mock_viz.assert_called_once()
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.optuna")
|
||||
def test_optimize_keyboard_interrupt_with_callback(
|
||||
self, mock_optuna, mock_evaluator
|
||||
):
|
||||
"""KeyboardInterrupt fires an 'interrupted' status callback."""
|
||||
mock_evaluator.return_value = Mock()
|
||||
callback = Mock()
|
||||
mock_study = Mock()
|
||||
mock_study.best_params = {"iterations": 1}
|
||||
mock_study.best_value = 0.3
|
||||
mock_study.trials = [Mock(), Mock(), Mock()]
|
||||
mock_study.optimize.side_effect = KeyboardInterrupt()
|
||||
mock_optuna.create_study.return_value = mock_study
|
||||
mock_optuna.samplers.TPESampler.return_value = Mock()
|
||||
|
||||
optimizer = _make_optimizer(n_trials=5, progress_callback=callback)
|
||||
with (
|
||||
patch.object(optimizer, "_save_results"),
|
||||
patch.object(optimizer, "_create_visualizations"),
|
||||
):
|
||||
optimizer.optimize()
|
||||
|
||||
interrupted_calls = [
|
||||
c
|
||||
for c in callback.call_args_list
|
||||
if c[0][2].get("status") == "interrupted"
|
||||
]
|
||||
assert len(interrupted_calls) == 1
|
||||
info = interrupted_calls[0][0][2]
|
||||
assert info["stage"] == "interrupted"
|
||||
assert info["trials_completed"] == 3
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.optuna")
|
||||
def test_optimize_keyboard_interrupt_no_callback(
|
||||
self, mock_optuna, mock_evaluator
|
||||
):
|
||||
"""KeyboardInterrupt without a callback does not raise."""
|
||||
mock_evaluator.return_value = Mock()
|
||||
mock_study = Mock()
|
||||
mock_study.best_params = {"iterations": 1}
|
||||
mock_study.best_value = 0.1
|
||||
mock_study.trials = []
|
||||
mock_study.optimize.side_effect = KeyboardInterrupt()
|
||||
mock_optuna.create_study.return_value = mock_study
|
||||
mock_optuna.samplers.TPESampler.return_value = Mock()
|
||||
|
||||
optimizer = _make_optimizer(n_trials=3)
|
||||
assert optimizer.progress_callback is None
|
||||
with (
|
||||
patch.object(optimizer, "_save_results"),
|
||||
patch.object(optimizer, "_create_visualizations"),
|
||||
):
|
||||
params, value = optimizer.optimize()
|
||||
assert params == {"iterations": 1}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# optimize() – progress_callback invoked
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestOptimizationCallbackInvoked:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.optuna")
|
||||
def test_optimization_callback_invoked(self, mock_optuna, mock_evaluator):
|
||||
"""progress_callback is called with 'starting' status before study.optimize."""
|
||||
mock_evaluator.return_value = Mock()
|
||||
callback = Mock()
|
||||
mock_study = Mock()
|
||||
mock_study.best_params = {"iterations": 3}
|
||||
mock_study.best_value = 0.7
|
||||
mock_study.trials = [Mock()]
|
||||
mock_optuna.create_study.return_value = mock_study
|
||||
mock_optuna.samplers.TPESampler.return_value = Mock()
|
||||
|
||||
optimizer = _make_optimizer(n_trials=1, progress_callback=callback)
|
||||
with (
|
||||
patch.object(optimizer, "_save_results"),
|
||||
patch.object(optimizer, "_create_visualizations"),
|
||||
):
|
||||
optimizer.optimize()
|
||||
|
||||
# At least one call should have status 'starting'
|
||||
all_statuses = [c[0][2].get("status") for c in callback.call_args_list]
|
||||
assert "starting" in all_statuses
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.optuna")
|
||||
def test_optimization_callback_invoked_on_completion(
|
||||
self, mock_optuna, mock_evaluator
|
||||
):
|
||||
"""progress_callback is called with 'completed' status after study.optimize."""
|
||||
mock_evaluator.return_value = Mock()
|
||||
callback = Mock()
|
||||
mock_study = Mock()
|
||||
mock_study.best_params = {"max_results": 40}
|
||||
mock_study.best_value = 0.82
|
||||
mock_study.trials = [Mock(), Mock()]
|
||||
mock_optuna.create_study.return_value = mock_study
|
||||
mock_optuna.samplers.TPESampler.return_value = Mock()
|
||||
|
||||
optimizer = _make_optimizer(n_trials=2, progress_callback=callback)
|
||||
with (
|
||||
patch.object(optimizer, "_save_results"),
|
||||
patch.object(optimizer, "_create_visualizations"),
|
||||
):
|
||||
optimizer.optimize()
|
||||
|
||||
completed_calls = [
|
||||
c
|
||||
for c in callback.call_args_list
|
||||
if c[0][2].get("status") == "completed"
|
||||
]
|
||||
assert len(completed_calls) == 1
|
||||
info = completed_calls[0][0][2]
|
||||
assert info["best_value"] == 0.82
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _save_results – joblib.dump called
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSaveResultsJoblib:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.joblib")
|
||||
@patch(
|
||||
"local_deep_research.security.file_write_verifier.write_json_verified"
|
||||
)
|
||||
def test_save_results_joblib(
|
||||
self, mock_write_json, mock_joblib, mock_evaluator, tmp_path
|
||||
):
|
||||
"""_save_results calls joblib.dump to persist the study object."""
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer(output_dir=str(tmp_path))
|
||||
|
||||
mock_study = Mock()
|
||||
mock_study.best_params = {"iterations": 2}
|
||||
mock_study.best_value = 0.75
|
||||
mock_study.trials = [Mock()]
|
||||
optimizer.study = mock_study
|
||||
optimizer.trials_history = []
|
||||
|
||||
optimizer._save_results()
|
||||
|
||||
mock_joblib.dump.assert_called_once()
|
||||
# First arg to dump should be the study, second arg should be the file path
|
||||
call_args = mock_joblib.dump.call_args
|
||||
assert call_args[0][0] is mock_study
|
||||
assert str(call_args[0][1]).endswith(".pkl")
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.joblib")
|
||||
@patch(
|
||||
"local_deep_research.security.file_write_verifier.write_json_verified"
|
||||
)
|
||||
def test_save_results_joblib_not_called_without_study(
|
||||
self, mock_write_json, mock_joblib, mock_evaluator, tmp_path
|
||||
):
|
||||
"""joblib.dump is NOT called when study is None."""
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer(output_dir=str(tmp_path))
|
||||
optimizer.study = None
|
||||
optimizer.trials_history = []
|
||||
|
||||
optimizer._save_results()
|
||||
|
||||
mock_joblib.dump.assert_not_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _create_visualizations – PLOTTING_AVAILABLE=False
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCreateVisualizationsNoMatplotlib:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.PLOTTING_AVAILABLE", False)
|
||||
def test_create_visualizations_no_matplotlib(self, mock_evaluator):
|
||||
"""_create_visualizations returns early and never calls plot functions when PLOTTING_AVAILABLE=False."""
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
mock_study = Mock()
|
||||
mock_study.trials = [Mock(), Mock(), Mock()]
|
||||
optimizer.study = mock_study
|
||||
|
||||
with patch(f"{MODULE}.plot_optimization_history") as mock_plot:
|
||||
optimizer._create_visualizations()
|
||||
mock_plot.assert_not_called()
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.PLOTTING_AVAILABLE", False)
|
||||
def test_create_visualizations_no_matplotlib_does_not_raise(
|
||||
self, mock_evaluator
|
||||
):
|
||||
"""Calling _create_visualizations without matplotlib available does not raise."""
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
optimizer.study = Mock()
|
||||
optimizer.study.trials = [Mock(), Mock()]
|
||||
# Should complete without exception
|
||||
optimizer._create_visualizations()
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.PLOTTING_AVAILABLE", True)
|
||||
def test_create_visualizations_skips_when_no_study(self, mock_evaluator):
|
||||
"""_create_visualizations returns early when study is None."""
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
optimizer.study = None
|
||||
|
||||
with patch(f"{MODULE}.plot_optimization_history") as mock_plot:
|
||||
optimizer._create_visualizations()
|
||||
mock_plot.assert_not_called()
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.PLOTTING_AVAILABLE", True)
|
||||
def test_create_visualizations_skips_with_only_one_trial(
|
||||
self, mock_evaluator
|
||||
):
|
||||
"""_create_visualizations returns early when fewer than 2 trials are present."""
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
mock_study = Mock()
|
||||
mock_study.trials = [Mock()] # only 1 trial
|
||||
optimizer.study = mock_study
|
||||
|
||||
with patch(f"{MODULE}.plot_optimization_history") as mock_plot:
|
||||
optimizer._create_visualizations()
|
||||
mock_plot.assert_not_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# metric_weights normalisation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestWeightNormalization:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_weight_normalization(self, mock_evaluator):
|
||||
"""Weights that don't sum to 1.0 are normalised so the total becomes 1.0."""
|
||||
mock_evaluator.return_value = Mock()
|
||||
# Deliberately unbalanced weights
|
||||
optimizer = _make_optimizer(
|
||||
metric_weights={"quality": 3.0, "speed": 1.0}
|
||||
)
|
||||
total = sum(optimizer.metric_weights.values())
|
||||
assert abs(total - 1.0) < 1e-9
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_weight_normalization_proportions_preserved(self, mock_evaluator):
|
||||
"""After normalisation, the relative proportions remain correct."""
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer(
|
||||
metric_weights={"quality": 3.0, "speed": 1.0}
|
||||
)
|
||||
# quality was 3x speed, so after normalisation quality should be 0.75
|
||||
assert abs(optimizer.metric_weights["quality"] - 0.75) < 1e-9
|
||||
assert abs(optimizer.metric_weights["speed"] - 0.25) < 1e-9
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_weight_normalization_already_normalised(self, mock_evaluator):
|
||||
"""Weights already summing to 1.0 remain unchanged."""
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer(
|
||||
metric_weights={"quality": 0.6, "speed": 0.4}
|
||||
)
|
||||
assert abs(optimizer.metric_weights["quality"] - 0.6) < 1e-9
|
||||
assert abs(optimizer.metric_weights["speed"] - 0.4) < 1e-9
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_weight_normalization_three_metrics(self, mock_evaluator):
|
||||
"""Three-metric weights are also normalised correctly."""
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer(
|
||||
metric_weights={"quality": 4.0, "speed": 3.0, "resource": 3.0}
|
||||
)
|
||||
total = sum(optimizer.metric_weights.values())
|
||||
assert abs(total - 1.0) < 1e-9
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_default_weights_sum_to_one(self, mock_evaluator):
|
||||
"""Default metric_weights are already normalised."""
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
total = sum(optimizer.metric_weights.values())
|
||||
assert abs(total - 1.0) < 1e-9
|
||||
@@ -0,0 +1,420 @@
|
||||
"""
|
||||
Coverage tests for benchmarks/optimization/optuna_optimizer.py.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
MODULE = "local_deep_research.benchmarks.optimization.optuna_optimizer"
|
||||
|
||||
|
||||
def _make_optimizer(**kwargs):
|
||||
from local_deep_research.benchmarks.optimization.optuna_optimizer import (
|
||||
OptunaOptimizer,
|
||||
)
|
||||
|
||||
defaults = {"base_query": "coverage test query"}
|
||||
defaults.update(kwargs)
|
||||
return OptunaOptimizer(**defaults)
|
||||
|
||||
|
||||
class TestObjectiveFloatParamSuggestion:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_float_param_with_log_scale(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
mock_trial = Mock()
|
||||
mock_trial.number = 0
|
||||
mock_trial.suggest_float.return_value = 0.01
|
||||
param_space = {
|
||||
"lr": {"type": "float", "low": 0.0001, "high": 1.0, "log": True}
|
||||
}
|
||||
with patch.object(optimizer, "_run_experiment") as mock_run:
|
||||
mock_run.return_value = {"score": 0.77}
|
||||
score = optimizer._objective(mock_trial, param_space=param_space)
|
||||
mock_trial.suggest_float.assert_called_once_with(
|
||||
"lr", 0.0001, 1.0, step=None, log=True
|
||||
)
|
||||
assert score == 0.77
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_float_param_with_step_no_log(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
mock_trial = Mock()
|
||||
mock_trial.number = 1
|
||||
mock_trial.suggest_float.return_value = 0.5
|
||||
param_space = {
|
||||
"dropout": {"type": "float", "low": 0.0, "high": 1.0, "step": 0.1}
|
||||
}
|
||||
with patch.object(optimizer, "_run_experiment") as mock_run:
|
||||
mock_run.return_value = {"score": 0.65}
|
||||
score = optimizer._objective(mock_trial, param_space=param_space)
|
||||
mock_trial.suggest_float.assert_called_once_with(
|
||||
"dropout", 0.0, 1.0, step=0.1, log=False
|
||||
)
|
||||
assert score == 0.65
|
||||
|
||||
|
||||
class TestObjectiveProgressCallbacks:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_callback_trial_started_then_completed(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
callback = Mock()
|
||||
optimizer = _make_optimizer(progress_callback=callback, n_trials=5)
|
||||
mock_trial = Mock()
|
||||
mock_trial.number = 2
|
||||
mock_trial.suggest_int.return_value = 3
|
||||
mock_trial.suggest_categorical.return_value = "rapid"
|
||||
with patch.object(optimizer, "_run_experiment") as mock_run:
|
||||
mock_run.return_value = {"score": 0.88}
|
||||
param_space = optimizer._get_default_param_space()
|
||||
optimizer._objective(mock_trial, param_space=param_space)
|
||||
stages = [c[0][2]["stage"] for c in callback.call_args_list]
|
||||
assert "trial_started" in stages
|
||||
assert "trial_completed" in stages
|
||||
completed_call = [
|
||||
c
|
||||
for c in callback.call_args_list
|
||||
if c[0][2]["stage"] == "trial_completed"
|
||||
][0]
|
||||
assert completed_call[0][2]["score"] == 0.88
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_callback_trial_error_on_exception(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
callback = Mock()
|
||||
optimizer = _make_optimizer(progress_callback=callback, n_trials=5)
|
||||
mock_trial = Mock()
|
||||
mock_trial.number = 3
|
||||
mock_trial.suggest_int.return_value = 1
|
||||
mock_trial.suggest_categorical.return_value = "standard"
|
||||
with patch.object(optimizer, "_run_experiment") as mock_run:
|
||||
mock_run.side_effect = RuntimeError("timeout")
|
||||
param_space = optimizer._get_default_param_space()
|
||||
score = optimizer._objective(mock_trial, param_space=param_space)
|
||||
assert score == float("-inf")
|
||||
error_calls = [
|
||||
c
|
||||
for c in callback.call_args_list
|
||||
if c[0][2].get("stage") == "trial_error"
|
||||
]
|
||||
assert len(error_calls) == 1
|
||||
assert "timeout" in error_calls[0][0][2]["error"]
|
||||
|
||||
|
||||
class TestOptimizeKeyboardInterrupt:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.optuna")
|
||||
def test_keyboard_interrupt_saves_and_calls_callback(
|
||||
self, mock_optuna, mock_evaluator
|
||||
):
|
||||
mock_evaluator.return_value = Mock()
|
||||
callback = Mock()
|
||||
mock_study = Mock()
|
||||
mock_study.best_params = {"iterations": 2}
|
||||
mock_study.best_value = 0.45
|
||||
mock_study.trials = [Mock(), Mock(), Mock()]
|
||||
mock_study.optimize.side_effect = KeyboardInterrupt()
|
||||
mock_optuna.create_study.return_value = mock_study
|
||||
optimizer = _make_optimizer(n_trials=20, progress_callback=callback)
|
||||
with (
|
||||
patch.object(optimizer, "_save_results") as mock_save,
|
||||
patch.object(optimizer, "_create_visualizations") as mock_viz,
|
||||
):
|
||||
best_params, best_value = optimizer.optimize()
|
||||
mock_save.assert_called_once()
|
||||
mock_viz.assert_called_once()
|
||||
assert best_params == {"iterations": 2}
|
||||
assert best_value == 0.45
|
||||
interrupted = [
|
||||
c
|
||||
for c in callback.call_args_list
|
||||
if c[0][2].get("status") == "interrupted"
|
||||
]
|
||||
assert len(interrupted) == 1
|
||||
assert interrupted[0][0][2]["trials_completed"] == 3
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.optuna")
|
||||
def test_keyboard_interrupt_without_callback(
|
||||
self, mock_optuna, mock_evaluator
|
||||
):
|
||||
mock_evaluator.return_value = Mock()
|
||||
mock_study = Mock()
|
||||
mock_study.best_params = {"iterations": 1}
|
||||
mock_study.best_value = 0.1
|
||||
mock_study.trials = []
|
||||
mock_study.optimize.side_effect = KeyboardInterrupt()
|
||||
mock_optuna.create_study.return_value = mock_study
|
||||
optimizer = _make_optimizer(n_trials=5)
|
||||
assert optimizer.progress_callback is None
|
||||
with (
|
||||
patch.object(optimizer, "_save_results"),
|
||||
patch.object(optimizer, "_create_visualizations"),
|
||||
):
|
||||
best_params, best_value = optimizer.optimize()
|
||||
assert best_params == {"iterations": 1}
|
||||
|
||||
|
||||
class TestOptimizeCompletionCallback:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.optuna")
|
||||
def test_completion_callback_includes_best_params_and_value(
|
||||
self, mock_optuna, mock_evaluator
|
||||
):
|
||||
mock_evaluator.return_value = Mock()
|
||||
callback = Mock()
|
||||
mock_study = Mock()
|
||||
mock_study.best_params = {"iterations": 4, "max_results": 80}
|
||||
mock_study.best_value = 0.93
|
||||
mock_study.trials = [Mock(), Mock()]
|
||||
mock_optuna.create_study.return_value = mock_study
|
||||
optimizer = _make_optimizer(n_trials=2, progress_callback=callback)
|
||||
with (
|
||||
patch.object(optimizer, "_save_results"),
|
||||
patch.object(optimizer, "_create_visualizations"),
|
||||
):
|
||||
optimizer.optimize()
|
||||
completed = [
|
||||
c
|
||||
for c in callback.call_args_list
|
||||
if c[0][2].get("status") == "completed"
|
||||
and c[0][2].get("stage") == "finished"
|
||||
]
|
||||
assert len(completed) == 1
|
||||
info = completed[0][0][2]
|
||||
assert info["best_params"] == {"iterations": 4, "max_results": 80}
|
||||
assert info["best_value"] == 0.93
|
||||
assert info["trials_completed"] == 2
|
||||
|
||||
|
||||
class TestOptimizationCallbackStoresTrial:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_saves_at_trial_20(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
mock_study = Mock()
|
||||
mock_trial = Mock()
|
||||
mock_trial.number = 20
|
||||
with (
|
||||
patch.object(optimizer, "_save_results") as mock_save,
|
||||
patch.object(optimizer, "_create_quick_visualizations") as mock_viz,
|
||||
):
|
||||
optimizer._optimization_callback(mock_study, mock_trial)
|
||||
mock_save.assert_called_once()
|
||||
mock_viz.assert_called_once()
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_no_save_at_trial_5(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
mock_study = Mock()
|
||||
mock_trial = Mock()
|
||||
mock_trial.number = 5
|
||||
with patch.object(optimizer, "_save_results") as mock_save:
|
||||
optimizer._optimization_callback(mock_study, mock_trial)
|
||||
mock_save.assert_not_called()
|
||||
|
||||
|
||||
class TestCreateQuickVisualizations:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.PLOTTING_AVAILABLE", True)
|
||||
@patch(f"{MODULE}.plot_optimization_history")
|
||||
def test_quick_viz_with_sufficient_trials(
|
||||
self, mock_plot_history, mock_evaluator, tmp_path
|
||||
):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer(output_dir=str(tmp_path))
|
||||
mock_study = Mock()
|
||||
mock_study.trials = [Mock(), Mock(), Mock()]
|
||||
optimizer.study = mock_study
|
||||
mock_fig = Mock()
|
||||
mock_plot_history.return_value = mock_fig
|
||||
optimizer._create_quick_visualizations()
|
||||
mock_plot_history.assert_called_once_with(mock_study)
|
||||
mock_fig.write_image.assert_called_once()
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.PLOTTING_AVAILABLE", True)
|
||||
def test_quick_viz_returns_early_fewer_than_2_trials(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
mock_study = Mock()
|
||||
mock_study.trials = [Mock()]
|
||||
optimizer.study = mock_study
|
||||
with patch(f"{MODULE}.plot_optimization_history") as mock_plot:
|
||||
optimizer._create_quick_visualizations()
|
||||
mock_plot.assert_not_called()
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.PLOTTING_AVAILABLE", False)
|
||||
def test_quick_viz_returns_early_without_matplotlib(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
optimizer.study = Mock()
|
||||
optimizer.study.trials = [Mock(), Mock()]
|
||||
with patch(f"{MODULE}.plot_optimization_history") as mock_plot:
|
||||
optimizer._create_quick_visualizations()
|
||||
mock_plot.assert_not_called()
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.PLOTTING_AVAILABLE", True)
|
||||
def test_quick_viz_returns_early_when_no_study(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
optimizer.study = None
|
||||
with patch(f"{MODULE}.plot_optimization_history") as mock_plot:
|
||||
optimizer._create_quick_visualizations()
|
||||
mock_plot.assert_not_called()
|
||||
|
||||
|
||||
class TestSaveResultsNumpyConversion:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.joblib")
|
||||
@patch(
|
||||
"local_deep_research.security.file_write_verifier.write_json_verified"
|
||||
)
|
||||
def test_numpy_int64_top_level_converted(
|
||||
self, mock_write_json, mock_joblib, mock_evaluator, tmp_path
|
||||
):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer(output_dir=str(tmp_path))
|
||||
optimizer.study = None
|
||||
optimizer.trials_history = [
|
||||
{
|
||||
"trial_number": np.int64(0),
|
||||
"score": np.float64(0.92),
|
||||
"params": {"iterations": np.int64(3)},
|
||||
}
|
||||
]
|
||||
optimizer._save_results()
|
||||
assert mock_write_json.call_count == 1
|
||||
written_data = mock_write_json.call_args_list[0][0][1]
|
||||
assert isinstance(written_data[0]["trial_number"], float)
|
||||
assert isinstance(written_data[0]["score"], float)
|
||||
assert isinstance(written_data[0]["params"]["iterations"], float)
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.joblib")
|
||||
@patch(
|
||||
"local_deep_research.security.file_write_verifier.write_json_verified"
|
||||
)
|
||||
def test_numpy_float64_in_result_dict_converted(
|
||||
self, mock_write_json, mock_joblib, mock_evaluator, tmp_path
|
||||
):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer(output_dir=str(tmp_path))
|
||||
optimizer.study = None
|
||||
optimizer.trials_history = [
|
||||
{
|
||||
"trial_number": 0,
|
||||
"result": {
|
||||
"quality_score": np.float64(0.85),
|
||||
"speed_score": np.float64(0.72),
|
||||
},
|
||||
"params": {},
|
||||
"score": 0.8,
|
||||
}
|
||||
]
|
||||
optimizer._save_results()
|
||||
written_data = mock_write_json.call_args_list[0][0][1]
|
||||
result_dict = written_data[0]["result"]
|
||||
assert isinstance(result_dict["quality_score"], float)
|
||||
assert isinstance(result_dict["speed_score"], float)
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.joblib")
|
||||
@patch(
|
||||
"local_deep_research.security.file_write_verifier.write_json_verified"
|
||||
)
|
||||
def test_save_results_with_study_saves_best_params_and_pkl(
|
||||
self, mock_write_json, mock_joblib, mock_evaluator, tmp_path
|
||||
):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer(output_dir=str(tmp_path))
|
||||
mock_study = Mock()
|
||||
mock_study.best_params = {"iterations": 3}
|
||||
mock_study.best_value = 0.91
|
||||
mock_study.trials = [Mock()]
|
||||
optimizer.study = mock_study
|
||||
optimizer.trials_history = []
|
||||
optimizer._save_results()
|
||||
assert mock_write_json.call_count == 2
|
||||
mock_joblib.dump.assert_called_once()
|
||||
|
||||
|
||||
class TestRunExperimentErrorPaths:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.SpeedProfiler")
|
||||
def test_evaluator_error_returns_failure_stops_profiler(
|
||||
self, mock_profiler_cls, mock_evaluator
|
||||
):
|
||||
mock_eval_instance = Mock()
|
||||
mock_eval_instance.evaluate.side_effect = ValueError("bad config")
|
||||
mock_evaluator.return_value = mock_eval_instance
|
||||
mock_profiler = Mock()
|
||||
mock_profiler_cls.return_value = mock_profiler
|
||||
optimizer = _make_optimizer()
|
||||
result = optimizer._run_experiment(
|
||||
{"iterations": 2, "questions_per_iteration": 1}
|
||||
)
|
||||
assert result["success"] is False
|
||||
assert result["score"] == 0.0
|
||||
assert "bad config" in result["error"]
|
||||
mock_profiler.start.assert_called_once()
|
||||
mock_profiler.stop.assert_called_once()
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.SpeedProfiler")
|
||||
def test_profiler_get_summary_error_caught(
|
||||
self, mock_profiler_cls, mock_evaluator
|
||||
):
|
||||
mock_eval_instance = Mock()
|
||||
mock_eval_instance.evaluate.return_value = {
|
||||
"quality_score": 0.7,
|
||||
"benchmark_results": {},
|
||||
}
|
||||
mock_evaluator.return_value = mock_eval_instance
|
||||
mock_profiler = Mock()
|
||||
mock_profiler.get_summary.side_effect = RuntimeError("profiler broken")
|
||||
mock_profiler_cls.return_value = mock_profiler
|
||||
optimizer = _make_optimizer()
|
||||
result = optimizer._run_experiment({"iterations": 1})
|
||||
assert result["success"] is False
|
||||
assert result["score"] == 0.0
|
||||
assert "profiler broken" in result["error"]
|
||||
assert mock_profiler.stop.call_count >= 1
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.SpeedProfiler")
|
||||
def test_successful_experiment_returns_all_fields(
|
||||
self, mock_profiler_cls, mock_evaluator
|
||||
):
|
||||
mock_eval_instance = Mock()
|
||||
mock_eval_instance.evaluate.return_value = {
|
||||
"quality_score": 0.85,
|
||||
"benchmark_results": {"simpleqa": {"accuracy": 0.85}},
|
||||
}
|
||||
mock_evaluator.return_value = mock_eval_instance
|
||||
mock_profiler = Mock()
|
||||
mock_profiler.get_summary.return_value = {"total_duration": 120.0}
|
||||
mock_profiler_cls.return_value = mock_profiler
|
||||
optimizer = _make_optimizer(
|
||||
metric_weights={"quality": 0.6, "speed": 0.4}
|
||||
)
|
||||
result = optimizer._run_experiment(
|
||||
{
|
||||
"iterations": 2,
|
||||
"questions_per_iteration": 3,
|
||||
"search_strategy": "iterdrag",
|
||||
"max_results": 50,
|
||||
}
|
||||
)
|
||||
assert result["success"] is True
|
||||
assert result["quality_score"] == 0.85
|
||||
assert result["speed_score"] == pytest.approx(2 / 3, abs=0.01)
|
||||
assert result["total_duration"] == 120.0
|
||||
assert "score" in result
|
||||
assert "benchmark_results" in result
|
||||
@@ -0,0 +1,505 @@
|
||||
"""
|
||||
Extra coverage tests for benchmarks/optimization/optuna_optimizer.py.
|
||||
|
||||
Targets the 74 missing lines not covered by test_optuna_optimizer_coverage.py:
|
||||
- _get_default_param_space structure
|
||||
- _objective int/categorical param suggestion paths
|
||||
- _objective with sanitize_data
|
||||
- _run_experiment success with combined score
|
||||
- _save_results with/without study, numpy arrays
|
||||
- _create_visualizations paths (PLOTTING_AVAILABLE, trial counts)
|
||||
- optimize() starting callback and no-callback paths
|
||||
- _optimization_callback with study.best_value
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
MODULE = "local_deep_research.benchmarks.optimization.optuna_optimizer"
|
||||
|
||||
|
||||
def _make_optimizer(**kwargs):
|
||||
from local_deep_research.benchmarks.optimization.optuna_optimizer import (
|
||||
OptunaOptimizer,
|
||||
)
|
||||
|
||||
defaults = {"base_query": "extra coverage query"}
|
||||
defaults.update(kwargs)
|
||||
return OptunaOptimizer(**defaults)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _get_default_param_space
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetDefaultParamSpace:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_param_space_contains_required_keys(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
space = optimizer._get_default_param_space()
|
||||
assert "iterations" in space
|
||||
assert "questions_per_iteration" in space
|
||||
assert "search_strategy" in space
|
||||
assert "max_results" in space
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_iterations_is_int_type(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
space = optimizer._get_default_param_space()
|
||||
assert space["iterations"]["type"] == "int"
|
||||
assert space["iterations"]["low"] == 1
|
||||
assert space["iterations"]["high"] == 5
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_search_strategy_is_categorical_with_choices(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
space = optimizer._get_default_param_space()
|
||||
assert space["search_strategy"]["type"] == "categorical"
|
||||
assert "source-based" in space["search_strategy"]["choices"]
|
||||
assert "focused-iteration" in space["search_strategy"]["choices"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _objective – int and categorical suggestion paths
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestObjectiveParamSuggestionTypes:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_int_param_suggested_with_step(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
mock_trial = Mock()
|
||||
mock_trial.number = 0
|
||||
mock_trial.suggest_int.return_value = 3
|
||||
mock_trial.suggest_categorical.return_value = "rapid"
|
||||
with patch.object(optimizer, "_run_experiment") as mock_run:
|
||||
mock_run.return_value = {"score": 0.5}
|
||||
param_space = {
|
||||
"iterations": {"type": "int", "low": 1, "high": 5, "step": 1}
|
||||
}
|
||||
optimizer._objective(mock_trial, param_space=param_space)
|
||||
mock_trial.suggest_int.assert_called_once_with(
|
||||
"iterations", 1, 5, step=1
|
||||
)
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_int_param_suggested_without_step(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
mock_trial = Mock()
|
||||
mock_trial.number = 1
|
||||
mock_trial.suggest_int.return_value = 2
|
||||
with patch.object(optimizer, "_run_experiment") as mock_run:
|
||||
mock_run.return_value = {"score": 0.4}
|
||||
param_space = {"count": {"type": "int", "low": 1, "high": 10}}
|
||||
optimizer._objective(mock_trial, param_space=param_space)
|
||||
mock_trial.suggest_int.assert_called_once_with("count", 1, 10, step=1)
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_categorical_param_suggested(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
mock_trial = Mock()
|
||||
mock_trial.number = 2
|
||||
mock_trial.suggest_categorical.return_value = "standard"
|
||||
with patch.object(optimizer, "_run_experiment") as mock_run:
|
||||
mock_run.return_value = {"score": 0.6}
|
||||
param_space = {
|
||||
"strategy": {
|
||||
"type": "categorical",
|
||||
"choices": ["standard", "rapid"],
|
||||
}
|
||||
}
|
||||
optimizer._objective(mock_trial, param_space=param_space)
|
||||
mock_trial.suggest_categorical.assert_called_once_with(
|
||||
"strategy", ["standard", "rapid"]
|
||||
)
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_objective_returns_score_from_run_experiment(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
mock_trial = Mock()
|
||||
mock_trial.number = 0
|
||||
mock_trial.suggest_int.return_value = 2
|
||||
mock_trial.suggest_categorical.return_value = "iterdrag"
|
||||
with patch.object(optimizer, "_run_experiment") as mock_run:
|
||||
mock_run.return_value = {"score": 0.73}
|
||||
param_space = optimizer._get_default_param_space()
|
||||
result = optimizer._objective(mock_trial, param_space=param_space)
|
||||
assert result == 0.73
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_objective_appends_to_trials_history(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
mock_trial = Mock()
|
||||
mock_trial.number = 5
|
||||
mock_trial.suggest_int.return_value = 1
|
||||
mock_trial.suggest_categorical.return_value = "source_based"
|
||||
with patch.object(optimizer, "_run_experiment") as mock_run:
|
||||
mock_run.return_value = {"score": 0.55}
|
||||
param_space = optimizer._get_default_param_space()
|
||||
optimizer._objective(mock_trial, param_space=param_space)
|
||||
assert len(optimizer.trials_history) == 1
|
||||
assert optimizer.trials_history[0]["score"] == 0.55
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _save_results – sanitize_data path
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSaveResultsSanitizeData:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.joblib")
|
||||
@patch(f"{MODULE}.sanitize_data", side_effect=lambda x: x)
|
||||
@patch(
|
||||
"local_deep_research.security.file_write_verifier.write_json_verified"
|
||||
)
|
||||
def test_sanitize_data_called_during_save(
|
||||
self,
|
||||
mock_write_json,
|
||||
mock_sanitize,
|
||||
mock_joblib,
|
||||
mock_evaluator,
|
||||
tmp_path,
|
||||
):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer(output_dir=str(tmp_path))
|
||||
optimizer.study = None
|
||||
optimizer.trials_history = [
|
||||
{"trial_number": 0, "score": 0.5, "params": {}}
|
||||
]
|
||||
optimizer._save_results()
|
||||
mock_sanitize.assert_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _run_experiment – combined score calculation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestRunExperimentCombinedScore:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.SpeedProfiler")
|
||||
def test_combined_score_with_quality_and_speed_weights(
|
||||
self, mock_profiler_cls, mock_evaluator
|
||||
):
|
||||
mock_eval_instance = Mock()
|
||||
mock_eval_instance.evaluate.return_value = {
|
||||
"quality_score": 0.9,
|
||||
"benchmark_results": {},
|
||||
}
|
||||
mock_evaluator.return_value = mock_eval_instance
|
||||
mock_profiler = Mock()
|
||||
mock_profiler.get_summary.return_value = {"total_duration": 60.0}
|
||||
mock_profiler_cls.return_value = mock_profiler
|
||||
|
||||
optimizer = _make_optimizer(
|
||||
metric_weights={"quality": 0.7, "speed": 0.3}
|
||||
)
|
||||
result = optimizer._run_experiment(
|
||||
{"iterations": 2, "questions_per_iteration": 2}
|
||||
)
|
||||
assert result["success"] is True
|
||||
assert result["quality_score"] == 0.9
|
||||
assert 0.0 <= result["score"] <= 1.0
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.SpeedProfiler")
|
||||
def test_run_experiment_includes_timing_info(
|
||||
self, mock_profiler_cls, mock_evaluator
|
||||
):
|
||||
mock_eval_instance = Mock()
|
||||
mock_eval_instance.evaluate.return_value = {
|
||||
"quality_score": 0.7,
|
||||
"benchmark_results": {},
|
||||
}
|
||||
mock_evaluator.return_value = mock_eval_instance
|
||||
mock_profiler = Mock()
|
||||
mock_profiler.get_summary.return_value = {"total_duration": 45.0}
|
||||
mock_profiler_cls.return_value = mock_profiler
|
||||
|
||||
optimizer = _make_optimizer()
|
||||
result = optimizer._run_experiment({"iterations": 1})
|
||||
assert "total_duration" in result
|
||||
assert result["total_duration"] == 45.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _save_results – edge cases
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSaveResultsEdgeCases:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.joblib")
|
||||
@patch(
|
||||
"local_deep_research.security.file_write_verifier.write_json_verified"
|
||||
)
|
||||
def test_save_results_with_empty_trials_history(
|
||||
self, mock_write_json, mock_joblib, mock_evaluator, tmp_path
|
||||
):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer(output_dir=str(tmp_path))
|
||||
optimizer.study = None
|
||||
optimizer.trials_history = []
|
||||
optimizer._save_results()
|
||||
mock_write_json.assert_called_once()
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.joblib")
|
||||
@patch(
|
||||
"local_deep_research.security.file_write_verifier.write_json_verified"
|
||||
)
|
||||
def test_save_results_numpy_array_converted(
|
||||
self, mock_write_json, mock_joblib, mock_evaluator, tmp_path
|
||||
):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer(output_dir=str(tmp_path))
|
||||
optimizer.study = None
|
||||
optimizer.trials_history = [
|
||||
{
|
||||
"trial_number": 0,
|
||||
"score": np.float32(0.65),
|
||||
"params": {"max_results": np.int32(50)},
|
||||
}
|
||||
]
|
||||
optimizer._save_results()
|
||||
written_data = mock_write_json.call_args_list[0][0][1]
|
||||
assert isinstance(written_data[0]["score"], float)
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.joblib")
|
||||
@patch(
|
||||
"local_deep_research.security.file_write_verifier.write_json_verified"
|
||||
)
|
||||
def test_save_results_with_study_writes_best_params_json(
|
||||
self, mock_write_json, mock_joblib, mock_evaluator, tmp_path
|
||||
):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer(output_dir=str(tmp_path))
|
||||
mock_study = Mock()
|
||||
mock_study.best_params = {"iterations": 3, "max_results": 60}
|
||||
mock_study.best_value = 0.88
|
||||
mock_study.trials = [Mock(), Mock()]
|
||||
optimizer.study = mock_study
|
||||
optimizer.trials_history = []
|
||||
optimizer._save_results()
|
||||
# 2 JSON writes: trials + best params
|
||||
assert mock_write_json.call_count == 2
|
||||
# Also dumps the study via joblib
|
||||
mock_joblib.dump.assert_called_once()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _create_visualizations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCreateVisualizations:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.PLOTTING_AVAILABLE", False)
|
||||
def test_create_visualizations_returns_early_without_plotting(
|
||||
self, mock_evaluator
|
||||
):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
optimizer.study = Mock()
|
||||
optimizer.study.trials = [Mock(), Mock()]
|
||||
with patch(f"{MODULE}.plot_optimization_history") as mock_plot:
|
||||
optimizer._create_visualizations()
|
||||
mock_plot.assert_not_called()
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.PLOTTING_AVAILABLE", True)
|
||||
def test_create_visualizations_returns_early_when_no_study(
|
||||
self, mock_evaluator
|
||||
):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
optimizer.study = None
|
||||
with patch(f"{MODULE}.plot_optimization_history") as mock_plot:
|
||||
optimizer._create_visualizations()
|
||||
mock_plot.assert_not_called()
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.PLOTTING_AVAILABLE", True)
|
||||
def test_create_visualizations_returns_early_fewer_than_2_trials(
|
||||
self, mock_evaluator
|
||||
):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
mock_study = Mock()
|
||||
mock_study.trials = [Mock()] # only 1 trial
|
||||
optimizer.study = mock_study
|
||||
with patch(f"{MODULE}.plot_optimization_history") as mock_plot:
|
||||
optimizer._create_visualizations()
|
||||
mock_plot.assert_not_called()
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.PLOTTING_AVAILABLE", True)
|
||||
@patch(f"{MODULE}.plot_optimization_history")
|
||||
@patch(f"{MODULE}.plot_param_importances")
|
||||
@patch(f"{MODULE}.plot_contour")
|
||||
@patch(f"{MODULE}.plot_slice")
|
||||
def test_create_visualizations_calls_all_plots_with_sufficient_trials(
|
||||
self,
|
||||
mock_slice,
|
||||
mock_contour,
|
||||
mock_importances,
|
||||
mock_history,
|
||||
mock_evaluator,
|
||||
tmp_path,
|
||||
):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer(output_dir=str(tmp_path))
|
||||
mock_study = Mock()
|
||||
mock_study.trials = [Mock() for _ in range(5)]
|
||||
optimizer.study = mock_study
|
||||
for mock_fn in [
|
||||
mock_history,
|
||||
mock_importances,
|
||||
mock_contour,
|
||||
mock_slice,
|
||||
]:
|
||||
mock_fig = Mock()
|
||||
mock_fn.return_value = mock_fig
|
||||
optimizer._create_visualizations()
|
||||
mock_history.assert_called_once_with(mock_study)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# optimize() – starting callback and study creation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestOptimizeStartingCallback:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.optuna")
|
||||
def test_starting_callback_fired_before_optimize(
|
||||
self, mock_optuna, mock_evaluator
|
||||
):
|
||||
mock_evaluator.return_value = Mock()
|
||||
callback = Mock()
|
||||
mock_study = Mock()
|
||||
mock_study.best_params = {"iterations": 2}
|
||||
mock_study.best_value = 0.5
|
||||
mock_study.trials = [Mock()]
|
||||
mock_optuna.create_study.return_value = mock_study
|
||||
mock_optuna.samplers.TPESampler.return_value = Mock()
|
||||
optimizer = _make_optimizer(n_trials=1, progress_callback=callback)
|
||||
with (
|
||||
patch.object(optimizer, "_save_results"),
|
||||
patch.object(optimizer, "_create_visualizations"),
|
||||
):
|
||||
optimizer.optimize()
|
||||
starting_calls = [
|
||||
c
|
||||
for c in callback.call_args_list
|
||||
if c[0][2].get("status") == "starting"
|
||||
]
|
||||
assert len(starting_calls) == 1
|
||||
assert starting_calls[0][0][2]["stage"] == "initialization"
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
@patch(f"{MODULE}.optuna")
|
||||
def test_optimize_no_callback_does_not_raise(
|
||||
self, mock_optuna, mock_evaluator
|
||||
):
|
||||
mock_evaluator.return_value = Mock()
|
||||
mock_study = Mock()
|
||||
mock_study.best_params = {"iterations": 1}
|
||||
mock_study.best_value = 0.3
|
||||
mock_study.trials = []
|
||||
mock_optuna.create_study.return_value = mock_study
|
||||
mock_optuna.samplers.TPESampler.return_value = Mock()
|
||||
optimizer = _make_optimizer(n_trials=1)
|
||||
assert optimizer.progress_callback is None
|
||||
with (
|
||||
patch.object(optimizer, "_save_results"),
|
||||
patch.object(optimizer, "_create_visualizations"),
|
||||
):
|
||||
params, value = optimizer.optimize()
|
||||
assert params == {"iterations": 1}
|
||||
assert value == 0.3
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _optimization_callback – best_value logging path
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestOptimizationCallbackBestValue:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_callback_at_trial_1_does_not_save(self, mock_evaluator):
|
||||
"""Trial 1 is not a multiple of 10, so no save is triggered."""
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
mock_study = Mock()
|
||||
mock_study.best_value = 0.77
|
||||
mock_trial = Mock()
|
||||
mock_trial.number = 1 # 1 % 10 != 0, no save
|
||||
with patch.object(optimizer, "_save_results") as mock_save:
|
||||
optimizer._optimization_callback(mock_study, mock_trial)
|
||||
mock_save.assert_not_called()
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_callback_at_trial_10_triggers_save(self, mock_evaluator):
|
||||
"""Trial 10 is a multiple of 10 and > 0, so save is triggered."""
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
mock_study = Mock()
|
||||
mock_trial = Mock()
|
||||
mock_trial.number = 10
|
||||
with (
|
||||
patch.object(optimizer, "_save_results") as mock_save,
|
||||
patch.object(optimizer, "_create_quick_visualizations") as mock_viz,
|
||||
):
|
||||
optimizer._optimization_callback(mock_study, mock_trial)
|
||||
mock_save.assert_called_once()
|
||||
mock_viz.assert_called_once()
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_callback_at_multiple_of_10_triggers_save(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer()
|
||||
mock_study = Mock()
|
||||
mock_trial = Mock()
|
||||
mock_trial.number = 30
|
||||
with (
|
||||
patch.object(optimizer, "_save_results") as mock_save,
|
||||
patch.object(optimizer, "_create_quick_visualizations") as mock_viz,
|
||||
):
|
||||
optimizer._optimization_callback(mock_study, mock_trial)
|
||||
mock_save.assert_called_once()
|
||||
mock_viz.assert_called_once()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# metric_weights normalization
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMetricWeightsNormalization:
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_weights_normalized_to_sum_one(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
optimizer = _make_optimizer(
|
||||
metric_weights={"quality": 3.0, "speed": 1.0}
|
||||
)
|
||||
total = sum(optimizer.metric_weights.values())
|
||||
assert abs(total - 1.0) < 1e-9
|
||||
|
||||
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
||||
def test_benchmark_weights_stored(self, mock_evaluator):
|
||||
mock_evaluator.return_value = Mock()
|
||||
weights = {"simpleqa": 0.6, "browsecomp": 0.4}
|
||||
optimizer = _make_optimizer(benchmark_weights=weights)
|
||||
assert optimizer.benchmark_weights == weights
|
||||
Reference in New Issue
Block a user