Testing and gates¶
A gate is a check that every change passes before it merges: the type check, the linter, the test suite at full coverage, the docs build and the client checks. Each gate proves one property of the codebase, and CI runs them all on every pull request. Setup lists the commands; this page says what each gate proves and where CI runs it.
Type check¶
basedpyright checks every path in the include list of [tool.basedpyright]: src, tests, the workbench source, its hatch_build.py and scripts. The gate requires 0 errors and 0 warnings. The reportUnknown* family, reportAny and reportExplicitAny report as warnings, and --warnings turns every warning into a failure. A suppression names one rule on one line, such as # pyright: ignore[reportAny], and reportUnnecessaryTypeIgnoreComment flags one that no longer suppresses anything. Tooling settings gives the reason for each setting.
Lint and format¶
uv run ruff check src packages scripts examples
uv run ruff format --check src packages scripts examples
ruff runs the E, F and I rule sets at a line length of 200 and sorts imports with two blank lines after them. It skips tests.
Tests and coverage¶
Every pytest run measures line coverage of src and fails below 100%. The workbench package sits outside the coverage source. exclude_also in pyproject.toml leaves out if TYPE_CHECKING: blocks, raise NotImplementedError, def __repr__, def print_tree, if __name__ == "__main__": and abstract methods. A line that runs only with gmsh or Elmer carries # pragma: no cover with a reason, and an integration test covers it on a machine that has both.
The integration tests skip themselves when gmsh, build123d or a native Elmer is missing, so the suite passes on a bare machine.
In a test that compares meshes, set deterministic=True on MeshConfig. It pins gmsh to one thread, so the same STEP and sizes always yield the identical mesh.
Run one file or one test¶
uv run pytest tests/test_release.py --no-cov
uv run pytest tests/meshing/test_firewall.py::test_block_gmsh_refuses_once_gmsh_is_loaded --no-cov
Unset FORCE_COLOR before you run pytest. With it set, Rich writes ANSI escapes into the captured output, and the log and CLI tests fail.
The guard tests¶
These tests run inside pytest and guard properties that no type checker sees.
| Test | What it proves |
|---|---|
tests/test_public_api.py |
every lazy package has an __init__.pyi stub, each stub name resolves to its source, dir() of each lazy package equals its stub, and every name in a public __all__ resolves |
tests/test_public_api.py |
every public name has a docstring of its own, every field of a curated record is documented, and every public callable with three or more parameters has an Args: block naming exactly its parameters |
tests/test_public_api.py |
ff.workbench without the workbench package names the install command pip install "femforge[workbench]" |
tests/test_field_docstrings.py |
each physical field in the listed modules has a `glyph` `[unit]`: meaning first line, pint parses the unit, and a property function's unit matches its declared dimensionality |
tests/test_import_time.py |
import femforge stays under 60 ms and loads no subpackage and none of pydantic, pint, numpy, loguru and rich; the first ff.SimulationConfig stays under 500 ms and loads no CAD, viz or run stack |
tests/meshing/test_firewall.py |
after block_gmsh, no import in the process loads gmsh, and the call refuses once gmsh has loaded |
tests/run/test_gmsh_firewall.py |
a driver that meshes through a WorkerMesher never loads gmsh, whether it meshes, reuses a cached mesh or rebinds one |
tests/test_schemas.py |
the schema export covers every registered model and writes valid JSON |
tests/test_docs_reference.py |
every public package, root module and workbench module has a ::: directive on a reference page, and every directive resolves |
tests/test_docs_snippets.py |
every fence whose info string is exactly python type-checks as a standalone module |
tests/test_release.py |
the two packages, __init__.py, the citation and the changelog carry one version, and LICENSE and NOTICE match across the packages; Release process lists each check |
The import budgets measure the fastest of five fresh interpreters. On a slow machine, set FEMFORGE_IMPORT_BUDGET_SCALE to multiply every budget; CI sets it to 2.
A docs page that needs a fragment that cannot stand alone marks its fence { .python .no-check }. Use that sparingly.
Docker end to end¶
tests/elmer/test_docker_integration.py converts, partitions and solves a partitioned case in containers, with no native Elmer. It skips unless FEMFORGE_DOCKER_TESTS is 1, since it needs Docker and the Elmer image.
Docs build¶
The strict build fails on any warning, such as a broken link, an autoref that resolves to nothing, or a snippet include of a missing file. Export the schemas first: docs/schema is build output, and the site serves it beside the JSON schemas page.
Client checks¶
Run these in packages/femforge-workbench/js when you change js/ or a workbench Python file:
| Command | What it proves |
|---|---|
npm run check |
tsc --noEmit passes and the Vitest tests pass |
npm run format:check |
Prettier finds nothing to reformat |
npm run build |
the client builds |
npm run check-dist |
the built bundle matches its sources, including the workbench Python files the source hash covers |
CI workflows¶
| Workflow | Job | What it runs | When |
|---|---|---|---|
| Quality | client |
npm ci, the four client checks, then uploads the bundle |
every push to main and every pull request |
| Quality | checks |
basedpyright, both ruff gates, and pytest under xvfb-run on Python 3.12, 3.13 and 3.14, with the bundle from client |
after client |
| Quality | docker-e2e |
fails if the runner has native Elmer, then the Docker end-to-end test | every push to main and every pull request |
| Quality | package |
uv build --all-packages --no-sources, twine check --strict, the LICENSE and NOTICE copies, the bundle files in the workbench wheel, and no build output or extra directories in the sdists |
after client |
| Fingerprint | fingerprint |
rejects agent configuration files, assistant attributions and internal ticket ids in the tracked files, the pushed commits, and the pull request title and description | every push to main; a pull request when it opens, changes, or reopens |
| Docs | docs |
the schema export and zensical build --strict, then uploads the site |
every push to main, every pull request, and on demand |
| Docs | mkdocs-material fallback |
builds the site with mkdocs-material instead of Zensical | on demand only |
| Docs | deploy |
publishes the site, as Release process describes | pushes to main in femforge-io/femforge |
Fingerprint lives in its own workflow, so editing a pull request title or description reruns only that check.
Linux¶
The checks and docker-e2e jobs and the Elmer image job install the libraries gmsh loads on Ubuntu 24.04:
sudo apt-get install -y --no-install-recommends libgl1 libglu1-mesa libxrender1 libxcursor1 libxft2 libxinerama1
checks adds xvfb and xauth and runs pytest under xvfb-run -a, which gives the rendering tests an X display. To match it locally on a headless Linux machine, install the same packages and run: