-
Notifications
You must be signed in to change notification settings - Fork 0
Testing
The test suite lives under tests/. It is organised so that the things
that should hold for every algorithm are checked once, in one place,
against every algorithm — and everything else is a focused module that
exercises a specific piece of code.
This page documents the conventions we follow when writing or extending tests, what the existing suite covers, and the rationale behind a few choices that are not obvious from the code.
tests/
├── conftest.py shared fixtures + mock problem
├── test_algorithms_uniform.py cross-algorithm baseline (registry-driven)
├── test_algorithm_protocol.py the OptimizationAlgorithm ABC
├── test_algorithms_unit.py algorithm-specific helpers, knobs, and edge cases
├── test_scipy_wrapper.py SciPyObjectiveAdapter behaviour
├── test_objective_*.py Objective invariants
├── test_problem_*.py Problem invariants
├── test_benchmark_smoke.py Benchmark orchestration
└── slow/
└── test_algorithms_integration.py real-problem (Voyager) integration
The slow/ directory contains tests marked @pytest.mark.slow; they are
excluded from the default pytest run and intended for the cluster.
tests/test_algorithms_uniform.py is the single source of truth for the
contract every OptimizationAlgorithm must satisfy. It is driven by a
registry — one AlgoSpec per algorithm — and parametrises the same
set of checks over every entry.
For each algorithm the suite verifies:
- the
algorithm_strandalgorithm_typeare well-formed and match the registry's expectation, - a short
optimize()call on the mock quadratic produces evaluations, a finitebest_loss, and a non-emptyloss_history, - every entry in
loss_historyis finite, -
best_lossequalsmin(loss_history)(i.e. tracking is consistent), -
time_stepsis monotonically non-decreasing, - the eval budget is respected up to a generous slack (twice the budget; this allows for batched algorithms whose last block overruns by one block),
-
obj.unboundedafter the run matches what the algorithm declares, -
obj.algorithm_strmatches the algorithm's class attribute, -
obj.best_params_boundedlies inside the problem's box, and - for algorithms that work in native bounded space, the raw
obj.best_paramsalso lies inside the box.
A small subset of algorithms — the ones whose internal RNG is fully captured by the seed we pass in — are additionally checked for reproducibility (same seed, same loss trajectory).
Append one entry to REGISTRY in
tests/test_algorithms_uniform.py.
Use the AlgorithmType matching the algorithm's src/dfbench/algorithms/
subfolder.
All tests then run against your algorithm automatically:
REGISTRY: list[AlgoSpec] = [
...
AlgoSpec(MyAlgorithm, AlgorithmType.GRADIENT_BASED, unbounded=True),
]If your algorithm needs extra kwargs to make progress under a small
budget (BoTorch is the canonical example — it counts iterations, not
evaluations), pass an extra_kwargs builder:
AlgoSpec(
MyAlgorithm,
AlgorithmType.SURROGATE_BASED,
unbounded=False,
extra_kwargs=lambda max_evals: {"n_initial": min(5, max_evals // 4)},
)If your algorithm is reproducibly broken in the current environment —
e.g. an upstream torch/JAX bug rather than your code — register it with
an xfail reason. Do not add pytest.mark.xfail decorators on the
tests themselves; the decorators belong on the registry entry, where
they describe the algorithm rather than the test.
AlgoSpec(
BrokenAlgo, AlgorithmType.EVOLUTIONARY, unbounded=False,
xfail="Upstream torch.compile aliasing bug, see #123.",
)Use skip instead of xfail only when running the algorithm would
crash the interpreter or hang the suite.
Anything that only applies to one algorithm. Examples that already live
in test_algorithms_unit.py:
- the SAGD transition-probability formula,
- SAM's
rhoparameter or Lookahead'sinner_optimizer_name, - Dogleg requiring a dense Hessian,
- the SR1 wrapper passing a
scipy.optimize.SR1strategy.
One class per algorithm, named after the algorithm.
A few conventions that make the suite predictable.
The mock problem in tests/conftest.py is a 2-parameter quadratic with
sigmoid bounding. It is fast, deterministic, and has a known optimum at
the origin. Use it for everything except integration tests that
explicitly need a real problem (Voyager, UIFO, …); those go under
tests/slow/ and are run on the cluster.
def test_my_algorithm(mock_problem):
obj = Objective(mock_problem, max_evals=30, max_time=60)
MyAlgorithm().optimize(obj, random_seed=42)
...Every test that runs an algorithm should pass random_seed=... (we use
42 by convention). Tests without seeds are flaky and waste time.
Default budgets in the suite are max_evals=30, max_time=60. Larger
budgets are fine when the test needs them (the Optax improvement
subset uses 50 evals), but most invariants do not require more than
a handful of steps.
Tests in the uniform suite assert things every OptimizationAlgorithm
should satisfy. Tests in algorithm-specific files assert things unique
to that algorithm. If you find yourself writing the same assertion
against several algorithms, lift it into the uniform suite — that is
the whole point.
When parametrising over algorithm classes, give pytest an id so the
test name carries the algorithm name:
@pytest.mark.parametrize("cls", MY_LIST, ids=lambda c: c.__name__)
def test_something(cls, mock_problem):
...Anything that needs Differometor, GPU, or a real problem goes in
tests/slow/ and gets pytestmark = pytest.mark.slow at module level.
The default pytest run does not pick these up. We run them
periodically on the cluster:
srun -p a100-galvani --gres=gpu:1 --time=0-00:50 \
pytest tests/slow -m slow
pytest # fast tests
pytest -m slow # slow tests only (run via srun)
pytest tests/test_algorithms_uniform.py # uniform algorithm tests
pytest -k "MyAlgorithm" # one algorithm, all uniform tests
pytest --collect-only -q # list tests without running
The fast tests finish in roughly two minutes on a laptop CPU; the uniform algorithm tests account for most of that time.
xfails are recorded with a one-line reason that explains what is
broken and where the bug lives. We use strict=False so a passing
xfail does not break CI — when the upstream bug is fixed, the next
person who notices the XPASS removes the mark.
We do not use xfail to silence flakiness. If a test is flaky, the test is wrong; fix the test or the algorithm.
Most JAX-based algorithms are deterministic given a seed. PyTorch-based
algorithms are not, because of nondeterministic CUDA kernels and the
way torch.compile reorders work. Algorithms whose determinism we
actually exercise are listed in DETERMINISTIC_ALGORITHMS in the
uniform suite; if you add an algorithm whose seed reproducibly fixes
the trajectory, add it there too.
As of the latest sweep on main, the fast-test suite reports
1308 passed, 15 skipped, 11 xfailed for
pytest tests/test_algorithms_uniform.py tests/test_algorithms_unit.py tests/test_bo_batch.py tests/test_dfo_algorithms.py.
The non-pass results all have a single root cause each:
-
11 xfailed — all
EvoxES. The 11 uniform-suite test cases applied toEvoxESare all marked xfail because the default variant (CMA-ES) trips atorch.compile/ dynamo aliasing bug insideevoxon torch >= 2.6. The bug is upstream; the otherEvoxESvariants are exercised in their own dedicated tests. -
15 skipped = 12 + 3:
-
12 are
ARCJAXacross the same uniform-suite tests —ARCJAXis intentionally exposed but raisesNotImplementedError. Its expected-failure behaviour is covered intests/test_custom_jax_batch.pyinstead. -
3 are in
tests/test_bo_batch.py, gated on optional dependencies that are not installed in the default environment:ax-platform(forAxSAASBO),HEBO, andSMAC3. Installing those packages re-enables the corresponding tests.
-
12 are
There are no unexpected failures; every skip and xfail carries an
explanatory reason in the registry / pytest.mark.skipif.
Artificial Scientist Lab | Website |University of Tübingen
Department of Computer Science
| Read our Documentation | Contact: laurin.sefa@student.uni-tuebingen.de, mario.krenn@uni-tuebingen.de, soham.basu@uni-tuebingen.de
Getting Started
Core API
Benchmarking
Contributing
Reference