Testing¶
736 tests, 94% line coverage. The suite is the specification: several tests encode properties the package promises, and weakening one to make a change pass is how a silent defect gets in.
pip install -e ".[all]"
pytest # about four minutes
pytest --cov=hiphopsllm --cov-report=term-missing
Layout¶
Directory |
Count |
What it covers |
|---|---|---|
|
15 files |
One module each, plus |
|
4 files |
The pipeline end to end, the public API surface, and the notebooks |
|
9 files, 157 tests |
HIP-LLM’s own suite, run against the vendored copy |
tests/vendor/ exists so that re-vendoring cannot quietly change the reliability
model. It is not our code and its tests are not ours to relax.
Fixtures¶
tests/conftest.py provides session-scoped fixtures so the expensive steps
happen once:
Fixture |
Is |
|---|---|
|
the three bundled architectures |
|
the bundled synthetic outcome table |
|
an analysed, uncalibrated study of |
|
the same study, calibrated from |
Use study when you need structure and calibrated when you need numbers.
Building a fresh AgenticReliabilityStudy in a test is fine and often clearer -
test_pipeline_unit.py does it for anything that mutates state.
The invariant tests¶
These are the ones to leave alone. If you change gate semantics, the synthesiser or the CPT conversion, this is what catches the mistake.
Test |
Property |
|---|---|
|
Two independent inference paths agree to nine significant figures |
|
The top event is monotone - this is what licenses |
|
Cut sets are sufficient, verified by Boolean evaluation |
|
Cut sets are minimal |
|
Tree reduction does not change the Boolean function |
|
On every hazard of every bundled example, |
|
Union splitting is exact to |
|
Calibrating twice lands on the same numbers |
|
CPTs are never fitted on a test split |
|
A snapshot shared through a variable is still a common-cause group |
|
Every HIP-LLM symbol stays reachable through this package |
Two of them exist because the bug happened. The shared-variable test was written
after a resource-detection regex was found never to match a variable simply
called model - which made every shared snapshot invisible and turned a single
point of failure into an apparently redundant architecture. The recalibration
test was written after calibrate() twice was found to compound.
Tests that police the notebooks¶
Test |
Refuses |
|---|---|
|
A cell that raises |
|
Committed output, or a cell without a stable id |
|
A notebook that differs from what the generator produces |
|
|
|
An optional package imported at a cell’s top level |
The last one AST-parses every cell and looks at where the import sits in the
tree. It exists because IPython, langgraph and pyagrum each broke every CI job in
turn by appearing as a bare import at the top of a cell. See
Notebooks.
Coverage floors¶
CI enforces 90% overall and 80% for any module over 20 statements. They are floors, not targets: they exist so a module added without tests fails there rather than quietly dragging the number down.
Coverage is not the goal. A test that executes a line without asserting anything about it is worse than no test, because it makes the number lie. Prefer asserting a property - cut sets are minimal, the union rule is exact, the two inference engines agree - over touching a branch.
Running a subset¶
pytest tests/unit/test_faulttree.py -q # one module
pytest -k "cut_set and not viz" -q # by name
pytest tests/unit/test_viz_plots.py::TestPlotArchitecture -q
pytest -x -q # stop at the first failure
pytest --lf # only what failed last time
pytest tests/vendor -q # HIP-LLM's own suite
Plot tests need a non-interactive backend. Every test module that draws sets it at import time, before pyplot is imported:
import matplotlib
matplotlib.use("Agg")
Writing a test that is worth having¶
Name it after the property, not the function: test_a_crashed_run_never_blames_a_node
says what breaks if it fails; test_run_and_observe_3 does not.
Assert on something you worked out yourself. Comparing one implementation against another catches a typo but not a misunderstanding, and a misunderstanding is what produces a confident wrong number.
Write the contrast case. test_run_and_observe.py has
test_none_is_a_missing_observation_not_a_failure immediately followed by
test_scoring_an_unreached_node_as_failed_would_be_worse, so the next reader can
see not just what the code does but which alternative was rejected and why.
Test the message, not only the raise. Most of this package’s guards exist so a
user is told what to do next, and pytest.raises(..., match="never inferred silently")
keeps that sentence from being deleted as decoration.