Testing Infrastructure
======================
This guide describes the BAGH testing infrastructure, explains how to run
and write tests, and provides best practices for contributors. It is aimed
at quantum chemistry developers who may not be deeply familiar with Python
testing tooling.
.. contents:: On this page
:local:
:depth: 2
Overview
--------
BAGH uses `pytest `_ as its test framework. The
test suite is organised into two broad categories:
* **Fast (unit) tests** -- verify that the code base is structurally sound,
that modules import correctly, that the input parser behaves as expected,
and that utility functions return correct results. These tests run in
seconds and require no quantum-chemistry computation.
* **Slow (regression) tests** -- execute BAGH on small input files and
compare computed energies, ionisation potentials, and electron affinities
against stored reference values. These tests exercise the full
computational pipeline.
The test files live in the ``tests/`` directory of the main BAGH repository:
.. list-table::
:header-rows: 1
:widths: 30 10 60
* - File
- Tests
- Purpose
* - ``tests/conftest.py``
- --
- Shared pytest fixtures available to all test modules
* - ``tests/test_build.py``
- 30
- Smoke tests: project structure, imports, dependency checks
* - ``tests/test_input_parser.py``
- 56
- Input parser and ``cc_input`` class validation
* - ``tests/test_utilities.py``
- 31
- ``check.py``, ``printer.py``, ``methodinfo``, ``math_util``
* - ``tests/test_regression.py``
- varies
- Regression tests driven by ``reference_data.yaml``
* - ``tests/reference_data.yaml``
- --
- Reference energies, tolerances, and metadata
Quick Start
-----------
All commands below assume you are in the root of the BAGH repository and
that BAGH and its dependencies (listed in ``requirements.txt``) are
installed in your active Python environment.
Run all fast tests (recommended first step)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: bash
python3 -m pytest tests/ -m fast
This finishes in a few seconds and catches import errors, typos, and
parser regressions without running any electronic-structure calculation.
Run the full test suite
^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: bash
python3 -m pytest tests/ -v
The ``-v`` flag produces verbose output so you can see each individual test
and its result.
Skip slow tests
^^^^^^^^^^^^^^^
.. code-block:: bash
python3 -m pytest tests/ -m "not slow"
Run only regression tests
^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: bash
python3 -m pytest tests/ -m regression
Run tests for a specific method
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: bash
# By marker
python3 -m pytest tests/ -m method_ccsd
# By keyword expression
python3 -m pytest tests/test_regression.py -k "CCSD"
Test Categories and Markers
---------------------------
pytest markers are defined in ``pyproject.toml`` and allow you to select
subsets of the test suite. The following markers are available:
.. list-table::
:header-rows: 1
:widths: 25 75
* - Marker
- Description
* - ``fast``
- Unit tests that require no BAGH computation
* - ``slow``
- Regression tests that execute BAGH on input files
* - ``regression``
- Full regression tests (overlaps with ``slow``)
* - ``parser``
- Input-parsing tests (``test_input_parser.py``)
* - ``utilities``
- Utility-function tests (``test_utilities.py``)
* - ``method_ccsd``
- CCSD-specific regression tests
* - ``method_adc``
- ADC-specific regression tests
* - ``method_eom``
- EOM-CC-specific regression tests
* - ``method_so``
- Spin--orbit-specific regression tests
* - ``method_rel``
- Relativistic-method regression tests
* - ``method_fno``
- Frozen natural orbital regression tests
Markers can be combined with boolean logic:
.. code-block:: bash
# Run fast tests that are also parser tests
python3 -m pytest tests/ -m "fast and parser"
# Run CCSD or ADC regression tests
python3 -m pytest tests/ -m "method_ccsd or method_adc"
# Exclude regression tests
python3 -m pytest tests/ -m "not regression"
Understanding Test Files
------------------------
test_build.py -- Build and import smoke tests
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
These 30 tests verify that:
* Essential source files and directories exist.
* Core Python modules can be imported without error.
* Required third-party packages listed in ``requirements.txt`` are
installed.
These tests catch packaging and environment problems early and run with
no computational overhead.
test_input_parser.py -- Parser tests
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The 56 tests in this module exercise the BAGH input parser and the
``cc_input`` class. They cover:
* Keyword recognition and default values.
* Correct handling of method-specification blocks.
* Edge cases and malformed input.
test_utilities.py -- Utility function tests
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
31 tests covering the helper modules:
* ``check.py`` -- validation utilities.
* ``printer.py`` -- formatted output helpers.
* ``methodinfo`` -- method metadata lookups.
* ``math_util`` -- mathematical helper functions.
test_regression.py -- Regression tests
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Regression tests are **parametrised** from ``tests/reference_data.yaml``.
Each entry in the YAML file becomes a separate test case. The test runner:
1. Locates the ``.inp`` file specified in the YAML entry.
2. Executes BAGH on that input.
3. Parses the output for the expected ``search_pattern``.
4. Compares every value listed under ``values`` against the computed
result, using the specified ``tolerance``.
Because these tests invoke full BAGH calculations they are marked ``slow``
and ``regression``.
conftest.py -- Shared fixtures
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``conftest.py`` defines fixtures that are automatically available to every
test module. If you need a fixture that is shared across multiple test
files, place it here rather than duplicating it.
Adding New Tests
----------------
Adding a unit test
^^^^^^^^^^^^^^^^^^
1. Identify which test file is appropriate (``test_build.py``,
``test_input_parser.py``, ``test_utilities.py``, or create a new
module).
2. Write a function whose name starts with ``test_``.
3. Apply the relevant marker(s) with ``@pytest.mark.``.
4. Run the new test in isolation to confirm it passes:
.. code-block:: bash
python3 -m pytest tests/test_utilities.py -k "test_my_new_test" -v
Example:
.. code-block:: python
import pytest
@pytest.mark.fast
@pytest.mark.utilities
def test_square_root_helper():
from bagh_code.math_util import my_sqrt
assert abs(my_sqrt(4.0) - 2.0) < 1e-12
Adding a regression test
^^^^^^^^^^^^^^^^^^^^^^^^
Regression tests are data-driven. You do **not** write a new Python test
function; instead you add an entry to ``tests/reference_data.yaml`` and the
parametrised test runner picks it up automatically.
**Step 1 -- Prepare the input file.**
Create or verify a ``.inp`` file in the ``test/`` directory. Keep it as
small as possible (minimal basis, small molecule) so the test finishes
quickly.
**Step 2 -- Obtain reference values.**
Run BAGH manually on the input file and record the quantities of interest
(energies, IPs, EAs, etc.) to full precision.
**Step 3 -- Add an entry to** ``reference_data.yaml``.
.. code-block:: yaml
MY_NEW_TEST:
input_file: my_new_test.inp
search_pattern: "CCSD correlation energy"
values:
E_CCSD: -0.07068008830844916
tolerance: 1.0e-8
category: fast
Fields:
* ``input_file`` -- path to the ``.inp`` file (relative to ``test/``).
* ``search_pattern`` -- string that the test runner searches for in the
BAGH output to locate the result line.
* ``values`` -- dictionary of quantity names to expected numerical values.
* ``tolerance`` -- maximum allowed absolute difference between computed
and reference values.
* ``category`` -- ``fast`` or ``slow``; controls which marker is applied.
**Step 4 -- Verify.**
.. code-block:: bash
python3 -m pytest tests/test_regression.py -k "MY_NEW_TEST" -v
Reference Data Management
--------------------------
All reference data lives in ``tests/reference_data.yaml``. When updating
reference values, follow these guidelines:
* **Never round reference values.** Store them at the full precision
produced by BAGH.
* **Choose tolerances carefully.** A tolerance of ``1.0e-8`` Hartree is
typical for total and correlation energies. Looser tolerances may be
appropriate for properties that are inherently less precise (e.g.,
numerical gradients).
* **Document the provenance.** If a reference value is updated, note the
BAGH version or commit hash that produced it in a comment.
* **Keep the file sorted.** Group entries by method (CCSD, ADC, EOM, etc.)
to make the file easy to navigate.
Example entry with a comment:
.. code-block:: yaml
CCSD:
input_file: CCSD.inp
search_pattern: "CCSD correlation energy"
values:
E_CCSD: -0.07068008830844916 # from commit abc1234
tolerance: 1.0e-8
category: fast
Troubleshooting
---------------
Tests fail on import
^^^^^^^^^^^^^^^^^^^^
Ensure BAGH is installed in the active environment:
.. code-block:: bash
pip install -e .
and that all dependencies are satisfied:
.. code-block:: bash
pip install -r requirements.txt
Regression test fails with a small numerical difference
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* Check whether the tolerance in ``reference_data.yaml`` is appropriate
for the quantity being tested.
* Confirm that the same basis set files and integral library are being
used.
* If a code change intentionally alters the result, update the reference
value and record the reason.
A test is not discovered
^^^^^^^^^^^^^^^^^^^^^^^^
* The function name must start with ``test_``.
* The file name must start with ``test_`` or end with ``_test.py``.
* If the test is in ``reference_data.yaml``, ensure the YAML is valid
(no indentation errors, no duplicate keys).
``pytest`` reports "no tests ran"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
You may have specified a marker or keyword expression that matches nothing.
Double-check the marker names (see the table above) and keyword spelling.
Known issues discovered by the test suite
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The following bugs were identified when the test suite was first run and
may serve as useful reference:
* **``methodinfo.py`` line 25** -- a typo where ``self.inlist`` should be
``self.intlist``.
* **``cc_input_read``** -- the ``fc`` keyword sets ``self.fc = True`` but
never initialises ``self.fc_no``, which can cause ``AttributeError`` in
downstream code.
Code Coverage
-------------
Generate an HTML coverage report with:
.. code-block:: bash
python3 -m pytest tests/ --cov=bagh_code --cov-report=html
This creates an ``htmlcov/`` directory. Open ``htmlcov/index.html`` in a
browser to see line-by-line coverage.
To print a summary to the terminal instead:
.. code-block:: bash
python3 -m pytest tests/ --cov=bagh_code --cov-report=term-missing
The ``--cov-report=term-missing`` flag highlights which lines are not
exercised by any test.
.. note::
Coverage requires the ``pytest-cov`` package. Install it with
``pip install pytest-cov`` if it is not already available.
Best Practices for Developers
-----------------------------
1. **Run fast tests before every commit.** A quick
``python3 -m pytest tests/ -m fast`` takes only seconds and catches
the most common mistakes.
2. **Run the full suite before opening a pull request.** Regression
failures caught early save review cycles.
3. **Keep unit tests deterministic.** Avoid random data unless you seed
the random number generator. Tests should produce the same result on
every run.
4. **Test one thing per function.** Small, focused tests are easier to
debug when they fail.
5. **Use markers consistently.** Every new test should carry at least one
marker (``fast`` or ``slow``) so that selective test runs work
correctly.
6. **Prefer absolute tolerances for energies.** Relative tolerances can
mask large errors when the reference value is close to zero.
7. **Do not commit large input files.** Regression inputs should use
minimal basis sets and small molecules to keep test times manageable.
8. **Add regression tests for bug fixes.** When you fix a bug, add a
test that would have caught it. This prevents regressions.
9. **Keep** ``reference_data.yaml`` **under version control.** Any change
to reference values should be reviewed in the same pull request as
the code change that motivated it.
Continuous Integration (CI/CD)
------------------------------
BAGH uses GitHub Actions for automated testing. Three workflows are configured:
CI — Fast Tests (on every push)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
**File:** ``.github/workflows/ci.yml``
Triggered on every push and pull request to ``master``. Runs fast unit tests
across Python 3.9–3.12 in about 2 minutes. No Fortran or Cython compilation
is needed.
.. code-block:: text
Trigger: push to master, PR to master
Runner: GitHub-hosted (ubuntu-latest)
Duration: ~2 minutes
Tests: 148 fast tests (parser, utilities, build checks)
Coverage: Generated for Python 3.11, uploaded as artifact
PR — Quality Gate
^^^^^^^^^^^^^^^^^^
**File:** ``.github/workflows/pr-check.yml``
Required check before merging any pull request. Verifies fast tests pass,
core imports work, and minimum code coverage threshold is met.
Nightly — Full Regression
^^^^^^^^^^^^^^^^^^^^^^^^^^
**File:** ``.github/workflows/nightly.yml``
Runs at 02:00 UTC every night. Two jobs:
1. **regression-fast** — Runs on GitHub-hosted runners. Executes regression
tests that need only Python (no compiled extensions).
2. **regression-full** — Runs on a **self-hosted runner** that has BAGH fully
built with Fortran/Cython extensions. Generates coverage report.
The nightly workflow can also be triggered manually from the GitHub Actions tab
with optional method and category filters.
Local Test Runner Script
^^^^^^^^^^^^^^^^^^^^^^^^^
A convenience script ``run_tests.sh`` is provided at the project root:
.. code-block:: bash
./run_tests.sh # Fast tests only (default)
./run_tests.sh fast # Fast tests only
./run_tests.sh all # All tests including regression
./run_tests.sh regression # Regression tests only
./run_tests.sh coverage # Fast tests with HTML coverage report
./run_tests.sh method CCSD # Specific method regression test
Setting Up a Self-Hosted Runner
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For the nightly full regression suite, you need a self-hosted GitHub Actions
runner on a machine where BAGH is fully compiled:
1. Go to **Settings > Actions > Runners** in your GitHub repository
2. Click **New self-hosted runner** and follow the setup instructions
3. Ensure the runner machine has:
- Intel Fortran compiler (``ifort``/``ifx``) or ``gfortran``
- MKL or OpenBLAS
- PySCF, NumPy, SciPy, h5py, Numba
- BAGH built: ``mkdir build && cd build && cmake .. && cmake --build . --target compile_all``
4. Start the runner: ``./run.sh`` (or configure as a system service)