Skip to content

Commit 413de7b

Browse files
committed
update
1 parent d53c1d3 commit 413de7b

6 files changed

Lines changed: 605 additions & 2 deletions

File tree

tests/test_benchmark_formatter.py

Lines changed: 279 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
import builtins
2+
from contextlib import redirect_stdout
23
import csv
4+
import io
5+
import json
6+
import math
7+
import os
38
import tempfile
49
import unittest
10+
from unittest.mock import patch
511

12+
import numpy as np
13+
14+
from benchmark.base import BenchmarkConfig
615
from benchmark.formatter import BenchmarkResults
7-
from benchmark.registry import DatasetRegistry
16+
from benchmark.metrics import BenchmarkMetrics
17+
from benchmark.registry import DatasetRegistry, ModelRegistry
18+
from benchmark.runner import BenchmarkRunner
819

920

1021
class BenchmarkResultsTest(unittest.TestCase):
@@ -37,6 +48,273 @@ def import_without_pandas(name, *args, **kwargs):
3748
self.assertEqual(rows[0]["model"], "rnn")
3849
self.assertEqual(rows[0]["metrics"], "{'mae': 0.1}")
3950

51+
def test_dataframe_and_export_formats(self):
52+
results = BenchmarkResults(
53+
[
54+
{"dataset": "a", "model": "rnn", "metrics": {"mae": 1.0, "rmse": 2.0}},
55+
{"dataset": "a", "model": "rnn", "metrics": {"mae": 3.0, "rmse": "bad"}},
56+
{"dataset": "b", "model": "tcn", "metrics": {"mae": 4.0}},
57+
]
58+
)
59+
60+
frame = results.to_dataframe()
61+
self.assertEqual(set(frame["dataset"]), {"a", "b"})
62+
row = frame[(frame["dataset"] == "a") & (frame["model"] == "rnn")].iloc[0]
63+
self.assertEqual(row["mae_mean"], 2.0)
64+
self.assertEqual(row["mae_std"], 1.0)
65+
66+
with tempfile.TemporaryDirectory() as tmpdir:
67+
csv_path = f"{tmpdir}/results.csv"
68+
json_path = f"{tmpdir}/results.json"
69+
results.to_csv(csv_path)
70+
results.to_json(json_path)
71+
with open(csv_path, encoding="utf-8") as fh:
72+
self.assertIn("mae_mean", fh.readline())
73+
with open(json_path, encoding="utf-8") as fh:
74+
self.assertEqual(json.load(fh)[0]["dataset"], "a")
75+
76+
def test_dataframe_reports_missing_pandas_dependency(self):
77+
results = BenchmarkResults([])
78+
original_import = builtins.__import__
79+
80+
def import_without_pandas(name, *args, **kwargs):
81+
if name == "pandas":
82+
raise ImportError("pandas disabled for dataframe test")
83+
return original_import(name, *args, **kwargs)
84+
85+
try:
86+
builtins.__import__ = import_without_pandas
87+
with self.assertRaisesRegex(ImportError, "pandas is required"):
88+
results.to_dataframe()
89+
finally:
90+
builtins.__import__ = original_import
91+
92+
def test_pivot_and_latex_cover_missing_and_invalid_values(self):
93+
results = BenchmarkResults(
94+
[
95+
{"metrics": {"mae": 1.0, "invalid": "not-a-number"}},
96+
{"dataset": "second", "model": "only", "metrics": {"mae": 2.0}},
97+
]
98+
)
99+
pivot = results._pivot()
100+
self.assertEqual(pivot["unknown"]["unknown"]["mae"], [1.0])
101+
self.assertEqual(pivot["unknown"]["unknown"]["invalid"], [])
102+
103+
with tempfile.NamedTemporaryFile(suffix=".tex") as tmp:
104+
results.to_latex(tmp.name, metric="rmse")
105+
tmp.seek(0)
106+
latex = tmp.read().decode("utf-8")
107+
self.assertIn("rmse", latex)
108+
self.assertIn("-", latex)
109+
110+
with tempfile.NamedTemporaryFile(suffix=".tex") as tmp:
111+
BenchmarkResults([{"dataset": "first", "model": "only", "metrics": {"mae": 1.0}}]).to_latex(
112+
tmp.name, metric="mae"
113+
)
114+
tmp.seek(0)
115+
self.assertIn("1.0000", tmp.read().decode("utf-8"))
116+
117+
def test_empty_exports_and_console_output(self):
118+
empty = BenchmarkResults([])
119+
with tempfile.TemporaryDirectory() as tmpdir:
120+
csv_path = f"{tmpdir}/empty.csv"
121+
original_import = builtins.__import__
122+
123+
def import_without_pandas(name, *args, **kwargs):
124+
if name == "pandas":
125+
raise ImportError("pandas disabled for empty fallback test")
126+
return original_import(name, *args, **kwargs)
127+
128+
try:
129+
builtins.__import__ = import_without_pandas
130+
empty.to_csv(csv_path)
131+
finally:
132+
builtins.__import__ = original_import
133+
self.assertFalse(os.path.exists(csv_path))
134+
latex_path = f"{tmpdir}/empty.tex"
135+
empty.to_latex(latex_path)
136+
with open(latex_path, encoding="utf-8") as fh:
137+
self.assertIn("No results", fh.read())
138+
139+
output = io.StringIO()
140+
with redirect_stdout(output):
141+
empty.print_table()
142+
self.assertIn("No results to display", output.getvalue())
143+
144+
def test_print_table_formats_nonempty_results(self):
145+
results = BenchmarkResults([{"dataset": "synthetic", "model": "rnn", "metrics": {"mae": 0.25}}])
146+
output = io.StringIO()
147+
with redirect_stdout(output):
148+
results.print_table()
149+
self.assertIn("Dataset: synthetic", output.getvalue())
150+
self.assertIn("mae", output.getvalue())
151+
152+
with patch.object(results, "_pivot", return_value={"synthetic": {"rnn": {"mae": []}}}):
153+
output = io.StringIO()
154+
with redirect_stdout(output):
155+
results.print_table()
156+
self.assertIn("N/A", output.getvalue())
157+
158+
159+
class BenchmarkHelpersTest(unittest.TestCase):
160+
def test_formatter_helpers_ignore_nan_values(self):
161+
from benchmark.formatter import _avg, _format_value, _std
162+
163+
self.assertEqual(_format_value(1.23456), "1.2346")
164+
self.assertEqual(_format_value("value"), "value")
165+
self.assertEqual(_avg([1.0, float("nan"), 3.0]), 2.0)
166+
self.assertEqual(_std([1.0, float("nan"), 3.0]), 1.0)
167+
self.assertTrue(math.isnan(_avg([])))
168+
self.assertTrue(math.isnan(_std([float("nan")])))
169+
170+
def test_benchmark_metrics_cover_standard_and_edge_cases(self):
171+
y_true = np.array([0.0, 2.0, 4.0])
172+
y_pred = np.array([0.0, 1.0, 2.0])
173+
metrics = BenchmarkMetrics(["mae", "mse", "rmse", "mape", "smape", "r2", "mape_pct"])
174+
values = metrics.compute(y_true, y_pred)
175+
self.assertEqual(values["mae"], 1.0)
176+
self.assertEqual(values["mse"], 5.0 / 3.0)
177+
self.assertAlmostEqual(values["rmse"], np.sqrt(5.0 / 3.0))
178+
self.assertIn("mape_pct", values)
179+
180+
self.assertTrue(math.isnan(BenchmarkMetrics.mape(np.zeros(2), np.ones(2))))
181+
self.assertTrue(math.isnan(BenchmarkMetrics.smape(np.zeros(2), np.zeros(2))))
182+
self.assertTrue(math.isnan(BenchmarkMetrics.r2(np.ones(2), np.zeros(2))))
183+
self.assertEqual(metrics.compute(y_true, y_pred, metrics=["does_not_exist"]), {})
184+
metrics.mae = lambda *_: (_ for _ in ()).throw(RuntimeError("broken metric"))
185+
self.assertTrue(math.isnan(metrics.compute(y_true, y_pred, metrics=["mae"])["mae"]))
186+
with self.assertRaises(ValueError):
187+
metrics.compute(y_true, np.zeros(2))
188+
with self.assertRaises(ValueError):
189+
BenchmarkMetrics(["does_not_exist"])
190+
191+
192+
class BenchmarkRunnerTest(unittest.TestCase):
193+
def test_registry_resolution_and_runner_validation(self):
194+
config = BenchmarkConfig(models=["rnn"], datasets=["toy"], output_dir="unused")
195+
dataset_registry = DatasetRegistry()
196+
dataset_registry.register("toy", object)
197+
model_registry = ModelRegistry()
198+
runner = BenchmarkRunner(config, dataset_registry, model_registry)
199+
200+
self.assertEqual(runner._resolve_datasets(), ["toy"])
201+
self.assertEqual(runner._resolve_models(), ["rnn"])
202+
with self.assertRaises(ValueError):
203+
BenchmarkRunner(
204+
BenchmarkConfig(models=["missing"], datasets=["toy"]), dataset_registry, model_registry
205+
)._resolve_models()
206+
with self.assertRaises(ValueError):
207+
BenchmarkRunner(
208+
BenchmarkConfig(models=["rnn"], datasets=["missing"]), dataset_registry, model_registry
209+
)._resolve_datasets()
210+
211+
all_config = BenchmarkConfig(models=["all"], datasets=["all"])
212+
all_runner = BenchmarkRunner(all_config, dataset_registry, model_registry)
213+
self.assertEqual(all_runner._resolve_datasets(), ["toy"])
214+
self.assertIn("rnn", all_runner._resolve_models())
215+
216+
def test_single_trial_uses_dataset_overrides(self):
217+
config = BenchmarkConfig(models=["rnn"], datasets=["toy"], epochs=3, batch_size=4, learning_rate=0.1)
218+
dataset_registry = DatasetRegistry()
219+
runner = BenchmarkRunner(config, dataset_registry, ModelRegistry())
220+
dataset = type("ToyDataset", (), {"train_length": 8, "predict_sequence_length": 2})()
221+
train = (np.zeros((2, 4, 1)), np.zeros((2, 2, 1)))
222+
valid = (np.zeros((1, 4, 1)), np.zeros((1, 2, 1)))
223+
dataset.get_train_valid_split = lambda **kwargs: (train, valid)
224+
history = type("History", (), {"history": {"loss": [np.float32(0.5)]}})()
225+
226+
with patch("benchmark.runner.AutoConfig") as auto_config, patch(
227+
"benchmark.runner.AutoModel"
228+
) as auto_model, patch("benchmark.runner.Trainer") as trainer_cls:
229+
auto_config.for_model.return_value = type("Config", (), {"input_shape": None})()
230+
auto_model.from_config.return_value = object()
231+
trainer_cls.return_value.train.return_value = history
232+
trainer_cls.return_value.predict.return_value = valid[1]
233+
result = runner._run_single_trial(
234+
dataset,
235+
"toy",
236+
"rnn",
237+
run_idx=0,
238+
seed=42,
239+
ds_config={"train_length": 5, "predict_sequence_length": 1, "epochs": 1, "batch_size": 2},
240+
)
241+
242+
self.assertEqual(result["train_length"], 5)
243+
self.assertEqual(result["predict_sequence_length"], 1)
244+
self.assertEqual(result["history"]["loss"], [0.5])
245+
246+
def test_run_and_save_results(self):
247+
with tempfile.TemporaryDirectory() as tmpdir:
248+
config = BenchmarkConfig(models=["rnn"], datasets=["toy"], output_dir=tmpdir)
249+
dataset_registry = DatasetRegistry()
250+
dataset_registry.register("toy", object)
251+
runner = BenchmarkRunner(config, dataset_registry, ModelRegistry())
252+
with patch.object(runner, "_run_experiment") as run_experiment:
253+
result = runner.run()
254+
run_experiment.assert_called_once_with("toy", "rnn")
255+
self.assertEqual(result.results, [])
256+
257+
def test_experiment_runs_each_seed_and_default_registry_is_available(self):
258+
config = BenchmarkConfig(models=["rnn"], datasets=["toy"], runs=2, seed=10)
259+
dataset_registry = DatasetRegistry()
260+
261+
class ToyDataset:
262+
train_length = 4
263+
predict_sequence_length = 1
264+
265+
dataset_registry.register("toy", ToyDataset)
266+
runner = BenchmarkRunner(config, dataset_registry, ModelRegistry())
267+
trial_results = [{"run": 0}, {"run": 1}]
268+
with patch("benchmark.runner.set_seed") as set_seed, patch.object(
269+
runner, "_run_single_trial", side_effect=trial_results
270+
) as run_trial:
271+
runner._run_experiment("toy", "rnn")
272+
self.assertEqual(runner.results, trial_results)
273+
self.assertEqual(set_seed.call_args_list[0].args, (10,))
274+
self.assertEqual(set_seed.call_args_list[1].args, (11,))
275+
self.assertEqual(run_trial.call_count, 2)
276+
277+
from benchmark.runner import _default_dataset_registry
278+
279+
default_names = _default_dataset_registry().list_datasets()
280+
self.assertIn("sine", default_names)
281+
self.assertIn("grocery_sales", default_names)
282+
283+
284+
class BenchmarkConfigTest(unittest.TestCase):
285+
def test_validation_and_dataset_overrides(self):
286+
with self.assertRaises(ValueError):
287+
BenchmarkConfig(runs=0)
288+
with self.assertRaises(ValueError):
289+
BenchmarkConfig(epochs=0)
290+
config = BenchmarkConfig(
291+
epochs=3,
292+
batch_size=4,
293+
per_dataset_config={"toy": {"epochs": 1, "train_length": 5}},
294+
)
295+
self.assertEqual(config.get_dataset_config("toy")["epochs"], 1)
296+
self.assertEqual(config.get_dataset_config("other")["epochs"], 3)
297+
298+
299+
class RegistryTest(unittest.TestCase):
300+
def test_base_registry_and_model_registry_operations(self):
301+
registry = DatasetRegistry()
302+
registry.register("toy", int)
303+
self.assertIn("toy", registry)
304+
self.assertEqual(registry.get("toy"), int)
305+
self.assertEqual(registry.list_items(), {"toy": int})
306+
with self.assertLogs("benchmark.registry", level="WARNING"):
307+
registry.register("toy", str)
308+
with self.assertRaises(KeyError):
309+
registry.get("missing")
310+
311+
models = ModelRegistry()
312+
models.register("custom", "CustomModel")
313+
self.assertIn("custom", models)
314+
self.assertEqual(models.get("custom"), "CustomModel")
315+
with self.assertRaises(KeyError):
316+
models.get("missing")
317+
40318

41319
class DatasetRegistryTest(unittest.TestCase):
42320
def test_lazy_dataset_registration_returns_instantiable_wrapper(self):

0 commit comments

Comments
 (0)