A tool to validate a project's build, CI and tooling against best practices.
It recognises a project's languages and build systems, and judges each language by its own
ecosystem's tools: a C++ project needs branch coverage from gcovr or llvm-cov, a Python project from coverage.py, a
Rust project from cargo-llvm-cov. docs/ecosystems has a page for each supported ecosystem, saying
how it is recognised and what each check accepts for it; project-validator --list shows which checks apply to which
ecosystems, and --format markdown lays a project's results out as a grid of checks against ecosystems, each
cell naming the tools that ran. The grid for project_validator itself is in the job summary of every
CI run.
Checks that do not apply to any of the project's languages are reported as SKIP. A project using several languages
is judged for each of them and gets the worst result. In a project of no recognised language, only the checks that
apply to every project run.
More ecosystems can be added with plugins (see Adding an ecosystem).
This README, the pages under docs/ and the generated support matrix are also published as a site at
cpp-best-practices.github.io/project_validator,
built from main by CI on every push; each page says which commit it reflects.
The validator itself is pure Python and has no build step; it only inspects
(and, with --build, configures and builds) the project it is pointed at.
- Python 3.12+ (the system Python on Ubuntu 24.04 LTS and later; no installation step,
run with
python3 -m project_validator <path>) - PyYAML 6.0+ (
python3-yamlon Ubuntu, orpython3 -m pip install PyYAML) - Optionally rich 13.0+ for colored output
- CMake / CTest, only for
--build(already required by any CMake project being validated)
On a machine without Python 3.12, uv run or a distro backport will work.
In CI, use actions/setup-python with python-version: "3.x" (or any version from 3.12).
python3 -m project_validator path/to/project
python3 -m project_validator --list
python3 -m project_validator --map path/to/project
python3 -m project_validator --facts path/to/project
python3 -m project_validator --answers path/to/project
python3 -m project_validator --only coverage,platform-matrix --format json path/to/project| Option | Meaning |
|---|---|
--list |
list the available checks; with --format json, also the ecosystems each one applies to; with --format markdown, as a table of check, ecosystems and description |
--support-matrix |
print what each check recognises for each ecosystem: the grid of every check against every ecosystem, with the tools each recognises (markdown; JSON with --format json). A tool several ecosystems share (CodeQL, osv-scanner, lizard) is listed once below the grid with the ecosystems it reads |
--map |
print the detected project structure as JSON: languages, formats (what its data files are in), build systems, the signals that recognised each ecosystem, the file suffixes no ecosystem recognises, vendored files (other projects' code and minified bundles, which add no language), the units the project is judged in (one for most projects; each package of a monorepo and each build root of a mixed-language project), and the CI providers, jobs and toolchains |
--facts |
print the facts found about the project as JSON, each with where it was found: the source and build files with their languages, the settings the configuration files hold (per tool), the CI jobs (one per matrix combination), the commands and actions each runs, the commands those run in turn (package scripts, Deno tasks, Composer scripts, Make targets, pre-commit hooks), what the coverage tools CI runs measure, and what could not be read (see docs/architecture.md) |
--answers |
print the answers to every question about the project as JSON, per language in each unit of the project: each yes, no, unknown (something that could answer it was not read) or impossible (no known tool provides it for the language), with what shows it (the tools CI runs and the files that define them, the coverage at each granularity, the settings met, the platforms, the manifests and lockfiles, …) and what the known tools could do |
--only CHECKS / --skip CHECKS |
comma separated check names to run / not run |
--format text|json|markdown |
output format; json gives each check's result and, in places, its result for each ecosystem in each unit (status, message, details, the tools that ran, what --auxiliary named); markdown lays those places out as a grid of checks against ecosystems, for a CI job summary (and says so when no ecosystem was recognised) |
--color auto|always|never |
color the text output (needs rich); auto colors only on a terminal when NO_COLOR is unset |
--exclude DIR |
leave DIR, relative to the project, out of the validation (e.g. test fixtures); may be repeated |
--build |
configure and build the project in a temporary directory and ask ctest for its tests; a build that cannot run makes whether there are tests unknown |
--strict |
treat warnings as failures |
--auxiliary WHAT |
judge this code as auxiliary, not the product's: by role (tooling, docs, example, benchmark, test), by language or ecosystem (python, c/c++), or by directory (scripts/); may be repeated. Every check still judges it in full, but its failures are warnings, labelled (auxiliary: tooling) and saying in a detail what named the code. Without it, every source outside vendored directories is the product's: a C++ project's scripts/release.py makes Python one of its languages, held to every check |
--max-file-size MIB |
committed files larger than this belong in Git LFS or DVC (default: 5) |
--allow-official-tags |
let the actions GitHub publishes (actions/, github/) be pinned to tags |
--allow-unknown |
judge by what could be read when an answer depends on CI configuration that could not be (a provider scanned heuristically, a matrix computed by an expression, a reusable workflow in another repository). Without it, such an answer is an ERROR: a gap in the validator's tooling, not a finding about the project |
--cmake PATH / --ctest PATH |
the executables used by --build |
--plugin-dir DIR |
load extra checks and CI providers from DIR/*.py |
Exit codes: 0 everything passed, 1 a check failed or raised an error, 2 usage error.
Every language is judged in each unit (build root or workspace member) of the project whose code is written in it, whatever directory that code is in: a C++ project's scripts/release.py or docs/conf.py makes Python one of its languages, held to every check as the product is. The dial is --auxiliary: code it names (by role, --auxiliary tooling; by language or ecosystem, --auxiliary python; or by directory, --auxiliary scripts/) is still judged in full, where it is, but its failures are warnings labelled (auxiliary: tooling), with a detail saying what named the code. Vendored code (third_party/, vendor/, minified bundles) is not judged. A format (JSON, TOML, YAML, XML: see Data) is an ecosystem of files that are not programs, judged the same way in each unit whose files are in it, for hygiene only; its files make no language the project's, and are read in the configuration directories (.github/, .vscode/) too. What each check accepts for a language is on that ecosystem's page in docs/ecosystems; python -m project_validator --support-matrix prints the grid of every check against every ecosystem, with the tools each recognises (on the site as the support matrix).
| Check | Passes when |
|---|---|
ci-present |
at least one CI configuration is found |
tests-run |
each language's tests exist in each unit (a separate test project's count for all of them) and CI runs them (ctest-runs is accepted as an old name). Warns when CI never runs the tests |
fuzz-testing |
each language with a fuzzer has fuzz targets in each unit (libFuzzer, cargo-fuzz, atheris, Go fuzzing, ...) and CI runs them (a ClusterFuzzLite action, or a CTest registration that passes a libFuzzer option, counts). Warns when CI never runs the targets, when CI runs a fuzzer but no targets are recognised, or when only property-based tests exist |
coverage |
A coverage tool CI runs measures branch coverage, or the finest coverage any known tool can measure for the language when none measures branches. The result says how far CI takes it: measured, reported (a report shows the numbers) or gated (CI fails under a threshold), and suggests the next rung. Path or MC/DC coverage is a bonus. A setting counts when CI runs the tool that reads it |
platform-matrix |
CI covers at least 2 operating systems, 2 toolchains (compilers, or the ecosystem's language versions) and 3 OS/toolchain combinations; for SQL, 2 database engine versions (the service containers CI runs: postgres:16), whatever the OS |
ci-build-types |
every OS/toolchain combination in CI builds Debug and Release, each with and without sanitizers |
sanitizers |
sanitizers are used in some configuration: for C/C++, AddressSanitizer and UndefinedBehaviorSanitizer (-fsanitize=address,undefined, MSVC's /fsanitize=address, cmake_template's ENABLE_SANITIZER_* options, Meson's b_sanitize), warning when only one of the two is; for Rust, cargo miri test or a nightly -Zsanitizer build; for Go, go test -race (the race detector; -asan and -msan count too). Fails when none is used; ci-build-types asks that each platform builds with and without them |
compiler-warnings |
a strict warning set is enabled. Warns listing what is missing |
warnings-as-errors |
warnings fail the build, the lint or the tests |
cxx-extensions |
compiler extensions to the language standard are turned off, so -std=c++NN is used rather than -std=gnu++NN (for Fortran, -std=f2018 rather than the GNU dialect) |
out-of-source-build |
the build prevents in-source builds: a CMake file compares CMAKE_SOURCE_DIR with CMAKE_BINARY_DIR (and stops with message(FATAL_ERROR) when they are equal), or sets CMAKE_DISABLE_IN_SOURCE_BUILD; Meson always builds out of source. Warns otherwise |
compile-commands |
the build exports compile_commands.json, so clang-based tooling (clang-tidy, clangd, IDEs) can read it: CMAKE_EXPORT_COMPILE_COMMANDS on in the CMake files, a preset or on the command line; Meson always writes it. Warns otherwise |
top-level-guard |
tests and developer-only settings are conditioned on the project being built on its own (PROJECT_IS_TOP_LEVEL, CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR; Meson: meson.is_subproject()), so that it works as a subproject through FetchContent, CPM or add_subdirectory. Warns otherwise |
hardening |
binary hardening is enabled: exploit mitigations that the build inserts into the binary (static analysis and dependency audits are judged by security-scanning and dependency-audit). Ecosystems that build no native binary are not judged. Warns listing what is missing |
code-formatting |
CI enforces each language's formatting in each unit: a formatter run that fails on unformatted code, or one that rewrites it where the job fails on the change or commits it (a pre-commit hook, git diff --exit-code). Its style is defined (e.g. .clang-format) or the tool's own. Warns when the style is defined but CI does not enforce it |
function-limits |
function length is limited: lizard runs with a length limit (lizard --length 30), or one of the ecosystem's own tools limits it |
static-analysis |
CI runs a static analyzer on each language in each unit (and every analyzer a language requires: clang-tidy and cppcheck for C/C++), with its checks defined where it has no defaults worth running. Warns when an analyzer is configured but does not run, or runs short; fails when none runs |
type-checking |
CI (or a pre-commit hook CI runs) runs a static type checker on each dynamically typed language in each unit. Strict mode is a bonus; without it the result suggests it. Warns when a type checker is configured but does not run; fails when none runs. Compiled languages, whose compilers check types, are not judged |
mutation-testing |
CI runs a mutation testing tool on each language in each unit (mutmut, cosmic-ray, Mull, cargo-mutants, gremlins, StrykerJS, PIT, Stryker.NET, Infection, mutant, Muter); running it on a pull request's changes only is a bonus (heuristic: the mutant names are taken as the pull request's changes). Warns when a tool is configured but does not run, or when none is found |
action-pinning |
every action and reusable workflow CI uses from another repository is pinned to a full commit SHA (actions/checkout@<SHA> # v4.2.2), and every docker:// image to a digest. Fails when one is pinned to a branch or not at all; warns when one is pinned to a tag or an abbreviated commit (--allow-official-tags accepts tags for actions/ and github/). Skipped without GitHub Actions |
ci-lint |
CI (or a pre-commit hook) lints the configuration of each CI provider it uses: GitHub Actions with actionlint or zizmor, GitLab CI with glab ci lint or gitlab-ci-local --preview, CircleCI with circleci config validate. Warns otherwise; skipped without CI, or when no linter is known for the providers used |
copy-paste-detection |
CI runs a duplicate code detector (PMD CPD, jscpd, Duplo, Simian, CCFinderX or SonarQube); warns otherwise |
security-scanning |
CI scans each language's code in each unit for security problems: CodeQL, GitLab SAST or semgrep, or the language's security lints where CI runs them. Warns otherwise |
secret-scanning |
CI or a pre-commit hook runs a secret scanner: gitleaks, TruffleHog, detect-secrets, ggshield or Nosey Parker. Warns otherwise, and says so when a scanner is configured (.gitleaks.toml, .secrets.baseline, .gitguardian.yaml) but nothing runs it. GitHub's own secret scanning is a repository setting, which cannot be seen from a checkout |
dependency-management |
dependencies come from a package manager and are locked (where the ecosystem expects a lock), in each unit (a lockfile locks its directory and those below it). Passes with a note when no dependencies are declared. Warns when they are not locked, or when third-party code is committed (third_party/, external/, extern/, vendor/ other than go mod vendor output, or git submodules) |
dependency-audit |
CI checks each language's dependencies in each unit against known vulnerabilities. osv-scanner, trivy fs, snyk test and GitLab Dependency Scanning (and their actions) count for every ecosystem whose lockfiles they read. Warns otherwise |
schema-validation |
CI validates each format's documents in each unit against their schemas: check-jsonschema, ajv, the jsonschema CLI or jv for JSON Schema instances; kubeconform, kubeval or helm lint --strict for Kubernetes manifests; frictionless (or its action), goodtables or csvlint with a schema for a Data Package's tables; graphql-inspector validate or GraphQL Code Generator for GraphQL operations. Fails otherwise |
schema-declared |
every schema declares its dialect and its identifier, so that it is read as meant and can be referred to: a JSON Schema's $schema and $id (a draft older than 2019-09 warns), an OpenAPI or AsyncAPI description's version and info.version (Swagger 2.0 warns), an XSD's targetNamespace and, on each document of one of the project's XSDs, the schema's location (xsi:schemaLocation, or an xml-model instruction), a .proto file's syntax (or edition) and package, a Data Package descriptor's profile and name. Judged per format in each unit; a test suite's schemas are reported, not judged. Fails listing what each schema leaves out |
schema-examples |
CI checks the examples that come with each format's schemas in each unit: valid ones that must pass and invalid ones that must fail (ajv test --valid, --invalid), a validator run on an examples directory (check-jsonschema, jv, xmllint, jing), a linter whose rules validate a description's examples (spectral, redocly, vacuum), or contract tests (schemathesis, dredd). Warns otherwise |
schema-compatibility |
CI checks each format's schemas in each unit for breaking changes against the main branch or a registry: buf breaking (or bufbuild/buf-action), oasdiff breaking (or oasdiff/oasdiff-action/breaking), openapi-diff --fail-on-incompatible, json-schema-diff, graphql-inspector diff (or graphql-hive/graphql-inspector), rover graph check. Warns otherwise |
link-check |
CI checks each documentation site's links in each unit and fails on a broken one: lychee (or lycheeverse/lychee-action), sphinx-build -b linkcheck, markdown-link-check (or its action), mdbook-linkcheck ([output.linkcheck] in book.toml), MkDocs' htmlproofer plugin, linkinator, muffet, htmltest, HTMLProofer or LinkChecker. A scheduled run counts, since links rot without a change to the repository. Warns otherwise |
migration-safety |
CI checks each language's database migrations in each unit for changes that are unsafe to apply to a live database (a lock held on a busy table, a column dropped): squawk (or sbdchd/squawk-action with fail-on-violations: true), atlas migrate lint (or ariga/atlas-action/migrate/lint), liquibase checks run. Warns otherwise; a project without migrations (Flyway-named files, or .sql under migrations/, db/migrate/, db/migrations/) is not judged |
data-wellformed |
every data file of a format with a parser (JSON, TOML, YAML and XML are built in; a plugin's format adds its own) parses, judged per format in each unit, the configuration in .github/, .gitlab/, .circleci/, .buildkite/, .devcontainer/ and .vscode/ included. JSON that tools read with comments (*.jsonc, tsconfig*.json, jsconfig.json, devcontainer.json, .vscode/*.json) may have comments and trailing commas; YAML tags of any tool (GitLab's !reference) are accepted; XML external entities are never read. Files over 5 MiB are not parsed. Fails listing each file that does not parse, with the line and column. Data files under test directories (tests/, spec/, fixtures/, …) are read but only reported, not judged: a parser's test data is malformed on purpose |
large-files |
no committed file is larger than 5 MiB (--max-file-size) unless Git LFS stores it (filter=lfs in a .gitattributes) or DVC tracks it (a .dvc file beside it). In a git checkout only the files git tracks are judged. Warns listing the largest files |
security-policy |
a SECURITY.md (or .rst, .txt, ...) at the root, in .github/ or in docs/ says how to report a vulnerability: an email address, a link, or the forge's private vulnerability reporting. Warns otherwise |
license |
a license file (LICENSE, LICENSE.md, LICENSE-MIT, COPYING, UNLICENSE, ... or REUSE's LICENSES/ directory) holds a recognised license: an SPDX-License-Identifier line, or the standard text of AGPL, LGPL, GPL, Apache-2.0, MPL-2.0, EPL-2.0, BSL-1.0, Unlicense, CC0-1.0, MIT, BSD, ISC, 0BSD or Zlib. Warns when the license is not recognised, or when a manifest declares another one. Fails without a license file |
dependency-updates |
Dependabot or Renovate is configured; warns otherwise |
status-badges |
the README (at the root, in .github/ or in docs/) shows a CI status badge of any supported CI provider (GitHub Actions badge.svg, GitLab pipeline, CircleCI, ... or their shields.io badges) and a quality badge (Codecov, Coveralls, SonarCloud, Codacy, Code Climate, DeepSource, Coverity Scan, OpenSSF Scorecard or Best Practices). Warns when only one kind is shown; fails when there is no README or no badge |
committed-artifacts |
no build output is committed. In a git checkout only the files git tracks are judged; --exclude keeps fixtures out. Fails listing the outputs |
vcs-ignore |
a .gitignore, .hgignore, .bzrignore or .fossil-settings/ignore-glob ignores the build outputs of each ecosystem in the project. Warns when an ignore file misses them |
CI detection is pluggable too. GitHub Actions workflows (including strategy.matrix
with include/exclude) and GitLab CI pipelines (including parallel: matrix:, local include: files, extends:
and !reference)
are parsed as YAML. A job is judged by what it runs: its commands (and the commands those run in turn), its image,
environment and actions. A GitHub step or job whose if: is false for a matrix combination does not run in it
(runner.os, matrix.*, env.* and contains/startsWith/endsWith are evaluated; conditions on anything else
are kept), and expressions whose values are known are evaluated the way GitHub evaluates them before a step runs.
Each build system reads the build types from its own commands (-DCMAKE_BUILD_TYPE, --config, ctest -C,
meson setup --buildtype, cargo --release, swift build -c, xcodebuild -configuration). Azure Pipelines,
AppVeyor, CircleCI, Travis CI, Jenkins, Buildkite, Drone, Cirrus CI, Bitbucket Pipelines and Woodpecker are detected
and scanned heuristically. pre-commit.ci is the CI of a .pre-commit-config.yaml with a ci: section: it runs the
hooks (but ci.skip), and builds nothing, so it is no platform or build configuration. A pre-commit hook counts only
where CI runs it (pre-commit.ci, pre-commit run or pre-commit/action). A job that names no compiler is counted with its runner's default compiler (gcc on Linux,
AppleClang on macOS, MSVC on Windows).
Known limitations:
- A GitLab
include:from another project, a URL or a template is not read. - A GitHub matrix computed by an expression (
fromJSON(...)) cannot be expanded; those jobs run with their matrix values unknown. - Evidence is found by scanning text, so every file under the project is considered;
use
--excludeto leave out test fixtures or vendored code. - A CI job that names no compiler counts as a C/C++ build with its runner's default compiler, unless it only runs another ecosystem's tools (e.g. pytest, but no CMake, Meson or compiler).
Any module-level Check, CiProvider or Ecosystem in a file passed with --plugin-dir is picked up, and so is
any component: a Tool (a formatter, a coverage tool), a SettingsReader, a CommandExpander or an Action
(see docs/architecture.md):
from project_validator.ci import CiProvider
from project_validator.core import Context, Result, check
EXAMPLE_CI = CiProvider(name="Example CI", files=("example-ci.yml",))
@check(name="has-readme", description="The project has a README")
def has_readme(context: Context) -> Result:
if context.project.exists("README.md"):
return Result.passed("README.md found")
return Result.failed("no README.md")Everything the validator knows about a language lives in its profile under project_validator/ecosystems/:
how it is recognised (build systems, source suffixes, how CI names its toolchain versions), the tools it uses
(formatters and coverage tools, which say how CI runs them), the settings its practices ask for, and what varies
between its CI jobs (compilers, toolchain versions). The checks are generic: each one judges every language some
component handles, and does not apply to the others. A plugin can add an ecosystem and its components the same way,
without changing the validator.
A built-in profile is found by its module or package, project_validator/ecosystems/<name>.py or <name>/; it comes
with its own tests/layouts/<name>.py, fixtures tests/fixtures/well_configured_<name> and
tests/fixtures/bare_<name>, and page docs/ecosystems/<name>.md, and no shared list names it.
python -m project_validator.scaffold <name> <Label> <suffix>... writes them: a package recognising the language by
its suffixes and its CI toolchain (guessed from the name, to be checked), with a module each for its tools and its
practices, and the fixtures' files that are not about the language; what is left is the language's own knowledge.
An ecosystem is a language (kind="language", the default) or a format (kind="format"): a kind of file that is not
a program (JSON, YAML, a Dockerfile), with no toolchain, tests or coverage, whose files are judged for hygiene. A
format's languages are its format names, the key every tool and check uses, so a tool that handles "YAML" applies
its check to the format; a format profile defines a Parser (project_validator.facts.wellformed) for each of its
formats, and data-wellformed judges their files. The built-in data profile is one.
A project is recognised as an ecosystem by any of:
BuildSystem.files: exact names (build.zig), file name patterns (*.csproj) or directory patterns ending in/(*.xcodeproj/), matched anywhere in the project;source_suffixes: suffixes only this ecosystem uses;source_names: file names, or name patterns, of sources with no telling suffix (Dockerfile,Dockerfile.*);detect: a probe that reads the project (e.g.project.read_head(path)) and returns the languages, or formats, it found, for ecosystems that file names cannot show (a Kubernetes manifest is YAML withapiVersionandkind);ambiguous_suffixes: suffixes other ecosystems may share (.h,.pl), counted only once the ecosystem was recognised another way. A suffix mapped toNoneis accounted for without adding a language.
Registering an ecosystem that claims another one's build file, source suffix or source name is an error.
--map lists the suffixes no ecosystem recognises, so the gaps are visible.
from project_validator.ecosystems.model import BuildSystem, Ecosystem, toolchain_patterns
from project_validator.facts.axes import versions_axis
from project_validator.facts.formatting import Enforcement, flagged, format_tool
from project_validator.facts.tools import defined_by
CRYSTAL = Ecosystem(
label="Crystal",
languages=frozenset({"Crystal"}),
build_systems=(BuildSystem(name="Shards", files=("shard.yml",), languages=frozenset({"Crystal"})),),
source_suffixes={".cr": "Crystal"},
# How CI names the toolchain: `crystal: 1.14.0` to crystal-lang/install-crystal, or `crystal spec` without one.
toolchain=toolchain_patterns(
"Crystal",
version=r"(\d+\.\d+(?:\.\d+)?|latest|nightly)",
inputs=("crystal",),
programs=(("crystal", "spec"), ("crystal", "build"), ("crystal", "run"), "shards"),
setup=("crystal",),
),
)
# platform-matrix: OS x Crystal version.
CRYSTAL_VERSIONS = versions_axis("Crystal version", "Crystal", frozenset({"Crystal"}))
# code-formatting: the formatter has one style; `crystal tool format --check` fails on unformatted files, without
# `--check` it rewrites them.
CRYSTAL_FORMAT = format_tool(
name="crystal tool format",
languages=frozenset({"Crystal"}),
can={Enforcement.CHECK: "crystal tool format --check", Enforcement.APPLY: "crystal tool format"},
formats=flagged(("crystal", "tool", "format"), checks=("--check",)),
style=defined_by(),
)--list --format json shows which ecosystems each check applies to, including those added by plugins.
docs/architecture.md describes how the validator is put together; docs/history.md what each step of its rearchitecture changed.
python3 -W error -m unittestThe project validates itself; CI runs the same command:
python3 -m project_validator --strict --exclude tests/fixtures .CI also appends --format markdown output to the job summary, a grid of every check against the ecosystems in the
project: open the latest CI run on main to see it.
Each quality gate is its own CI job, so a pull request shows which one failed. To run them locally:
| CI job | Command |
|---|---|
lint |
ruff check . |
formatting |
ruff format --check --diff . |
typing |
mypy |
copy-paste-detection |
npx --yes jscpd@4.3.0 (settings in .jscpd.json) |
function-limits |
lizard --length 30 --warnings_only project_validator tests |
mutation testing |
mapfile -t p < <(python tests/changed_mutants.py origin/main...HEAD) && mutmut run "${p[@]}": the mutants of the functions a pull request changes (settings in [tool.mutmut]; with no patterns, mutmut run would mutate everything); mutmut results lists survivors. mutmut copies the tree into mutants/, which the self-referential tests need to be a git repository: git -C mutants init && git -C mutants add -A after a first run |
pages |
python tests/build_site.py _site (needs Markdown): the documentation site, written to _site/; CI publishes it to GitHub Pages from main and only builds it on a pull request that changes the builder or its workflow |
fuzzing |
python tests/fuzz/fuzz_<parser>.py tests/fuzz/corpus/<parser> -max_total_time=60 for each of the six parsers (atheris); locally, write new corpus entries to a temp directory first: python tests/fuzz/fuzz_<parser>.py "$(mktemp -d)" tests/fuzz/corpus/<parser> -max_total_time=60 |
Branch coverage is measured with coverage.py
(configured in pyproject.toml, which fails the report below 95%):
python3 -m pip install "coverage>=7.10"
coverage run --branch -m unittest
coverage reportCI uploads the coverage report to Codecov and fails if branch coverage drops below the threshold.
The validator is meant to run anywhere a project can be built, with as little setup as possible. To keep it that way:
- The standard library comes first. If something can reasonably be done with the Python standard library, do it that way.
- External dependencies are allowed, but must be carefully weighed.
A PR that adds one must explain:
- what problem it solves that the standard library can't reasonably solve
- how mature it is, how well it's maintained, and its license
- its transitive dependencies (fewer is better; pure-Python is preferred)
- whether it can be optional, with the validator degrading gracefully (and saying so in its output) when it's missing
- Required dependencies are listed in
pyproject.tomlwith a lower bound and justified in this section. Optional dependencies go in an[project.optional-dependencies]extra. - Plugins (checks) must not add dependencies that the rest of the validator doesn't already have.
| Dependency | Kind | Why |
|---|---|
| PyYAML >=6.0 | required | The standard library has no YAML parser, and CI configuration is YAML. PyYAML is mature, MIT licensed, has no dependencies, and is packaged by every major distro (python3-yaml on Ubuntu). It is required rather than optional because scanning CI files as text cannot tell which compiler runs on which OS, nor apply a matrix's include/exclude, so it over-counts platform combinations and can pass platform-matrix for a project that should fail. Without PyYAML the validator exits with an error that says how to install it. |
| rich >=13.0 | optional (pretty extra) | Colored, aligned text output with a per-status summary. MIT licensed and widely used. Without it the validator prints plain text and says so. |
| coverage.py >=7.10 | development only (coverage extra) | Measures the branch coverage of the test suite; the standard library's trace module does not measure branches. coverage.py is the de facto Python coverage tool, Apache 2.0 licensed and has no required dependencies. The validator itself never imports it. |
| ruff, mypy, lizard, jscpd, mutmut, pytest, atheris | CI only | The lint, typing, function-limits and copy-paste-detection quality gates, the mutation-testing gate (mutmut needs pytest to run the suite) and atheris, the fuzz-testing gate. They are installed by their CI jobs and are never imported by the validator. |
PyYAML is the only required dependency.
The tools CI installs are locked with hashes: each .github/requirements/<job>.in names a job's tools,
and <job>.txt pins them and everything they depend on, by version and hash. CI installs them with
--require-hashes, so a tampered package fails the install. Dependabot updates both files; to change
a tool by hand, edit its .in file and recompile the lock with pip-tools:
cd .github/requirements
pip-compile --generate-hashes --allow-unsafe --strip-extras --no-emit-index-url --output-file=<job>.txt <job>.in