Compare commits

..
Author SHA1 Message Date
Niels Lohmann f7d39622a8 Merge develop
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-28 07:11:42 +01:00
Niels Lohmann 9fe832104a Merge branch 'develop' into bench/json-view-comparison-fairness
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-27 17:25:34 +01:00
Niels Lohmann 7ab30dbf37 Merge develop
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-27 14:31:52 +01:00
Niels Lohmann 3d70a35bef Merge branch 'develop' into bench/json-view-comparison-fairness
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-27 07:29:19 +01:00
Niels Lohmann 18fdd81212 Merge origin/develop into bench/json-view-comparison-fairness
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 21:40:23 +01:00
Niels Lohmann 43beef41b9 Merge branch 'develop' into bench/json-view-comparison-fairness
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 15:53:30 +01:00
Niels Lohmann b38c056204 json_view benchmarks: compare fairly and state what each row means
- bench_view: add "(editable)" rows for json_editable_document and a
  parse+dump workload, so the cost of lazy float conversion shows next to the
  parse row (the traverse workload already includes the parse). Check that
  all engines' dumps agree where outputs are comparable.
- compare.py: add --aligned, which also builds and runs with aligned code and
  reports both tables, so code-layout artifacts become visible. Off by
  default; the default output is unchanged. Not run end to end yet.
- README: say which engines are like-for-like peers (yyjson, simdjson DOM) and
  which are mutable reference points (Boost.JSON, json::parse, the editable
  json_view); explain the lazy float conversion and the eager prototype that
  was measured and dropped.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-25 10:54:04 +01:00
16 changed files with 594 additions and 1418 deletions

No files matched your search

-9
View File
@@ -154,15 +154,6 @@ and each page's title (H1) and declaration by
[`docs/docset/generate_docset.py`](https://github.com/nlohmann/json/blob/develop/docs/docset/generate_docset.py);
`make build` reports API pages that cannot be classified.
A pull request that changes only the documentation, Markdown files, GitHub metadata, or license files skips the
compiler workflows (Ubuntu, macOS, Windows); the documentation and license checks still run. See
[`tools/check_path_filters`](https://github.com/nlohmann/json/tree/develop/tools/check_path_filters) for the list of
skipped files and what to do when a compiler job starts reading one of them.
Most compiler jobs run only once a maintainer adds the label "🚀 ready to merge" to a pull request; until then, a
smaller set of jobs runs on every push. See
[How CI runs](https://json.nlohmann.me/community/quality_assurance/#how-ci-runs) for the sets and when they run.
### Amalgamate the source code
The single-header files
-19
View File
@@ -7,25 +7,6 @@ on:
- master
- release/*
pull_request:
# Skip pull requests that change only files no job here reads. The list is checked by
# tools/check_path_filters/check_path_filters.py (repository_checks.yml); change both together.
paths:
- '**'
- '!docs/**'
- '!**.md'
- '!.github/*'
- '!.github/ISSUE_TEMPLATE/**'
- '!.github/external_ci/**'
- '!.github/workflows/**'
- '!.reuse/**'
- '!LICENSES/**'
- '!CITATION.cff'
- '!REUSE.toml'
# read by tools/check_build_options in the ci_meson_install job (ubuntu.yml)
- 'docs/mkdocs/docs/integration/package_managers.md'
# built by the swiftpm job (macos.yml)
- 'docs/mkdocs/docs/integration/swift/**'
- '.github/workflows/macos.yml'
workflow_dispatch:
concurrency:
-83
View File
@@ -1,83 +0,0 @@
name: Repository checks
# Checks that concern files the compiler workflows (ubuntu.yml, macos.yml,
# windows.yml) skip with their path filter: the documentation, the license
# information, and the CI configuration itself. This workflow has no path filter.
on:
push:
branches:
- develop
- master
- release/*
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs:
check_ci_configuration:
runs-on: ubuntu-26.04
steps:
- name: Harden Runner
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check the path filters of the compiler workflows
run: python3 tools/check_path_filters/check_path_filters.py
- name: Check the job sets of the compiler workflows and their documentation
run: |
python3 -m venv venv
venv/bin/pip install --quiet -r tools/ci_jobs/requirements.txt
venv/bin/python tools/ci_jobs/ci_jobs.py
ci_reuse_compliance:
runs-on: ubuntu-26.04
steps:
- name: Harden Runner
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ci_reuse_compliance
ci_test_documentation:
runs-on: ubuntu-26.04
strategy:
matrix:
target: [ci_test_examples, ci_test_build_documentation, ci_test_documentation_mermaid]
steps:
- name: Harden Runner
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# the git-revision-date-localized plugin needs the history; a shallow clone makes the strict build fail
fetch-depth: ${{ matrix.target == 'ci_test_build_documentation' && '0' || '1' }}
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
if: matrix.target == 'ci_test_documentation_mermaid'
with:
node-version: 24
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ${{ matrix.target }}
+262 -422
View File
@@ -7,62 +7,17 @@ on:
- master
- release/*
pull_request:
# labeled: the "🚀 ready to merge" label starts the full set (see below)
types: [opened, synchronize, reopened, labeled]
# Skip pull requests that change only files no job here reads. The list is checked by
# tools/check_path_filters/check_path_filters.py (repository_checks.yml); change both together.
paths:
- '**'
- '!docs/**'
- '!**.md'
- '!.github/*'
- '!.github/ISSUE_TEMPLATE/**'
- '!.github/external_ci/**'
- '!.github/workflows/**'
- '!.reuse/**'
- '!LICENSES/**'
- '!CITATION.cff'
- '!REUSE.toml'
# read by tools/check_build_options in the ci_meson_install job (ubuntu.yml)
- 'docs/mkdocs/docs/integration/package_managers.md'
# built by the swiftpm job (macos.yml)
- 'docs/mkdocs/docs/integration/swift/**'
- '.github/workflows/ubuntu.yml'
schedule:
# the nightly set (see below), on develop
- cron: '0 2 * * *'
workflow_dispatch:
permissions:
contents: read
# A run triggered by any other label gets a group of its own, so it does not
# cancel the run of the last push; its jobs are skipped.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'schedule' && 'nightly' || github.ref || github.run_id }}${{ github.event.action == 'labeled' && github.event.label.name != '🚀 ready to merge' && format('-{0}', github.run_id) || '' }}
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
# The jobs are split into three sets (see "How CI runs" in
# docs/mkdocs/docs/community/quality_assurance.md):
#
# - PR set: every pull request push and every push; chosen because these jobs
# caught 92 % of the failing commits in Sep/Oct 2026.
# - full set: after the PR set passed, on pushes and on pull requests with the
# "🚀 ready to merge" label; every remaining compiler, version and standard.
# - nightly set: slow tools and rare compilers, nightly on develop, on pushes
# to master and release/*, and when started by hand.
#
# A matrix whose entries belong to different sets is split into one job per
# set; the jobs share their steps through YAML anchors. tools/ci_jobs checks
# that every job belongs to exactly one set.
jobs:
###########################################################################
# PR set
###########################################################################
ci_test_gcc:
if: &pr_set ${{ github.event_name != 'schedule' && (github.event.action != 'labeled' || github.event.label.name == '🚀 ready to merge') }}
runs-on: ubuntu-latest
container: gcc:latest
steps:
@@ -77,7 +32,6 @@ jobs:
run: cmake --build build --target ci_test_gcc
ci_meson_install:
if: *pr_set
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -122,342 +76,7 @@ jobs:
cmake -S tests/cmake_import/project -B build-import-split -DCMAKE_PREFIX_PATH=${{ github.workspace }}/install-split
cmake --build build-import-split
ci_static_analysis_ubuntu:
if: *pr_set
runs-on: ubuntu-26.04
strategy:
matrix:
target: [ci_cpplint]
steps: &static_analysis_ubuntu_steps
- name: Harden Runner
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
- name: Install Valgrind
if: matrix.target == 'ci_test_valgrind'
run: sudo apt-get update ; sudo apt-get install -y valgrind
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ${{ matrix.target }}
ci_static_analysis_clang:
if: *pr_set
runs-on: ubuntu-latest
container: silkeh/clang:22
strategy:
matrix:
target: [ci_test_clang, ci_clang_tidy, ci_test_clang_sanitizer, ci_single_binaries]
steps: &static_analysis_clang_steps
- name: Install git, clang-tools, iwyu (ci_single_binaries), and unzip
run: apt-get update ; apt-get install -y git clang-tools iwyu unzip
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ${{ matrix.target }}
ci_cmake_options:
if: *pr_set
runs-on: ubuntu-latest
container: ubuntu:24.04
strategy:
matrix:
target: [ci_test_diagnostics, ci_test_diagnostic_positions, ci_test_noexceptions, ci_test_noglobaludls]
steps: &cmake_options_steps
- name: Install build-essential
run: apt-get update ; apt-get install -y build-essential unzip wget git
# the CMake 3.5.0 release binary that ci_cmake_flags downloads links
# against libidn.so.11, which Ubuntu 24.04 no longer ships
- name: Install libidn11 for CMake 3.5.0
if: matrix.target == 'ci_cmake_flags'
run: |
wget -q http://archive.ubuntu.com/ubuntu/pool/main/libi/libidn/libidn11_1.33-2.2ubuntu2_amd64.deb
dpkg -i libidn11_1.33-2.2ubuntu2_amd64.deb
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ${{ matrix.target }}
ci_test_compilers_gcc_old:
if: *pr_set
runs-on: ubuntu-latest
strategy:
matrix:
compiler: ['4.8']
# official gcc:4.8/4.9/5/6 images fail to check out code (too old for
# actions/checkout); install the old compilers on top of official ubuntu:20.04
# instead, mirroring what the (now retired) custom json-ci image did.
container: ubuntu:20.04
steps: &compilers_gcc_old_steps
- name: Install g++-${{ matrix.compiler }}
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends software-properties-common ca-certificates gnupg make git
# add-apt-repository resolves the PPA through the Launchpad API,
# which intermittently times out or fails the team lookup (the plain
# "deb ..." sources below never hit Launchpad and never flake).
# Retry with backoff so a transient Launchpad blip does not fail CI.
for attempt in 1 2 3 4 5; do
add-apt-repository -y ppa:ubuntu-toolchain-r/test && break
echo "::warning::add-apt-repository ppa:ubuntu-toolchain-r/test failed (attempt ${attempt}/5); retrying"
sleep $((attempt * 10))
done
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ bionic main"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ bionic universe"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial main"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial universe"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial-updates main"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial-updates universe"
apt-get update
apt-get install -y --no-install-recommends g++-${{ matrix.compiler }}
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: CXX=g++-${{ matrix.compiler }} cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ci_test_compiler_g++-${{ matrix.compiler }}
ci_test_compilers_clang:
if: *pr_set
runs-on: ubuntu-latest
strategy:
matrix:
compiler: ['3.4']
container: silkeh/clang:${{ matrix.compiler }}
steps: &compilers_clang_steps
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Disable filesystem support (std::filesystem is unusable) for clang 7 / 8 / 9 / 10
run: echo "JSON_FORCED_GLOBAL_COMPILE_OPTIONS=-DJSON_HAS_FILESYSTEM=0;-DJSON_HAS_EXPERIMENTAL_FILESYSTEM=0" >> "$GITHUB_ENV"
if: ${{ matrix.compiler == '7' || matrix.compiler == '8' || matrix.compiler == '9' || matrix.compiler == '10' }}
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ci_test_compiler_default
ci_test_standards_gcc:
if: *pr_set
runs-on: ubuntu-latest
container: gcc:latest
strategy:
matrix:
standard: [26]
steps: &standards_gcc_steps
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ci_test_gcc_cxx${{ matrix.standard }}
ci_test_standards_clang:
if: *pr_set
runs-on: ubuntu-latest
container: silkeh/clang:latest
strategy:
matrix:
standard: [26]
stdlib: [libcxx]
steps: &standards_clang_steps
- name: Install git and unzip
run: apt-get update ; apt-get install -y git unzip
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build with libc++
run: cmake --build build --target ci_test_clang_libcxx_cxx${{ matrix.standard }}
if: ${{ matrix.stdlib == 'libcxx' }}
- name: Build with libstdc++
run: cmake --build build --target ci_test_clang_cxx${{ matrix.standard }}
if: ${{ matrix.stdlib == 'libstdcxx' }}
ci_emscripten:
if: *pr_set
runs-on: ubuntu-26.04
steps:
- name: Harden Runner
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
- name: Install emscripten
uses: mymindstorm/setup-emsdk@4528d102f7230f0e7b276855c01ea1159be0e984 # v16
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -GNinja
- name: Build
run: cmake --build build
###########################################################################
# full set: starts after the PR set passed
###########################################################################
ci_static_analysis_ubuntu_full:
needs: &pr_set_jobs [ci_test_gcc, ci_meson_install, ci_static_analysis_ubuntu, ci_static_analysis_clang, ci_cmake_options, ci_test_compilers_gcc_old, ci_test_compilers_clang, ci_test_standards_gcc, ci_test_standards_clang, ci_emscripten]
if: &full_set ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, '🚀 ready to merge') }}
runs-on: ubuntu-26.04
strategy:
matrix:
target: [ci_test_single_header, ci_cppcheck, ci_reproducible_tests, ci_non_git_tests, ci_offline_testdata]
steps: *static_analysis_ubuntu_steps
ci_static_analysis_clang_full:
needs: *pr_set_jobs
if: *full_set
runs-on: ubuntu-latest
container: silkeh/clang:22
strategy:
matrix:
target: [ci_clang_analyze]
steps: *static_analysis_clang_steps
ci_cmake_options_full:
needs: *pr_set_jobs
if: *full_set
runs-on: ubuntu-latest
container: ubuntu:24.04
strategy:
matrix:
target: [ci_cmake_flags, ci_test_noimplicitconversions, ci_test_legacycomparison, ci_test_disableenumserialization, ci_test_disabletuplereferenceconversion, ci_test_skiplibraryversioncheck, ci_test_simdutf, ci_test_strict_nul_handling, ci_test_delete_deprecated_functions, ci_test_no_thread_local]
steps: *cmake_options_steps
ci_test_compilers_gcc_old_full:
needs: *pr_set_jobs
if: *full_set
runs-on: ubuntu-latest
strategy:
matrix:
compiler: ['4.9', '5', '6']
container: ubuntu:20.04
steps: *compilers_gcc_old_steps
ci_test_compilers_gcc_full:
needs: *pr_set_jobs
if: *full_set
runs-on: ubuntu-latest
strategy:
matrix:
# older GCC docker images (4, 5, 6) fail to check out code
compiler: ['7', '8', '9', '10', '11', '12', '13', '14', '15', '16', 'latest']
container: gcc:${{ matrix.compiler }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ci_test_compiler_default
ci_test_compilers_clang_full:
needs: *pr_set_jobs
if: *full_set
runs-on: ubuntu-latest
strategy:
matrix:
compiler: ['3.5', '3.6', '3.7', '3.8', '3.9', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15-bullseye', '16', '17', '18', '19', '20', '21', '22', 'latest']
container: silkeh/clang:${{ matrix.compiler }}
steps: *compilers_clang_steps
ci_test_standards_gcc_full:
needs: *pr_set_jobs
if: *full_set
runs-on: ubuntu-latest
container: gcc:latest
strategy:
matrix:
standard: [11, 14, 17, 20, 23]
steps: *standards_gcc_steps
ci_test_standards_clang_full:
needs: *pr_set_jobs
if: *full_set
runs-on: ubuntu-latest
container: silkeh/clang:latest
strategy:
matrix:
standard: [11, 14, 17, 20, 23, 26]
stdlib: [libcxx, libstdcxx]
exclude:
# in the PR set
- standard: 26
stdlib: libcxx
steps: *standards_clang_steps
ci_module_cpp20:
needs: *pr_set_jobs
if: *full_set
strategy:
matrix:
container: ['gcc:latest', 'silkeh/clang:latest']
runs-on: ubuntu-latest
container: ${{ matrix.container }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# The module test uses `import std;`, which needs CMake's experimental
# import-std support. Its opt-in token is CMake-version-specific, so pin
# CMake to the version whose token is set in tests/module_cpp20/CMakeLists.txt.
- name: Get pinned CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
with:
cmakeVersion: 4.3.4
# Clang: the std library module is provided by libc++ (the image's libstdc++
# ships none), and the image's libc++ module manifest has a broken relative
# path — repoint it at the real module sources.
- name: Use libc++ and fix its module manifest path (Clang)
if: matrix.container == 'silkeh/clang:latest'
run: |
echo "CXXFLAGS=-stdlib=libc++" >> "$GITHUB_ENV"
mkdir -p /usr/lib/share && ln -sf /usr/lib/llvm-*/share/libc++ /usr/lib/share/libc++
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ci_module_cpp20
###########################################################################
# nightly set
###########################################################################
ci_infer:
if: &nightly_set ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref != 'refs/heads/develop') }}
runs-on: ubuntu-26.04
steps:
- name: Harden Runner
@@ -486,16 +105,76 @@ jobs:
path: ${{ github.workspace }}/build/build_infer/infer-out/report.txt
if-no-files-found: ignore
ci_static_analysis_ubuntu_nightly:
if: *nightly_set
ci_static_analysis_ubuntu:
runs-on: ubuntu-26.04
strategy:
matrix:
target: [ci_test_valgrind]
steps: *static_analysis_ubuntu_steps
target: [ci_test_single_header, ci_cppcheck, ci_cpplint, ci_reproducible_tests, ci_non_git_tests, ci_offline_testdata, ci_reuse_compliance, ci_test_valgrind]
steps:
- name: Harden Runner
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
- name: Install Valgrind
if: matrix.target == 'ci_test_valgrind'
run: sudo apt-get update ; sudo apt-get install -y valgrind
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ${{ matrix.target }}
ci_static_analysis_clang:
runs-on: ubuntu-latest
container: silkeh/clang:22
strategy:
matrix:
target: [ci_test_clang, ci_clang_tidy, ci_test_clang_sanitizer, ci_clang_analyze, ci_single_binaries]
steps:
- name: Install git, clang-tools, iwyu (ci_single_binaries), and unzip
run: apt-get update ; apt-get install -y git clang-tools iwyu unzip
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ${{ matrix.target }}
ci_cmake_options:
runs-on: ubuntu-latest
container: ubuntu:24.04
strategy:
matrix:
target: [ci_cmake_flags, ci_test_diagnostics, ci_test_diagnostic_positions, ci_test_noexceptions, ci_test_noimplicitconversions, ci_test_legacycomparison, ci_test_noglobaludls, ci_test_disableenumserialization, ci_test_disabletuplereferenceconversion, ci_test_skiplibraryversioncheck, ci_test_simdutf, ci_test_strict_nul_handling, ci_test_delete_deprecated_functions, ci_test_no_thread_local]
steps:
- name: Install build-essential
run: apt-get update ; apt-get install -y build-essential unzip wget git
# the CMake 3.5.0 release binary that ci_cmake_flags downloads links
# against libidn.so.11, which Ubuntu 24.04 no longer ships
- name: Install libidn11 for CMake 3.5.0
if: matrix.target == 'ci_cmake_flags'
run: |
wget -q http://archive.ubuntu.com/ubuntu/pool/main/libi/libidn/libidn11_1.33-2.2ubuntu2_amd64.deb
dpkg -i libidn11_1.33-2.2ubuntu2_amd64.deb
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ${{ matrix.target }}
ci_test_coverage:
if: *nightly_set
runs-on: ubuntu-26.04
steps:
- name: Harden Runner
@@ -529,8 +208,128 @@ jobs:
path-to-lcov: ${{ github.workspace }}/build/json.info.filtered.noexcept
fail-on-error: false
ci_test_compilers_gcc_old:
runs-on: ubuntu-latest
strategy:
matrix:
compiler: ['4.8', '4.9', '5', '6']
# official gcc:4.8/4.9/5/6 images fail to check out code (too old for
# actions/checkout); install the old compilers on top of official ubuntu:20.04
# instead, mirroring what the (now retired) custom json-ci image did.
container: ubuntu:20.04
steps:
- name: Install g++-${{ matrix.compiler }}
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends software-properties-common ca-certificates gnupg make git
# add-apt-repository resolves the PPA through the Launchpad API,
# which intermittently times out or fails the team lookup (the plain
# "deb ..." sources below never hit Launchpad and never flake).
# Retry with backoff so a transient Launchpad blip does not fail CI.
for attempt in 1 2 3 4 5; do
add-apt-repository -y ppa:ubuntu-toolchain-r/test && break
echo "::warning::add-apt-repository ppa:ubuntu-toolchain-r/test failed (attempt ${attempt}/5); retrying"
sleep $((attempt * 10))
done
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ bionic main"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ bionic universe"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial main"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial universe"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial-updates main"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial-updates universe"
apt-get update
apt-get install -y --no-install-recommends g++-${{ matrix.compiler }}
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: CXX=g++-${{ matrix.compiler }} cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ci_test_compiler_g++-${{ matrix.compiler }}
ci_test_compilers_gcc:
runs-on: ubuntu-latest
strategy:
matrix:
# older GCC docker images (4, 5, 6) fail to check out code
compiler: ['7', '8', '9', '10', '11', '12', '13', '14', '15', '16', 'latest']
container: gcc:${{ matrix.compiler }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ci_test_compiler_default
ci_test_compilers_clang:
runs-on: ubuntu-latest
strategy:
matrix:
compiler: ['3.4', '3.5', '3.6', '3.7', '3.8', '3.9', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15-bullseye', '16', '17', '18', '19', '20', '21', '22', 'latest']
container: silkeh/clang:${{ matrix.compiler }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Disable filesystem support (std::filesystem is unusable) for clang 7 / 8 / 9 / 10
run: echo "JSON_FORCED_GLOBAL_COMPILE_OPTIONS=-DJSON_HAS_FILESYSTEM=0;-DJSON_HAS_EXPERIMENTAL_FILESYSTEM=0" >> "$GITHUB_ENV"
if: ${{ matrix.compiler == '7' || matrix.compiler == '8' || matrix.compiler == '9' || matrix.compiler == '10' }}
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ci_test_compiler_default
ci_test_standards_gcc:
runs-on: ubuntu-latest
container: gcc:latest
strategy:
matrix:
standard: [11, 14, 17, 20, 23, 26]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ci_test_gcc_cxx${{ matrix.standard }}
ci_test_standards_clang:
runs-on: ubuntu-latest
container: silkeh/clang:latest
strategy:
matrix:
standard: [11, 14, 17, 20, 23, 26]
stdlib: [libcxx, libstdcxx]
steps:
- name: Install git and unzip
run: apt-get update ; apt-get install -y git unzip
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build with libc++
run: cmake --build build --target ci_test_clang_libcxx_cxx${{ matrix.standard }}
if: ${{ matrix.stdlib == 'libcxx' }}
- name: Build with libstdc++
run: cmake --build build --target ci_test_clang_cxx${{ matrix.standard }}
if: ${{ matrix.stdlib == 'libstdcxx' }}
ci_cuda_example:
if: *nightly_set
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -553,8 +352,37 @@ jobs:
- name: Build
run: cmake --build build --target ci_cuda_example
ci_module_cpp20:
strategy:
matrix:
container: ['gcc:latest', 'silkeh/clang:latest']
runs-on: ubuntu-latest
container: ${{ matrix.container }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# The module test uses `import std;`, which needs CMake's experimental
# import-std support. Its opt-in token is CMake-version-specific, so pin
# CMake to the version whose token is set in tests/module_cpp20/CMakeLists.txt.
- name: Get pinned CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
with:
cmakeVersion: 4.3.4
# Clang: the std library module is provided by libc++ (the image's libstdc++
# ships none), and the image's libc++ module manifest has a broken relative
# path — repoint it at the real module sources.
- name: Use libc++ and fix its module manifest path (Clang)
if: matrix.container == 'silkeh/clang:latest'
run: |
echo "CXXFLAGS=-stdlib=libc++" >> "$GITHUB_ENV"
mkdir -p /usr/lib/share && ln -sf /usr/lib/llvm-*/share/libc++ /usr/lib/share/libc++
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ci_module_cpp20
ci_icpc:
if: *nightly_set
runs-on: ubuntu-latest
# Intel discontinued the classic icc/icpc compiler in oneAPI 2024.0; this is
# Intel's own last officially published image that still includes it.
@@ -574,7 +402,6 @@ jobs:
run: cmake --build build --target ci_icpc
ci_icpx:
if: *nightly_set
runs-on: ubuntu-latest
container: intel/oneapi-hpckit:latest
steps:
@@ -589,7 +416,6 @@ jobs:
run: cmake --build build --target ci_icpx
ci_nvhpc:
if: *nightly_set
runs-on: ubuntu-latest
container: nvcr.io/nvidia/nvhpc:25.5-devel-cuda12.9-ubuntu22.04
steps:
@@ -603,10 +429,54 @@ jobs:
- name: Build
run: cmake --build build --target ci_nvhpc
ci_emscripten:
runs-on: ubuntu-26.04
steps:
- name: Harden Runner
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
- name: Install emscripten
uses: mymindstorm/setup-emsdk@4528d102f7230f0e7b276855c01ea1159be0e984 # v16
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -GNinja
- name: Build
run: cmake --build build
ci_test_documentation:
runs-on: ubuntu-26.04
strategy:
matrix:
target: [ci_test_examples, ci_test_build_documentation, ci_test_documentation_mermaid]
steps:
- name: Harden Runner
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# the git-revision-date-localized plugin needs the history; a shallow clone makes the strict build fail
fetch-depth: ${{ matrix.target == 'ci_test_build_documentation' && '0' || '1' }}
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
if: matrix.target == 'ci_test_documentation_mermaid'
with:
node-version: 24
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ${{ matrix.target }}
# Native Linux ARM64 runner with the latest GCC. The ubuntu-24.04-arm label is
# only available for public repositories.
ci_test_arm64:
if: *nightly_set
runs-on: ubuntu-24.04-arm
container: gcc:latest
steps:
@@ -621,33 +491,3 @@ jobs:
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ci_test_compiler_default
# A failure of a scheduled run opens an issue, or comments on the open one.
nightly_report:
needs: [ci_infer, ci_static_analysis_ubuntu_nightly, ci_test_coverage, ci_cuda_example, ci_icpc, ci_icpx, ci_nvhpc, ci_test_arm64]
if: ${{ always() && github.event_name == 'schedule' && contains(needs.*.result, 'failure') }}
runs-on: ubuntu-26.04
permissions:
issues: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
- name: Open or update the nightly failure issue
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
WORKFLOW: ${{ github.workflow }}
SHA: ${{ github.sha }}
run: |
title="Nightly CI failure (${WORKFLOW})"
body="The nightly run of ${WORKFLOW} on ${SHA:0:10} failed: ${RUN_URL}"
number=$(gh issue list --state open --search "in:title \"${title}\"" --json number,title --jq "map(select(.title == \"${title}\")) | .[0].number // empty")
if [ -n "$number" ]; then
gh issue comment "$number" --body "$body"
else
gh issue create --title "$title" --body "$body"
fi
-19
View File
@@ -7,25 +7,6 @@ on:
- master
- release/*
pull_request:
# Skip pull requests that change only files no job here reads. The list is checked by
# tools/check_path_filters/check_path_filters.py (repository_checks.yml); change both together.
paths:
- '**'
- '!docs/**'
- '!**.md'
- '!.github/*'
- '!.github/ISSUE_TEMPLATE/**'
- '!.github/external_ci/**'
- '!.github/workflows/**'
- '!.reuse/**'
- '!LICENSES/**'
- '!CITATION.cff'
- '!REUSE.toml'
# read by tools/check_build_options in the ci_meson_install job (ubuntu.yml)
- 'docs/mkdocs/docs/integration/package_managers.md'
# built by the swiftpm job (macos.yml)
- 'docs/mkdocs/docs/integration/swift/**'
- '.github/workflows/windows.yml'
workflow_dispatch:
permissions:
@@ -13,72 +13,6 @@ describe each of them.
<p class="nj-caption" markdown>The checks grouped by the stage at which they run, from push or pull request through the merge into develop to a release. The badge on each card says whether the check runs on every push or PR, on a schedule, or continuously.</p>
## How CI runs
Not every job runs for every change. The compiler jobs on Ubuntu are split into three sets, so a pull request gets fast
feedback and the long jobs run when they matter:
| Set | When it runs | What it contains |
|-------------|-------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| PR set | every push to a pull request, and every push to `develop`, `master`, and `release/*` | GCC and Clang with all warnings, Clang-Tidy, sanitizers, IWYU, cpplint, the oldest and newest GCC and Clang, common CMake options, Meson, Emscripten; with the checks that always run, they would have caught at least 92 % of the failing commits of September and October 2026 |
| full set | after the PR set passed: on pull requests with the label :rocket: ready to merge, and on every push | every other compiler, version, C++ standard, and CMake option, the Clang Static Analyzer, Cppcheck, and the C++20 module tests |
| nightly set | every night on `develop`, on every push to `master` and `release/*`, and when started by hand | Infer, Valgrind, coverage, the Intel and NVIDIA compilers, CUDA, and ARM64; a failure opens an issue |
The jobs on macOS and Windows, the documentation and license checks, and the amalgamation check run on every push and
pull request. A pull request that changes only the documentation, Markdown files, GitHub metadata, or license files
skips the compiler workflows (see [`tools/check_path_filters`](https://github.com/nlohmann/json/tree/develop/tools/check_path_filters)).
A maintainer adds the label :rocket: ready to merge when a pull request is ready; the full set then runs, and again on
every later push to that pull request. A pull request is merged only when the full set passed.
??? abstract "Jobs of the compiler workflows and their sets"
The table is generated by [`tools/ci_jobs`](https://github.com/nlohmann/json/tree/develop/tools/ci_jobs), which
also checks in CI that it is up to date and that every job belongs to exactly one set.
<!-- ci_jobs: begin -->
| Set | Workflow | Job | Configurations |
|-----|----------|-----|----------------|
| PR set | ubuntu.yml | `ci_test_gcc` | - |
| PR set | ubuntu.yml | `ci_meson_install` | - |
| PR set | ubuntu.yml | `ci_static_analysis_ubuntu` | ci_cpplint |
| PR set | ubuntu.yml | `ci_static_analysis_clang` | ci_test_clang; ci_clang_tidy; ci_test_clang_sanitizer; ci_single_binaries |
| PR set | ubuntu.yml | `ci_cmake_options` | ci_test_diagnostics; ci_test_diagnostic_positions; ci_test_noexceptions; ci_test_noglobaludls |
| PR set | ubuntu.yml | `ci_test_compilers_gcc_old` | 4.8 |
| PR set | ubuntu.yml | `ci_test_compilers_clang` | 3.4 |
| PR set | ubuntu.yml | `ci_test_standards_gcc` | 26 |
| PR set | ubuntu.yml | `ci_test_standards_clang` | 26, libcxx |
| PR set | ubuntu.yml | `ci_emscripten` | - |
| full set | ubuntu.yml | `ci_static_analysis_ubuntu_full` | ci_test_single_header; ci_cppcheck; ci_reproducible_tests; ci_non_git_tests; ci_offline_testdata |
| full set | ubuntu.yml | `ci_static_analysis_clang_full` | ci_clang_analyze |
| full set | ubuntu.yml | `ci_cmake_options_full` | ci_cmake_flags; ci_test_noimplicitconversions; ci_test_legacycomparison; ci_test_disableenumserialization; ci_test_disabletuplereferenceconversion; ci_test_skiplibraryversioncheck; ci_test_simdutf; ci_test_strict_nul_handling; ci_test_delete_deprecated_functions; ci_test_no_thread_local |
| full set | ubuntu.yml | `ci_test_compilers_gcc_old_full` | 4.9; 5; 6 |
| full set | ubuntu.yml | `ci_test_compilers_gcc_full` | 7; 8; 9; 10; 11; 12; 13; 14; 15; 16; latest |
| full set | ubuntu.yml | `ci_test_compilers_clang_full` | 3.5; 3.6; 3.7; 3.8; 3.9; 4; 5; 6; 7; 8; 9; 10; 11; 12; 13; 14; 15-bullseye; 16; 17; 18; 19; 20; 21; 22; latest |
| full set | ubuntu.yml | `ci_test_standards_gcc_full` | 11; 14; 17; 20; 23 |
| full set | ubuntu.yml | `ci_test_standards_clang_full` | 11, libcxx; 11, libstdcxx; 14, libcxx; 14, libstdcxx; 17, libcxx; 17, libstdcxx; 20, libcxx; 20, libstdcxx; 23, libcxx; 23, libstdcxx; 26, libstdcxx |
| full set | ubuntu.yml | `ci_module_cpp20` | gcc:latest; silkeh/clang:latest |
| nightly set | ubuntu.yml | `ci_infer` | - |
| nightly set | ubuntu.yml | `ci_static_analysis_ubuntu_nightly` | ci_test_valgrind |
| nightly set | ubuntu.yml | `ci_test_coverage` | - |
| nightly set | ubuntu.yml | `ci_cuda_example` | 11.8.0; 12.1.1; 12.6.3 |
| nightly set | ubuntu.yml | `ci_icpc` | - |
| nightly set | ubuntu.yml | `ci_icpx` | - |
| nightly set | ubuntu.yml | `ci_nvhpc` | - |
| nightly set | ubuntu.yml | `ci_test_arm64` | - |
| every push and PR | macos.yml | `macos-15` | 16.0; 16.1; 16.2; 16.3; 16.4; 26.0.1; 26.1.1; 26.2; 26.3 |
| every push and PR | macos.yml | `macos-26` | 26.4.1; 26.5; 26.6 |
| every push and PR | macos.yml | `xcode_standards` | 11; 14; 17; 20; 23; 26 |
| every push and PR | macos.yml | `swiftpm` | - |
| every push and PR | windows.yml | `mingw` | x64; x86 |
| every push and PR | windows.yml | `msvc` | Debug, Win32, default; Debug, Win32, latest; Debug, x64, default; Debug, x64, latest; Release, Win32, default; Release, Win32, latest; Release, x64, default; Release, x64, latest |
| every push and PR | windows.yml | `msvc-vs2026` | Debug, Win32, default; Debug, Win32, latest; Debug, x64, default; Debug, x64, latest; Release, Win32, default; Release, Win32, latest; Release, x64, default; Release, x64, latest |
| every push and PR | windows.yml | `msvc-arm64` | Debug; Release |
| every push and PR | windows.yml | `clang` | 11.0.1; 12.0.1; 13.0.1; 14.0.6; 15.0.7; 16.0.6; 18.1.8; 19.1.7; 20.1.8 |
| every push and PR | windows.yml | `clang-cl` | Win32; x64 |
| every push and PR | windows.yml | `ci_module_cpp20` | - |
<!-- ci_jobs: end -->
## C++ language compliance and compiler compatibility
!!! success "Requirement: Compiler support"
+160 -174
View File
@@ -1,31 +1,26 @@
<svg class="nj-fig nj-exceptions-qa nj-exceptions-qa--wide" viewBox="0 0 760 862" role="img" aria-labelledby="nj-qa-title-w nj-qa-desc-w" xmlns="http://www.w3.org/2000/svg">
<svg class="nj-fig nj-exceptions-qa nj-exceptions-qa--wide" viewBox="0 0 760 808" role="img" aria-labelledby="nj-qa-title-w nj-qa-desc-w" xmlns="http://www.w3.org/2000/svg">
<title id="nj-qa-title-w">Quality assurance pipeline</title>
<desc id="nj-qa-desc-w">A push or pull request starts these checks; the badges say when each runs: Build matrix (every push / PR, before merge): 50+ compilers: G&#8203;CC, Clang, M&#8203;SVC, AppleClang, Intel, NVIDIA, ... C++11, 14, 17, 20, 23, and 26 libc++ and libstdc++ warnings: Clang -Weverything, G&#8203;CC 300+ warnings a subset on every push and PR, all before merge; Intel, NVIDIA, C&#8203;UDA, and ARM64 nightly; Unit tests (doctest) (every push / PR): every public A&#8203;PI function every exception id and message all Unicode code points single header and split headers custom number, string, array, and object types; Coverage (nightly): unit tests cover all lines of the code base; Static analysis (every push / PR, scheduled): Clang-Tidy, Cppcheck, Infer Clang Static Analyzer (52 checkers), I&#8203;WYU, Codacy CodeQL, Semgrep, Flawfinder (also scheduled); Infer nightly; Dynamic analysis (every push / PR, nightly): runtime assertions Valgrind (Memcheck), nightly address and undefined behavior sanitizers, integer overflow, nullability violations; Fuzzing (every PR, continuous): CIFuzz on every pull request OSS-Fuzz: billions of inputs every crash fixed with a reproducing unit test; Style (every push / PR): Artistic Style (astyle), enforced in the C&#8203;I cpplint with 61 rules; Docs and examples (every push / PR, scheduled): every documentation example is tested; changed output fails site built with warnings as errors; links checked regularly (scheduled); Integration, supply chain (every push / PR, scheduled): CMake 3.5, 3.31.6, and 4.0.0 Meson, Swift Package Manager dependency review on every PR Dependabot daily, OpenSSF Scorecard, R&#8203;EUSE. When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open.</desc>
<desc id="nj-qa-desc-w">Every push or pull request starts these checks in parallel: Build matrix (every push / PR): 50+ compilers: G&#8203;CC, Clang, M&#8203;SVC, AppleClang, Intel, NVIDIA, ... C++11, 14, 17, 20, 23, and 26 libc++ and libstdc++ warnings: Clang -Weverything, G&#8203;CC 300+ warnings; Unit tests (doctest) (every push / PR): every public A&#8203;PI function every exception id and message all Unicode code points single header and split headers custom number, string, array, and object types; Coverage (every push / PR): unit tests cover all lines of the code base; Static analysis (every push / PR, scheduled): Clang-Tidy, Cppcheck, Infer Clang Static Analyzer (52 checkers), I&#8203;WYU, Codacy CodeQL, Semgrep, Flawfinder (also scheduled); Dynamic analysis (every push / PR): runtime assertions Valgrind (Memcheck) address and undefined behavior sanitizers, integer overflow, nullability violations; Fuzzing (every PR, continuous): CIFuzz on every pull request OSS-Fuzz: billions of inputs every crash fixed with a reproducing unit test; Style (every push / PR): Artistic Style (astyle), enforced in the C&#8203;I cpplint with 61 rules; Docs and examples (every push / PR, scheduled): every documentation example is tested; changed output fails site built with warnings as errors; links checked regularly (scheduled); Integration, supply chain (every push / PR, scheduled): CMake 3.5, 3.31.6, and 4.0.0 Meson, Swift Package Manager dependency review on every PR Dependabot daily, OpenSSF Scorecard, R&#8203;EUSE. When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open.</desc>
<defs><marker id="nj-qa-arrow-w" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>
<rect class="nj-exceptions-groupbox" x="12" y="70" width="736" height="648" rx="12"/>
<rect class="nj-exceptions-groupbox" x="12" y="70" width="736" height="594" rx="12"/>
<rect class="nj-exceptions-stage" x="265" y="8" width="230" height="34" rx="17"/>
<text class="nj-exceptions-stagetext" x="380" y="30">push or pull request</text>
<line class="nj-exceptions-edge" x1="380" y1="42" x2="380" y2="68" marker-end="url(#nj-qa-arrow-w)"/>
<text class="nj-exceptions-group" x="26" y="94">checks, run in parallel on GitHub Actions and AppVeyor</text>
<g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="110" width="228" height="238" rx="8"/>
<rect class="nj-exceptions-box" x="26" y="110" width="228" height="184" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="135">Build matrix</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="146" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="160">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="156.5" y="146" width="91.6" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="202.3" y="160">before merge</text>
<text class="nj-exceptions-line" x="40" y="188">50+ compilers: G&#8203;CC, Clang, M&#8203;SVC,</text>
<text class="nj-exceptions-line" x="40" y="206">AppleClang, Intel, NVIDIA, ...</text>
<text class="nj-exceptions-line" x="40" y="224">C++11, 14, 17, 20, 23, and 26</text>
<text class="nj-exceptions-line" x="40" y="242">libc++ and libstdc++</text>
<text class="nj-exceptions-line" x="40" y="260">warnings: Clang -Weverything,</text>
<text class="nj-exceptions-line" x="40" y="278">G&#8203;CC 300+ warnings</text>
<text class="nj-exceptions-line" x="40" y="296">a subset on every push and PR,</text>
<text class="nj-exceptions-line" x="40" y="314">all before merge; Intel, NVIDIA,</text>
<text class="nj-exceptions-line" x="40" y="332">C&#8203;UDA, and ARM64 nightly</text>
</g>
<g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="266" y="110" width="228" height="238" rx="8"/>
<rect class="nj-exceptions-box" x="266" y="110" width="228" height="184" rx="8"/>
<text class="nj-exceptions-ctitle" x="280" y="135">Unit tests (doctest)</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="280" y="146" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="335.25" y="160">every push / PR</text>
@@ -37,218 +32,209 @@
<text class="nj-exceptions-line" x="280" y="278">and object types</text>
</g>
<g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="506" y="110" width="228" height="238" rx="8"/>
<rect class="nj-exceptions-box" x="506" y="110" width="228" height="184" rx="8"/>
<text class="nj-exceptions-ctitle" x="520" y="135">Coverage</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="520" y="146" width="60.1" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="550.05" y="160">nightly</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="520" y="146" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="575.25" y="160">every push / PR</text>
<text class="nj-exceptions-line" x="520" y="188">unit tests cover all lines</text>
<text class="nj-exceptions-line" x="520" y="206">of the code base</text>
</g>
<g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="360" width="228" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="385">Static analysis</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="396" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="410">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="396" width="72.7" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="410">scheduled</text>
<text class="nj-exceptions-line" x="40" y="438">Clang-Tidy, Cppcheck, Infer</text>
<text class="nj-exceptions-line" x="40" y="456">Clang Static Analyzer (52</text>
<text class="nj-exceptions-line" x="40" y="474">checkers), I&#8203;WYU, Codacy</text>
<text class="nj-exceptions-line" x="40" y="492">CodeQL, Semgrep, Flawfinder</text>
<text class="nj-exceptions-line" x="40" y="510">(also scheduled); Infer nightly</text>
<rect class="nj-exceptions-box" x="26" y="306" width="228" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="331">Static analysis</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="342" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="356">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="342" width="72.7" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="356">scheduled</text>
<text class="nj-exceptions-line" x="40" y="384">Clang-Tidy, Cppcheck, Infer</text>
<text class="nj-exceptions-line" x="40" y="402">Clang Static Analyzer (52</text>
<text class="nj-exceptions-line" x="40" y="420">checkers), I&#8203;WYU, Codacy</text>
<text class="nj-exceptions-line" x="40" y="438">CodeQL, Semgrep, Flawfinder</text>
<text class="nj-exceptions-line" x="40" y="456">(also scheduled)</text>
</g>
<g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="266" y="360" width="228" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="280" y="385">Dynamic analysis</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="280" y="396" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="335.25" y="410">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="396.5" y="396" width="60.1" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="426.55" y="410">nightly</text>
<text class="nj-exceptions-line" x="280" y="438">runtime assertions</text>
<text class="nj-exceptions-line" x="280" y="456">Valgrind (Memcheck), nightly</text>
<text class="nj-exceptions-line" x="280" y="474">address and undefined behavior</text>
<text class="nj-exceptions-line" x="280" y="492">sanitizers, integer overflow,</text>
<text class="nj-exceptions-line" x="280" y="510">nullability violations</text>
<rect class="nj-exceptions-box" x="266" y="306" width="228" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="280" y="331">Dynamic analysis</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="280" y="342" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="335.25" y="356">every push / PR</text>
<text class="nj-exceptions-line" x="280" y="384">runtime assertions</text>
<text class="nj-exceptions-line" x="280" y="402">Valgrind (Memcheck)</text>
<text class="nj-exceptions-line" x="280" y="420">address and undefined behavior</text>
<text class="nj-exceptions-line" x="280" y="438">sanitizers, integer overflow,</text>
<text class="nj-exceptions-line" x="280" y="456">nullability violations</text>
</g>
<g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="506" y="360" width="228" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="520" y="385">Fuzzing</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="520" y="396" width="66.4" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="553.2" y="410">every PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--cont" x="592.4" y="396" width="79" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--cont" x="631.9" y="410">continuous</text>
<text class="nj-exceptions-line" x="520" y="438">CIFuzz on every pull request</text>
<text class="nj-exceptions-line" x="520" y="456">OSS-Fuzz: billions of inputs</text>
<text class="nj-exceptions-line" x="520" y="474">every crash fixed with a</text>
<text class="nj-exceptions-line" x="520" y="492">reproducing unit test</text>
<rect class="nj-exceptions-box" x="506" y="306" width="228" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="520" y="331">Fuzzing</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="520" y="342" width="66.4" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="553.2" y="356">every PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--cont" x="592.4" y="342" width="79" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--cont" x="631.9" y="356">continuous</text>
<text class="nj-exceptions-line" x="520" y="384">CIFuzz on every pull request</text>
<text class="nj-exceptions-line" x="520" y="402">OSS-Fuzz: billions of inputs</text>
<text class="nj-exceptions-line" x="520" y="420">every crash fixed with a</text>
<text class="nj-exceptions-line" x="520" y="438">reproducing unit test</text>
</g>
<g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="538" width="228" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="563">Style</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="574" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="588">every push / PR</text>
<text class="nj-exceptions-line" x="40" y="616">Artistic Style (astyle),</text>
<text class="nj-exceptions-line" x="40" y="634">enforced in the C&#8203;I</text>
<text class="nj-exceptions-line" x="40" y="652">cpplint with 61 rules</text>
<rect class="nj-exceptions-box" x="26" y="484" width="228" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="509">Style</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="520" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="534">every push / PR</text>
<text class="nj-exceptions-line" x="40" y="562">Artistic Style (astyle),</text>
<text class="nj-exceptions-line" x="40" y="580">enforced in the C&#8203;I</text>
<text class="nj-exceptions-line" x="40" y="598">cpplint with 61 rules</text>
</g>
<g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="266" y="538" width="228" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="280" y="563">Docs and examples</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="280" y="574" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="335.25" y="588">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="396.5" y="574" width="72.7" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="432.85" y="588">scheduled</text>
<text class="nj-exceptions-line" x="280" y="616">every documentation example is</text>
<text class="nj-exceptions-line" x="280" y="634">tested; changed output fails</text>
<text class="nj-exceptions-line" x="280" y="652">site built with warnings</text>
<text class="nj-exceptions-line" x="280" y="670">as errors; links checked</text>
<text class="nj-exceptions-line" x="280" y="688">regularly (scheduled)</text>
<rect class="nj-exceptions-box" x="266" y="484" width="228" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="280" y="509">Docs and examples</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="280" y="520" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="335.25" y="534">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="396.5" y="520" width="72.7" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="432.85" y="534">scheduled</text>
<text class="nj-exceptions-line" x="280" y="562">every documentation example is</text>
<text class="nj-exceptions-line" x="280" y="580">tested; changed output fails</text>
<text class="nj-exceptions-line" x="280" y="598">site built with warnings</text>
<text class="nj-exceptions-line" x="280" y="616">as errors; links checked</text>
<text class="nj-exceptions-line" x="280" y="634">regularly (scheduled)</text>
</g>
<g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="506" y="538" width="228" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="520" y="563">Integration, supply chain</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="520" y="574" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="575.25" y="588">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="636.5" y="574" width="72.7" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="672.85" y="588">scheduled</text>
<text class="nj-exceptions-line" x="520" y="616">CMake 3.5, 3.31.6, and 4.0.0</text>
<text class="nj-exceptions-line" x="520" y="634">Meson, Swift Package Manager</text>
<text class="nj-exceptions-line" x="520" y="652">dependency review on every PR</text>
<text class="nj-exceptions-line" x="520" y="670">Dependabot daily, OpenSSF</text>
<text class="nj-exceptions-line" x="520" y="688">Scorecard, R&#8203;EUSE</text>
<rect class="nj-exceptions-box" x="506" y="484" width="228" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="520" y="509">Integration, supply chain</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="520" y="520" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="575.25" y="534">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="636.5" y="520" width="72.7" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="672.85" y="534">scheduled</text>
<text class="nj-exceptions-line" x="520" y="562">CMake 3.5, 3.31.6, and 4.0.0</text>
<text class="nj-exceptions-line" x="520" y="580">Meson, Swift Package Manager</text>
<text class="nj-exceptions-line" x="520" y="598">dependency review on every PR</text>
<text class="nj-exceptions-line" x="520" y="616">Dependabot daily, OpenSSF</text>
<text class="nj-exceptions-line" x="520" y="634">Scorecard, R&#8203;EUSE</text>
</g>
<line class="nj-exceptions-edge" x1="380" y1="718" x2="380" y2="744" marker-end="url(#nj-qa-arrow-w)"/>
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="265" y="746" width="230" height="34" rx="17"/>
<text class="nj-exceptions-stagetext" x="380" y="768">merge into develop</text>
<line class="nj-exceptions-edge" x1="380" y1="780" x2="380" y2="806" marker-end="url(#nj-qa-arrow-w)"/>
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="230" y="808" width="300" height="46" rx="23"/>
<text class="nj-exceptions-stagetext" x="380" y="830">release</text>
<text class="nj-exceptions-stagesub" x="380" y="846">no open vulnerability alert</text>
<line class="nj-exceptions-edge" x1="380" y1="664" x2="380" y2="690" marker-end="url(#nj-qa-arrow-w)"/>
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="265" y="692" width="230" height="34" rx="17"/>
<text class="nj-exceptions-stagetext" x="380" y="714">merge into develop</text>
<line class="nj-exceptions-edge" x1="380" y1="726" x2="380" y2="752" marker-end="url(#nj-qa-arrow-w)"/>
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="230" y="754" width="300" height="46" rx="23"/>
<text class="nj-exceptions-stagetext" x="380" y="776">release</text>
<text class="nj-exceptions-stagesub" x="380" y="792">no open vulnerability alert</text>
</svg>
<svg class="nj-fig nj-exceptions-qa nj-exceptions-qa--narrow" viewBox="0 0 360 1840" role="img" aria-labelledby="nj-qa-title-n nj-qa-desc-n" xmlns="http://www.w3.org/2000/svg">
<svg class="nj-fig nj-exceptions-qa nj-exceptions-qa--narrow" viewBox="0 0 360 1786" role="img" aria-labelledby="nj-qa-title-n nj-qa-desc-n" xmlns="http://www.w3.org/2000/svg">
<title id="nj-qa-title-n">Quality assurance pipeline</title>
<desc id="nj-qa-desc-n">A push or pull request starts these checks; the badges say when each runs: Build matrix (every push / PR, before merge): 50+ compilers: G&#8203;CC, Clang, M&#8203;SVC, AppleClang, Intel, NVIDIA, ... C++11, 14, 17, 20, 23, and 26 libc++ and libstdc++ warnings: Clang -Weverything, G&#8203;CC 300+ warnings a subset on every push and PR, all before merge; Intel, NVIDIA, C&#8203;UDA, and ARM64 nightly; Unit tests (doctest) (every push / PR): every public A&#8203;PI function every exception id and message all Unicode code points single header and split headers custom number, string, array, and object types; Coverage (nightly): unit tests cover all lines of the code base; Static analysis (every push / PR, scheduled): Clang-Tidy, Cppcheck, Infer Clang Static Analyzer (52 checkers), I&#8203;WYU, Codacy CodeQL, Semgrep, Flawfinder (also scheduled); Infer nightly; Dynamic analysis (every push / PR, nightly): runtime assertions Valgrind (Memcheck), nightly address and undefined behavior sanitizers, integer overflow, nullability violations; Fuzzing (every PR, continuous): CIFuzz on every pull request OSS-Fuzz: billions of inputs every crash fixed with a reproducing unit test; Style (every push / PR): Artistic Style (astyle), enforced in the C&#8203;I cpplint with 61 rules; Docs and examples (every push / PR, scheduled): every documentation example is tested; changed output fails site built with warnings as errors; links checked regularly (scheduled); Integration, supply chain (every push / PR, scheduled): CMake 3.5, 3.31.6, and 4.0.0 Meson, Swift Package Manager dependency review on every PR Dependabot daily, OpenSSF Scorecard, R&#8203;EUSE. When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open.</desc>
<desc id="nj-qa-desc-n">Every push or pull request starts these checks in parallel: Build matrix (every push / PR): 50+ compilers: G&#8203;CC, Clang, M&#8203;SVC, AppleClang, Intel, NVIDIA, ... C++11, 14, 17, 20, 23, and 26 libc++ and libstdc++ warnings: Clang -Weverything, G&#8203;CC 300+ warnings; Unit tests (doctest) (every push / PR): every public A&#8203;PI function every exception id and message all Unicode code points single header and split headers custom number, string, array, and object types; Coverage (every push / PR): unit tests cover all lines of the code base; Static analysis (every push / PR, scheduled): Clang-Tidy, Cppcheck, Infer Clang Static Analyzer (52 checkers), I&#8203;WYU, Codacy CodeQL, Semgrep, Flawfinder (also scheduled); Dynamic analysis (every push / PR): runtime assertions Valgrind (Memcheck) address and undefined behavior sanitizers, integer overflow, nullability violations; Fuzzing (every PR, continuous): CIFuzz on every pull request OSS-Fuzz: billions of inputs every crash fixed with a reproducing unit test; Style (every push / PR): Artistic Style (astyle), enforced in the C&#8203;I cpplint with 61 rules; Docs and examples (every push / PR, scheduled): every documentation example is tested; changed output fails site built with warnings as errors; links checked regularly (scheduled); Integration, supply chain (every push / PR, scheduled): CMake 3.5, 3.31.6, and 4.0.0 Meson, Swift Package Manager dependency review on every PR Dependabot daily, OpenSSF Scorecard, R&#8203;EUSE. When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open.</desc>
<defs><marker id="nj-qa-arrow-n" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>
<rect class="nj-exceptions-groupbox" x="12" y="70" width="336" height="1626" rx="12"/>
<rect class="nj-exceptions-groupbox" x="12" y="70" width="336" height="1572" rx="12"/>
<rect class="nj-exceptions-stage" x="65" y="8" width="230" height="34" rx="17"/>
<text class="nj-exceptions-stagetext" x="180" y="30">push or pull request</text>
<line class="nj-exceptions-edge" x1="180" y1="42" x2="180" y2="68" marker-end="url(#nj-qa-arrow-n)"/>
<text class="nj-exceptions-group" x="26" y="94">checks, run in parallel</text>
<g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="110" width="308" height="238" rx="8"/>
<rect class="nj-exceptions-box" x="26" y="110" width="308" height="184" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="135">Build matrix</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="146" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="160">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="156.5" y="146" width="91.6" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="202.3" y="160">before merge</text>
<text class="nj-exceptions-line" x="40" y="188">50+ compilers: G&#8203;CC, Clang, M&#8203;SVC,</text>
<text class="nj-exceptions-line" x="40" y="206">AppleClang, Intel, NVIDIA, ...</text>
<text class="nj-exceptions-line" x="40" y="224">C++11, 14, 17, 20, 23, and 26</text>
<text class="nj-exceptions-line" x="40" y="242">libc++ and libstdc++</text>
<text class="nj-exceptions-line" x="40" y="260">warnings: Clang -Weverything,</text>
<text class="nj-exceptions-line" x="40" y="278">G&#8203;CC 300+ warnings</text>
<text class="nj-exceptions-line" x="40" y="296">a subset on every push and PR,</text>
<text class="nj-exceptions-line" x="40" y="314">all before merge; Intel, NVIDIA,</text>
<text class="nj-exceptions-line" x="40" y="332">C&#8203;UDA, and ARM64 nightly</text>
</g>
<g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="360" width="308" height="184" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="385">Unit tests (doctest)</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="396" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="410">every push / PR</text>
<text class="nj-exceptions-line" x="40" y="438">every public A&#8203;PI function</text>
<text class="nj-exceptions-line" x="40" y="456">every exception id and message</text>
<text class="nj-exceptions-line" x="40" y="474">all Unicode code points</text>
<text class="nj-exceptions-line" x="40" y="492">single header and split headers</text>
<text class="nj-exceptions-line" x="40" y="510">custom number, string, array,</text>
<text class="nj-exceptions-line" x="40" y="528">and object types</text>
<rect class="nj-exceptions-box" x="26" y="306" width="308" height="184" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="331">Unit tests (doctest)</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="342" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="356">every push / PR</text>
<text class="nj-exceptions-line" x="40" y="384">every public A&#8203;PI function</text>
<text class="nj-exceptions-line" x="40" y="402">every exception id and message</text>
<text class="nj-exceptions-line" x="40" y="420">all Unicode code points</text>
<text class="nj-exceptions-line" x="40" y="438">single header and split headers</text>
<text class="nj-exceptions-line" x="40" y="456">custom number, string, array,</text>
<text class="nj-exceptions-line" x="40" y="474">and object types</text>
</g>
<g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="556" width="308" height="112" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="581">Coverage</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="40" y="592" width="60.1" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="70.05" y="606">nightly</text>
<text class="nj-exceptions-line" x="40" y="634">unit tests cover all lines</text>
<text class="nj-exceptions-line" x="40" y="652">of the code base</text>
<rect class="nj-exceptions-box" x="26" y="502" width="308" height="112" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="527">Coverage</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="538" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="552">every push / PR</text>
<text class="nj-exceptions-line" x="40" y="580">unit tests cover all lines</text>
<text class="nj-exceptions-line" x="40" y="598">of the code base</text>
</g>
<g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="680" width="308" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="705">Static analysis</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="716" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="730">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="716" width="72.7" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="730">scheduled</text>
<text class="nj-exceptions-line" x="40" y="758">Clang-Tidy, Cppcheck, Infer</text>
<text class="nj-exceptions-line" x="40" y="776">Clang Static Analyzer (52</text>
<text class="nj-exceptions-line" x="40" y="794">checkers), I&#8203;WYU, Codacy</text>
<text class="nj-exceptions-line" x="40" y="812">CodeQL, Semgrep, Flawfinder</text>
<text class="nj-exceptions-line" x="40" y="830">(also scheduled); Infer nightly</text>
<rect class="nj-exceptions-box" x="26" y="626" width="308" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="651">Static analysis</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="662" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="676">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="662" width="72.7" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="676">scheduled</text>
<text class="nj-exceptions-line" x="40" y="704">Clang-Tidy, Cppcheck, Infer</text>
<text class="nj-exceptions-line" x="40" y="722">Clang Static Analyzer (52</text>
<text class="nj-exceptions-line" x="40" y="740">checkers), I&#8203;WYU, Codacy</text>
<text class="nj-exceptions-line" x="40" y="758">CodeQL, Semgrep, Flawfinder</text>
<text class="nj-exceptions-line" x="40" y="776">(also scheduled)</text>
</g>
<g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="858" width="308" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="883">Dynamic analysis</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="894" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="908">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="894" width="60.1" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="186.55" y="908">nightly</text>
<text class="nj-exceptions-line" x="40" y="936">runtime assertions</text>
<text class="nj-exceptions-line" x="40" y="954">Valgrind (Memcheck), nightly</text>
<text class="nj-exceptions-line" x="40" y="972">address and undefined behavior</text>
<text class="nj-exceptions-line" x="40" y="990">sanitizers, integer overflow,</text>
<text class="nj-exceptions-line" x="40" y="1008">nullability violations</text>
<rect class="nj-exceptions-box" x="26" y="804" width="308" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="829">Dynamic analysis</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="840" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="854">every push / PR</text>
<text class="nj-exceptions-line" x="40" y="882">runtime assertions</text>
<text class="nj-exceptions-line" x="40" y="900">Valgrind (Memcheck)</text>
<text class="nj-exceptions-line" x="40" y="918">address and undefined behavior</text>
<text class="nj-exceptions-line" x="40" y="936">sanitizers, integer overflow,</text>
<text class="nj-exceptions-line" x="40" y="954">nullability violations</text>
</g>
<g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="1036" width="308" height="148" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="1061">Fuzzing</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1072" width="66.4" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="73.2" y="1086">every PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--cont" x="112.4" y="1072" width="79" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--cont" x="151.9" y="1086">continuous</text>
<text class="nj-exceptions-line" x="40" y="1114">CIFuzz on every pull request</text>
<text class="nj-exceptions-line" x="40" y="1132">OSS-Fuzz: billions of inputs</text>
<text class="nj-exceptions-line" x="40" y="1150">every crash fixed with a</text>
<text class="nj-exceptions-line" x="40" y="1168">reproducing unit test</text>
<rect class="nj-exceptions-box" x="26" y="982" width="308" height="148" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="1007">Fuzzing</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1018" width="66.4" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="73.2" y="1032">every PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--cont" x="112.4" y="1018" width="79" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--cont" x="151.9" y="1032">continuous</text>
<text class="nj-exceptions-line" x="40" y="1060">CIFuzz on every pull request</text>
<text class="nj-exceptions-line" x="40" y="1078">OSS-Fuzz: billions of inputs</text>
<text class="nj-exceptions-line" x="40" y="1096">every crash fixed with a</text>
<text class="nj-exceptions-line" x="40" y="1114">reproducing unit test</text>
</g>
<g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="1196" width="308" height="130" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="1221">Style</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1232" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="1246">every push / PR</text>
<text class="nj-exceptions-line" x="40" y="1274">Artistic Style (astyle),</text>
<text class="nj-exceptions-line" x="40" y="1292">enforced in the C&#8203;I</text>
<text class="nj-exceptions-line" x="40" y="1310">cpplint with 61 rules</text>
<rect class="nj-exceptions-box" x="26" y="1142" width="308" height="130" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="1167">Style</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1178" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="1192">every push / PR</text>
<text class="nj-exceptions-line" x="40" y="1220">Artistic Style (astyle),</text>
<text class="nj-exceptions-line" x="40" y="1238">enforced in the C&#8203;I</text>
<text class="nj-exceptions-line" x="40" y="1256">cpplint with 61 rules</text>
</g>
<g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="1338" width="308" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="1363">Docs and examples</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1374" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="1388">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="1374" width="72.7" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="1388">scheduled</text>
<text class="nj-exceptions-line" x="40" y="1416">every documentation example is</text>
<text class="nj-exceptions-line" x="40" y="1434">tested; changed output fails</text>
<text class="nj-exceptions-line" x="40" y="1452">site built with warnings</text>
<text class="nj-exceptions-line" x="40" y="1470">as errors; links checked</text>
<text class="nj-exceptions-line" x="40" y="1488">regularly (scheduled)</text>
<rect class="nj-exceptions-box" x="26" y="1284" width="308" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="1309">Docs and examples</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1320" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="1334">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="1320" width="72.7" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="1334">scheduled</text>
<text class="nj-exceptions-line" x="40" y="1362">every documentation example is</text>
<text class="nj-exceptions-line" x="40" y="1380">tested; changed output fails</text>
<text class="nj-exceptions-line" x="40" y="1398">site built with warnings</text>
<text class="nj-exceptions-line" x="40" y="1416">as errors; links checked</text>
<text class="nj-exceptions-line" x="40" y="1434">regularly (scheduled)</text>
</g>
<g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="1516" width="308" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="1541">Integration, supply chain</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1552" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="1566">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="1552" width="72.7" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="1566">scheduled</text>
<text class="nj-exceptions-line" x="40" y="1594">CMake 3.5, 3.31.6, and 4.0.0</text>
<text class="nj-exceptions-line" x="40" y="1612">Meson, Swift Package Manager</text>
<text class="nj-exceptions-line" x="40" y="1630">dependency review on every PR</text>
<text class="nj-exceptions-line" x="40" y="1648">Dependabot daily, OpenSSF</text>
<text class="nj-exceptions-line" x="40" y="1666">Scorecard, R&#8203;EUSE</text>
<rect class="nj-exceptions-box" x="26" y="1462" width="308" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="1487">Integration, supply chain</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1498" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="1512">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="1498" width="72.7" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="1512">scheduled</text>
<text class="nj-exceptions-line" x="40" y="1540">CMake 3.5, 3.31.6, and 4.0.0</text>
<text class="nj-exceptions-line" x="40" y="1558">Meson, Swift Package Manager</text>
<text class="nj-exceptions-line" x="40" y="1576">dependency review on every PR</text>
<text class="nj-exceptions-line" x="40" y="1594">Dependabot daily, OpenSSF</text>
<text class="nj-exceptions-line" x="40" y="1612">Scorecard, R&#8203;EUSE</text>
</g>
<line class="nj-exceptions-edge" x1="180" y1="1696" x2="180" y2="1722" marker-end="url(#nj-qa-arrow-n)"/>
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="65" y="1724" width="230" height="34" rx="17"/>
<text class="nj-exceptions-stagetext" x="180" y="1746">merge into develop</text>
<line class="nj-exceptions-edge" x1="180" y1="1758" x2="180" y2="1784" marker-end="url(#nj-qa-arrow-n)"/>
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="30" y="1786" width="300" height="46" rx="23"/>
<text class="nj-exceptions-stagetext" x="180" y="1808">release</text>
<text class="nj-exceptions-stagesub" x="180" y="1824">no open vulnerability alert</text>
<line class="nj-exceptions-edge" x1="180" y1="1642" x2="180" y2="1668" marker-end="url(#nj-qa-arrow-n)"/>
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="65" y="1670" width="230" height="34" rx="17"/>
<text class="nj-exceptions-stagetext" x="180" y="1692">merge into develop</text>
<line class="nj-exceptions-edge" x1="180" y1="1704" x2="180" y2="1730" marker-end="url(#nj-qa-arrow-n)"/>
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="30" y="1732" width="300" height="46" rx="23"/>
<text class="nj-exceptions-stagetext" x="180" y="1754">release</text>
<text class="nj-exceptions-stagesub" x="180" y="1770">no open vulnerability alert</text>
</svg>

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 22 KiB

+6 -11
View File
@@ -22,24 +22,19 @@ P = "nj-exceptions-"
TRIGGERS = {
"push": ("every push / PR", "push"),
"pr": ("every PR", "push"),
"merge": ("before merge", "push"),
"nightly": ("nightly", "sched"),
"sched": ("scheduled", "sched"),
"cont": ("continuous", "cont"),
}
# title, triggers, lines (at most 31 characters, so they fit a card of the three-column drawing)
LANES = [
("Build matrix", ["push", "merge"], [
("Build matrix", ["push"], [
"50+ compilers: GCC, Clang, MSVC,",
"AppleClang, Intel, NVIDIA, ...",
"C++11, 14, 17, 20, 23, and 26",
"libc++ and libstdc++",
"warnings: Clang -Weverything,",
"GCC 300+ warnings",
"a subset on every push and PR,",
"all before merge; Intel, NVIDIA,",
"CUDA, and ARM64 nightly",
]),
("Unit tests (doctest)", ["push"], [
"every public API function",
@@ -49,7 +44,7 @@ LANES = [
"custom number, string, array,",
"and object types",
]),
("Coverage", ["nightly"], [
("Coverage", ["push"], [
"unit tests cover all lines",
"of the code base",
]),
@@ -58,11 +53,11 @@ LANES = [
"Clang Static Analyzer (52",
"checkers), IWYU, Codacy",
"CodeQL, Semgrep, Flawfinder",
"(also scheduled); Infer nightly",
"(also scheduled)",
]),
("Dynamic analysis", ["push", "nightly"], [
("Dynamic analysis", ["push"], [
"runtime assertions",
"Valgrind (Memcheck), nightly",
"Valgrind (Memcheck)",
"address and undefined behavior",
"sanitizers, integer overflow,",
"nullability violations",
@@ -183,7 +178,7 @@ def drawing(W, cols, suffix):
defs = ('<defs><marker id="%s" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">'
'<path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>' % mid)
tid, did = "nj-qa-title-" + suffix, "nj-qa-desc-" + suffix
desc = ("A push or pull request starts these checks; the badges say when each runs: " +
desc = ("Every push or pull request starts these checks in parallel: " +
"; ".join("%s (%s): %s" % (t, ", ".join(TRIGGERS[k][0] for k in trig), " ".join(lines)) for t, trig, lines in LANES) +
". When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open.")
out = ['<svg class="nj-fig %sqa %sqa--%s" viewBox="0 0 %d %d" role="img" aria-labelledby="%s %s" xmlns="http://www.w3.org/2000/svg">'
+27 -1
View File
@@ -21,6 +21,10 @@ python3 tests/benchmarks/json_view/compare.py --data <json_test_data directory>
- The other libraries come from the system: pkg-config, or Homebrew (`brew install yyjson simdjson boost`). With
`--download`, pinned releases are downloaded instead and checked against their SHA-256. Without Boost headers (or
with `--no-boost`), the Boost.JSON columns are skipped, and the results say so.
- `--aligned` additionally builds and runs everything with all functions aligned to 64 bytes (`-mllvm
-align-all-functions=6` for Clang, `-falign-functions=64 -falign-loops=64` for GCC), and reports both result
tables as "default build" and "aligned build". Code layout alone can move a result by a few percent; a difference
that appears in only one of the two tables is a layout artifact. Off by default, which doubles the run time.
- `--corpus file...` adds files to the corpus benchmark, e.g. those of
[simdjson-data](https://github.com/simdjson/simdjson-data) or the
[yyjson benchmark](https://github.com/ibireme/yyjson_benchmark).
@@ -39,7 +43,7 @@ where `json_view` stands on another architecture; they are not meant for publica
## What is measured
`bench_view.cpp` runs four workloads on twitter, citm_catalog, canada, jeopardy, a single tweet (`status`), and a
`bench_view.cpp` runs these workloads on twitter, citm_catalog, canada, jeopardy, a single tweet (`status`), and a
JSON-RPC request (`rpc`):
| workload | what it does |
@@ -48,6 +52,7 @@ JSON-RPC request (`rpc`):
| traverse | parse, then visit every value, convert every number, touch every string and key |
| select | parse, then read a few fields per record (e.g. id, user name, and retweet count of each tweet) |
| dump | serialize a parsed document (compact) |
| parse+dump | build a fresh document, then serialize it (compact), in one timed call |
`bench_corpus.cpp` runs parse, traverse, and dump on any list of files, so that no library is tuned to a handful of
documents. Its dump also writes the numbers as they are in the input: `json_view` with `number_format::source`, and
@@ -72,11 +77,32 @@ call follows an untimed call of the same engine: otherwise the engine after `jso
cleaning up the tens of thousands of nodes `json::parse` just freed (with glibc, this made `json_view` look 1.7 times
slower on citm_catalog traverse).
`json_view` is an index into the source text, not a tree of owning nodes. Its like-for-like peers are yyjson and
simdjson DOM (immutable, indexed, no per-node allocation). Boost.JSON and `json::parse` build mutable, owning DOMs and
are reference points for what that costs, not equals; simdjson On-Demand is lazy and forward-only, so it is the other
way round: it has no random access to pay for. All engines validate UTF-8 and syntax by default.
Number conversion differs: `json_view` converts integers while parsing but converts floating-point numbers lazily,
when they are read or dumped; yyjson (default flags) and simdjson DOM convert every number while parsing. The cost of
float conversion therefore appears in the traverse and dump rows for `json_view`, and in the parse row for the others.
For float-heavy documents (canada), the fair reading is a sum of parse and use: the `traverse` workload already
includes the parse, and `parse+dump` measures a fresh document and its dump in one timed call; the `dump` row alone
leaves the conversion in `json_view` and out of the others. Eager conversion at parse time was prototyped as an opt-in
and measured: canada traverse (with its parse) was 4-7 % faster, but parse+dump 11-30 % slower and parse 36-90 % slower. It was
dropped, so lazy conversion is the only mode.
`json_view` can also be edited: `json_editable_document` has the same read API, and the `(editable)` rows in
`bench_view` measure it on the same workloads. They are the fair counterpart of Boost.JSON and `json::parse`, which are
mutable too; the plain `json_view` rows are the counterpart of yyjson and simdjson DOM, which are not. `bench_edit` adds
the read-modify-write workloads. Read the tables with that in mind: an immutable-only library is not compared with an
editable `json_view` unless it says so.
The engines do not all offer the same features, which the numbers should be read with:
| engine | document | random access | editable | notes |
|---|---|---|---|---|
| `json_view` | immutable index into the text | yes | no | a fresh document per parse; "reused" parses into the same document |
| `json_view` (editable) | index into the text plus edits | yes | yes, in place | `json_editable_document`; keeps member order and number spellings |
| yyjson | immutable (`yyjson_read`) | yes | via a mutable copy | |
| simdjson DOM | immutable, parser reused | yes | no | "fresh" uses a new parser per parse |
| simdjson On-Demand | none: forward-only, lazy | no | no | only traverse and select |
+92 -24
View File
@@ -8,8 +8,9 @@
// Same-feature-set benchmark: read-only JSON documents with random access.
//
// json_view nlohmann/json_view.hpp (fresh document per parse / reused)
// yyjson yyjson_read(): immutable document, random access
// json_view nlohmann/json_view.hpp (fresh document per parse / reused;
// "(editable)": json_editable_document, same read API)
// yyjson yyjson_read(): immutable document, random access
// simdjson DOM dom::parser (reused, as recommended; "fresh": a new parser
// per parse): immutable, random access
// references (different feature sets):
@@ -19,7 +20,11 @@
//
// Workloads: parse (build + free), traverse (visit everything, convert every
// number, touch every string and key), select (a few fields per document),
// dump (compact serialization of the parsed document).
// dump (compact serialization of the parsed document), and parse+dump (a fresh
// document, then its dump, in one timed call). json_view converts floating-point
// numbers lazily (when read or dumped), yyjson and simdjson DOM while parsing:
// for float-heavy documents, traverse (which includes the parse) and parse+dump
// are the fair reading of the parse and dump rows.
// All engines run interleaved in every round, each timed call after an untimed
// one of the same engine; the best round is reported.
#include <nlohmann/json_view.hpp>
@@ -43,6 +48,8 @@
using nlohmann::json;
using nlohmann::json_document;
using nlohmann::json_editable_document;
using nlohmann::json_editable_view;
using nlohmann::json_view;
static volatile double g_sink;
@@ -55,7 +62,8 @@ struct stats
// ---------------- traversal ----------------
static void walk(json_view v, stats& st)
template<class V>
static void walk_view(V v, stats& st)
{
++st.nodes;
switch (v.type())
@@ -64,32 +72,42 @@ static void walk(json_view v, stats& st)
for (auto it = v.begin(); it != v.end(); ++it)
{
st.str += it.key().size();
walk(*it, st);
walk_view<V>(*it, st);
}
break;
case json::value_t::array:
for (const json_view e : v)
for (const V e : v)
{
walk(e, st);
walk_view<V>(e, st);
}
break;
case json::value_t::string:
st.str += v.get_string().size();
break;
case json::value_t::number_integer:
st.num += static_cast<double>(v.get<std::int64_t>());
st.num += static_cast<double>(v.template get<std::int64_t>());
break;
case json::value_t::number_unsigned:
st.num += static_cast<double>(v.get<std::uint64_t>());
st.num += static_cast<double>(v.template get<std::uint64_t>());
break;
case json::value_t::number_float:
st.num += v.get<double>();
st.num += v.template get<double>();
break;
default:
break;
}
}
static void walk(json_view v, stats& st)
{
walk_view<json_view>(v, st);
}
static void walk(json_editable_view v, stats& st)
{
walk_view<json_editable_view>(v, st);
}
static void walk(const json& j, stats& st)
{
++st.nodes;
@@ -300,20 +318,21 @@ static void walk(const boost::json::value& v, stats& st)
// status (one tweet): id, user.screen_name, retweet_count
// rpc: method, params.minuend, id
static double pick(const std::string& name, json_view r)
template<class V>
static double pick_view(const std::string& name, V r)
{
double acc = 0;
if (name == "twitter" || name == "status")
{
auto one = [&](json_view s)
auto one = [&](V s)
{
acc += static_cast<double>(s["id"].get<std::uint64_t>());
acc += static_cast<double>(s["id"].template get<std::uint64_t>());
acc += static_cast<double>(s["user"]["screen_name"].get_string().size());
acc += static_cast<double>(s["retweet_count"].get<std::int64_t>());
acc += static_cast<double>(s["retweet_count"].template get<std::int64_t>());
};
if (name == "twitter")
{
for (const json_view s : r["statuses"])
for (const V s : r["statuses"])
{
one(s);
}
@@ -325,20 +344,20 @@ static double pick(const std::string& name, json_view r)
}
else if (name == "citm_catalog")
{
for (const json_view p : r["performances"])
for (const V p : r["performances"])
{
acc += static_cast<double>(p["id"].get<std::uint64_t>() + p["eventId"].get<std::uint64_t>() + p["seatCategories"].size());
acc += static_cast<double>(p["id"].template get<std::uint64_t>() + p["eventId"].template get<std::uint64_t>() + p["seatCategories"].size());
}
acc += static_cast<double>(r["events"].size());
}
else if (name == "canada")
{
const json_view g = r["features"][0]["geometry"];
const V g = r["features"][0]["geometry"];
acc += static_cast<double>(r["type"].get_string().size() + g["type"].get_string().size() + g["coordinates"].size());
}
else if (name == "jeopardy")
{
for (const json_view q : r)
for (const V q : r)
{
acc += q["round"].get_string() == "Final Jeopardy!" ? 1 : 0;
acc += static_cast<double>(q["category"].get_string().size());
@@ -347,11 +366,21 @@ static double pick(const std::string& name, json_view r)
else if (name == "rpc")
{
acc += static_cast<double>(r["method"].get_string().size());
acc += static_cast<double>(r["params"]["minuend"].get<std::int64_t>() + r["id"].get<std::int64_t>());
acc += static_cast<double>(r["params"]["minuend"].template get<std::int64_t>() + r["id"].template get<std::int64_t>());
}
return acc;
}
static double pick(const std::string& name, json_view r)
{
return pick_view<json_view>(name, r);
}
static double pick(const std::string& name, json_editable_view r)
{
return pick_view<json_editable_view>(name, r);
}
static double pick(const std::string& name, const json& r)
{
double acc = 0;
@@ -634,15 +663,35 @@ int main(int argc, char** argv)
walk(json::parse(dc.text), a);
auto d = json_document::parse(dc.text);
walk(d.root(), b);
auto ed = json_editable_document::parse(dc.text);
stats e;
walk(ed.root(), e);
yyjson_doc* y = yyjson_read(dc.text.data(), dc.text.size(), 0);
walk(yyjson_doc_get_root(y), c);
simdjson::dom::parser p;
walk(p.parse(dc.text).value(), dd);
const bool ok = a.nodes == b.nodes && a.nodes == c.nodes && a.nodes == dd.nodes && a.str == b.str && a.str == c.str && a.str == dd.str
const bool ok = a.nodes == b.nodes && a.nodes == e.nodes && a.str == e.str && std::fabs(a.num - e.num) <= 1e-9 * std::fabs(a.num) && a.nodes == c.nodes && a.nodes == dd.nodes && a.str == b.str && a.str == c.str && a.str == dd.str
&& std::fabs(a.num - b.num) <= 1e-9 * std::fabs(a.num) && std::fabs(a.num - c.num) <= 1e-9 * std::fabs(a.num);
const double pa = pick(dc.name, json::parse(dc.text)), pb = pick(dc.name, d.root()), pc = pick(dc.name, yyjson_doc_get_root(y)), pd = pick(dc.name, p.parse(dc.text).value());
std::printf("check %-13s traverse %s select %s\n", dc.name.c_str(), ok ? "OK" : "MISMATCH", (pa == pb && pa == pc && pa == pd) ? "OK" : "MISMATCH");
const double pa = pick(dc.name, json::parse(dc.text)), pb = pick(dc.name, d.root()), pe = pick(dc.name, ed.root()), pc = pick(dc.name, yyjson_doc_get_root(y)), pd = pick(dc.name, p.parse(dc.text).value());
std::printf("check %-13s traverse %s select %s\n", dc.name.c_str(), ok ? "OK" : "MISMATCH", (pa == pb && pa == pe && pa == pc && pa == pd) ? "OK" : "MISMATCH");
yyjson_doc_free(y);
// the dumps of the engines may format floats differently, so only: every dump is non-empty, and the
// dump of json_view (also editable) and of json::parse parse back to the same number of nodes
const std::string dj = json::parse(dc.text).dump();
const std::string dv = d.root().dump();
const std::string de = ed.root().dump();
std::size_t yn = 0;
yyjson_doc* y2 = yyjson_read(dc.text.data(), dc.text.size(), 0);
char* yo = yyjson_write(y2, 0, &yn);
const bool dumps_ok = !dj.empty() && !dv.empty() && !de.empty() && yo != nullptr && yn > 0 && !simdjson::to_string(p.parse(dc.text).value()).empty();
std::free(yo);
yyjson_doc_free(y2);
stats sj_nodes, sv_nodes, se_nodes;
walk(json::parse(dj), sj_nodes);
walk(json::parse(dv), sv_nodes);
walk(json::parse(de), se_nodes);
std::printf("check %-13s dump %s\n", dc.name.c_str(), (dumps_ok && sj_nodes.nodes == a.nodes && sv_nodes.nodes == a.nodes && se_nodes.nodes == a.nodes) ? "OK" : "MISMATCH");
}
std::FILE* csv = std::fopen("bench_view.csv", "w");
@@ -669,6 +718,7 @@ int main(int argc, char** argv)
workloads.push_back({"parse", {
{"json_view", [&] { auto d = json_document::parse(s); g_sink = static_cast<double>(d.node_count()); }},
{"json_view (reused)", [&] { reused.read(s); g_sink = static_cast<double>(reused.node_count()); }},
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); g_sink = static_cast<double>(d.root().size()); }},
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = static_cast<double>(yyjson_doc_get_val_count(d)); yyjson_doc_free(d); }},
{"simdjson DOM", [&] { auto e = sj.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
{"simdjson DOM (fresh)", [&] { simdjson::dom::parser p; auto e = p.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
@@ -680,6 +730,7 @@ int main(int argc, char** argv)
workloads.push_back({"traverse", {
{"json_view", [&] { auto d = json_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }},
{"json_view (reused)", [&] { reused.read(s); stats st; walk(reused.root(), st); g_sink = st.num; }},
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }},
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); stats st; walk(yyjson_doc_get_root(d), st); g_sink = st.num; yyjson_doc_free(d); }},
{"simdjson DOM", [&] { stats st; walk(sj.parse(ps).value_unsafe(), st); g_sink = st.num; }},
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); stats st; walk_od(d.get_value().value_unsafe(), st); g_sink = st.num; }},
@@ -691,15 +742,31 @@ int main(int argc, char** argv)
workloads.push_back({"select", {
{"json_view", [&] { auto d = json_document::parse(s); g_sink = pick(name, d.root()); }},
{"json_view (reused)", [&] { reused.read(s); g_sink = pick(name, reused.root()); }},
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); g_sink = pick(name, d.root()); }},
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = pick(name, yyjson_doc_get_root(d)); yyjson_doc_free(d); }},
{"simdjson DOM", [&] { g_sink = pick(name, sj.parse(ps).value_unsafe()); }},
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); g_sink = pick_od(name, d); }},
{"json::parse", [&] { json j = json::parse(s); g_sink = pick(name, j); }},
}});
// pipeline: a fresh document, then a dump of it, in one timed call
// (simdjson DOM reuses its parser, as in the parse row); the traverse
// workload above is already parse+traverse
workloads.push_back({"parse+dump", {
{"json_view", [&] { auto d = json_document::parse(s); std::string o = d.root().dump(); g_sink = static_cast<double>(o.size()); }},
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); std::string o = d.root().dump(); g_sink = static_cast<double>(o.size()); }},
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); std::size_t n = 0; char* o = yyjson_write(d, 0, &n); g_sink = static_cast<double>(n); std::free(o); yyjson_doc_free(d); }},
{"simdjson DOM", [&] { std::string o = simdjson::to_string(sj.parse(ps).value_unsafe()); g_sink = static_cast<double>(o.size()); }},
#if JSON_VIEW_BENCH_BOOST
{"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); std::string o = boost::json::serialize(v); g_sink = static_cast<double>(o.size()); }},
#endif
{"json::parse", [&] { json j = json::parse(s); std::string o = j.dump(); g_sink = static_cast<double>(o.size()); }},
}});
{
// serialization of an already parsed document
static json_document vd;
vd.read(s);
static json_editable_document ed = json_editable_document::parse(s);
ed = json_editable_document::parse(s);
static yyjson_doc* yd = nullptr;
if (yd)
{
@@ -713,6 +780,7 @@ int main(int argc, char** argv)
jd = json::parse(s);
workloads.push_back({"dump", {
{"json_view", [&] { std::string o = vd.root().dump(); g_sink = static_cast<double>(o.size()); }},
{"json_view (editable)", [&] { std::string o = ed.root().dump(); g_sink = static_cast<double>(o.size()); }},
{"yyjson", [&] { std::size_t n = 0; char* o = yyjson_write(yd, 0, &n); g_sink = static_cast<double>(n); std::free(o); }},
{"simdjson DOM", [&] { std::string o = simdjson::to_string(se); g_sink = static_cast<double>(o.size()); }},
{"json::parse", [&] { std::string o = jd.dump(); g_sink = static_cast<double>(o.size()); }},
@@ -740,7 +808,7 @@ int main(int argc, char** argv)
}
}
const double ref = best[0];
std::printf("%-13s %-9s", dc.name.c_str(), wl.first.c_str());
std::printf("%-13s %-14s", dc.name.c_str(), wl.first.c_str());
for (std::size_t k = 0; k < wl.second.size(); ++k)
{
const double us = best[k] / 1e3;
+47 -25
View File
@@ -239,6 +239,8 @@ def main():
ap.add_argument('--download', action='store_true', help='use pinned downloads instead of system libraries')
ap.add_argument('--no-boost', action='store_true', help='skip Boost.JSON')
ap.add_argument('--native', action='store_true', help='compile for this CPU (-march=native / -mcpu=native)')
ap.add_argument('--aligned', action='store_true',
help='also build and run with all functions (and loops) aligned, to make code layout artifacts visible')
ap.add_argument('--rounds', type=int, default=30, help='rounds of bench_view (default: 30)')
ap.add_argument('--corpus', nargs='*', default=[], help='more files for bench_corpus')
ap.add_argument('--build-dir', default=os.path.join(HERE, 'build'), help='where to build (default: build/ next to this script)')
@@ -282,32 +284,46 @@ def main():
+ ['-I' + d for d in lib.include])
objects.append(obj)
binaries = {}
for bench in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
exe = os.path.join(args.build_dir, bench)
run(cxx + flags + include + [os.path.join(HERE, bench + '.cpp')] + objects + link + ['-o', exe])
binaries[bench] = exe
cxx_version = output(cxx + ['--version'])
is_clang = 'clang' in cxx_version.lower()
# alignment flags: functions and (gcc) loops on 64-byte boundaries
align_flags = ['-mllvm', '-align-all-functions=6'] if is_clang else ['-falign-functions=64', '-falign-loops=64']
# the builds: the default one and, with --aligned, one with aligned code
variants = [('default build', args.build_dir, [])]
if args.aligned:
variants.append(('aligned build', os.path.join(args.build_dir, 'aligned'), align_flags))
# run: bench_view on its documents, bench_corpus on those and the given files
corpus = [os.path.join(args.data, f) for f in DEFAULT_CORPUS] + args.corpus
outputs = {}
outputs['bench_view'] = run([binaries['bench_view'], args.data, str(args.rounds)], cwd=args.build_dir,
capture_output=True, text=True).stdout
outputs['bench_corpus'] = run([binaries['bench_corpus']] + corpus, cwd=args.build_dir,
capture_output=True, text=True).stdout
outputs['bench_edit'] = run([binaries['bench_edit'], args.data, str(max(1, args.rounds // 2))], cwd=args.build_dir,
capture_output=True, text=True).stdout
outputs['bench_decode'] = run([binaries['bench_decode'], args.data, str(max(1, args.rounds // 2))], cwd=args.build_dir,
capture_output=True, text=True).stdout
for name, text in outputs.items():
print(text)
# label -> {benchmark: output}
all_outputs = {}
for label, build_dir, extra in variants:
os.makedirs(build_dir, exist_ok=True)
binaries = {}
for bench in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
exe = os.path.join(build_dir, bench)
run(cxx + flags + extra + include + [os.path.join(HERE, bench + '.cpp')] + objects + link + ['-o', exe])
binaries[bench] = exe
# run: bench_view on its documents, bench_corpus on those and the given files
outputs = {}
outputs['bench_view'] = run([binaries['bench_view'], args.data, str(args.rounds)], cwd=build_dir,
capture_output=True, text=True).stdout
outputs['bench_corpus'] = run([binaries['bench_corpus']] + corpus, cwd=build_dir,
capture_output=True, text=True).stdout
outputs['bench_edit'] = run([binaries['bench_edit'], args.data, str(max(1, args.rounds // 2))], cwd=build_dir,
capture_output=True, text=True).stdout
outputs['bench_decode'] = run([binaries['bench_decode'], args.data, str(max(1, args.rounds // 2))],
cwd=build_dir, capture_output=True, text=True).stdout
for name, text in outputs.items():
print(text)
all_outputs[label] = outputs
# results with their metadata
now = datetime.datetime.now()
host = re.sub(r'[^A-Za-z0-9-]+', '-', platform.node().split('.')[0]) or 'host'
stem = os.path.join(HERE, 'results', f'{now:%Y-%m-%d}-{host}')
os.makedirs(os.path.dirname(stem), exist_ok=True)
cxx_version = output(cxx + ['--version'])
compiler_version = cxx_version.splitlines()[0] if cxx_version else ' '.join(cxx)
meta = [
('date', f'{now:%Y-%m-%d %H:%M}'),
@@ -316,6 +332,7 @@ def main():
('OS', f'{platform.system()} {platform.release()} ({platform.machine()})'),
('compiler', compiler_version),
('flags', ' '.join(flags)),
('aligned results', ('included (flags: ' + ' '.join(align_flags) + ')') if args.aligned else 'not included'),
('yyjson', libs['yyjson'].version),
('simdjson', libs['simdjson'].version),
('Boost.JSON', libs['boost'].version if with_boost else 'skipped (not found)'),
@@ -328,15 +345,20 @@ def main():
f.write('| | |\n|---|---|\n')
for key, value in meta:
f.write(f'| {key} | {value} |\n')
for name, text in outputs.items():
f.write(f'\n## {name}\n\n```\n{text.rstrip()}\n```\n')
for label, outputs in all_outputs.items():
# (without --aligned there is only the default build, and no extra heading level)
prefix = f'{label}: ' if args.aligned else ''
for name, text in outputs.items():
f.write(f'\n## {prefix}{name}\n\n```\n{text.rstrip()}\n```\n')
with open(stem + '.csv', 'w', encoding='utf-8') as out:
out.write(''.join(f'# {key}: {value}\n' for key, value in meta))
for name in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
path = os.path.join(args.build_dir, name + '.csv')
if os.path.isfile(path):
with open(path, encoding='utf-8') as f:
out.write(f'# {name}\n' + f.read())
for label, build_dir, _ in variants:
for name in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
path = os.path.join(build_dir, name + '.csv')
if os.path.isfile(path):
with open(path, encoding='utf-8') as f:
# (the default build keeps its plain section names)
out.write(f'# {name}' + (f' ({label})' if args.aligned else '') + '\n' + f.read())
print(f'results: {stem}.md, {stem}.csv')
-34
View File
@@ -1,34 +0,0 @@
# check_path_filters
The compiler workflows [`ubuntu.yml`](../../.github/workflows/ubuntu.yml),
[`macos.yml`](../../.github/workflows/macos.yml), and [`windows.yml`](../../.github/workflows/windows.yml) skip pull
requests that change only files no compiler job reads: the documentation, Markdown files, GitHub metadata, other
workflows, and the license files. Pushes to `develop`, `master`, and `release/*` always run them. The documentation,
license, and path-filter checks run in [`repository_checks.yml`](../../.github/workflows/repository_checks.yml), which
has no path filter.
The filter starts with `'**'` and excludes these files explicitly, so a new file runs the compiler workflows unless it
is excluded on purpose. [`check_path_filters.py`](check_path_filters.py) holds the filter (`PATHS`) and checks that
- each of the three workflows has exactly this filter, with its own workflow file included,
- the CI targets that read excluded files (`UNFILTERED_TARGETS`) do not run in these workflows, and
- no file the filter keeps (sources, tests, build files, tools, workflows) mentions an excluded file or directory
outside a comment line, unless the mention is listed in `KNOWN_REFERENCES` with a reason.
When the check reports a mention, decide whether a compiler job reads that file:
- If it does, include the file in `PATHS` after the exclusions (as `package_managers.md` and the Swift example are) and
copy the new filter into the three workflows.
- If it does not, add the mention to `KNOWN_REFERENCES` with the reason.
Run the check with
```shell
python3 tools/check_path_filters/check_path_filters.py
```
To see which workflows a pull request would run, pass the changed files:
```shell
python3 tools/check_path_filters/check_path_filters.py --changed $(git diff --name-only origin/develop)
```
@@ -1,273 +0,0 @@
#!/usr/bin/env python3
"""Check the path filters of the compiler workflows.
ubuntu.yml, macos.yml and windows.yml skip pull requests that change only files
no compiler job reads (documentation, Markdown files, GitHub metadata, license
files). The filter starts with '**' and excludes these files explicitly, so a
new file runs the workflows unless it is excluded on purpose.
The script checks that
- each of the three workflows has exactly the filter defined in PATHS below,
with its own workflow file included, and
- no tracked file that the filter keeps (sources, tests, build files, tools,
workflows) mentions a file the filter excludes, unless the mention is listed
in KNOWN_REFERENCES with the reason why it does not matter.
A mention found this way must either be added to PATHS as an included path
(the file is read by a compiler job) or to KNOWN_REFERENCES (it is not).
With --changed FILE..., the script prints instead which workflows a pull
request changing these files would run.
"""
import argparse
import os
import re
import subprocess # nosec B404
import sys
REPO_ROOT = os.path.normpath(os.path.join(sys.path[0], '..', '..'))
SELF = 'tools/check_path_filters/check_path_filters.py'
WORKFLOWS = ['.github/workflows/ubuntu.yml', '.github/workflows/macos.yml', '.github/workflows/windows.yml']
# The pull_request paths filter of the workflows above; '{workflow}' is replaced
# with the workflow's own file. GitHub applies the patterns in order: a file
# matching a later pattern is included ('pattern') or excluded ('!pattern').
PATHS = [
'**',
'!docs/**',
'!**.md',
'!.github/*',
'!.github/ISSUE_TEMPLATE/**',
'!.github/external_ci/**',
'!.github/workflows/**',
'!.reuse/**',
'!LICENSES/**',
'!CITATION.cff',
'!REUSE.toml',
# read by tools/check_build_options in the ci_meson_install job (ubuntu.yml)
'docs/mkdocs/docs/integration/package_managers.md',
# built by the swiftpm job (macos.yml)
'docs/mkdocs/docs/integration/swift/**',
'{workflow}',
]
# Mentions of excluded files that no job of the three workflows depends on:
# (file that mentions it, the mention, reason)
KNOWN_REFERENCES = [
('cmake/ci.cmake', 'docs/', 'ci_test_examples; runs in repository_checks.yml, which has no path filter'),
('cmake/ci.cmake', 'docs/mkdocs/', 'ci_test_build_documentation and ci_test_documentation_mermaid; they run in '
'repository_checks.yml, which has no path filter'),
('cmake/ci.cmake', 'docs/mkdocs/docs/examples/', 'indentation check of ci_test_amalgamation; the same check runs in '
'check_amalgamation.yml, which has no path filter'),
('Makefile', 'docs/mkdocs/docs/examples/', 'make pretty; runs in check_amalgamation.yml, which has no path filter'),
('Makefile', 'ChangeLog.md', 'make ChangeLog.md writes the file; no CI job runs it'),
('Makefile', 'tests/fuzzing.md', 'help text'),
('Makefile', 'tools/generate_natvis/README.md', 'help text'),
('tests/src/make_test_data_available.hpp', 'README.md', 'a file of the external test data repository'),
('tools/check_build_options/check_build_options.py', 'docs/', 'builds the path of package_managers.md, which '
'PATHS includes'),
('tools/ci_jobs/ci_jobs.py', 'docs/mkdocs/docs/community/quality_assurance.md',
'checks the job table; runs in repository_checks.yml, which has no path filter'),
('tools/ci_jobs/ci_jobs.py', '.github/workflows/repository_checks.yml', 'read by --compare, a manual check'),
('tools/ci_jobs/ci_jobs.py', '.github/workflows/check_amalgamation.yml', 'read by --compare, a manual check'),
]
# CI targets that read excluded files; they must run in a workflow without a path filter
UNFILTERED_TARGETS = ['ci_test_examples', 'ci_test_build_documentation', 'ci_test_documentation_mermaid',
'ci_test_amalgamation', 'ci_reuse_compliance']
def git_files(root):
# fixed command, no user input; includes new files not yet added, so a local run
# sees what CI sees after the commit
out = subprocess.check_output(['git', 'ls-files', '-z', '--cached', '--others', '--exclude-standard'], cwd=root) # nosec B603, B607
return [f for f in out.decode('utf-8').split('\0') if f]
def glob_to_regex(pattern):
"""Translate a GitHub Actions path pattern to a regular expression."""
regex = ''
i = 0
while i < len(pattern):
if pattern.startswith('**', i):
regex += '.*'
i += 2
elif pattern[i] == '*':
regex += '[^/]*'
i += 1
elif pattern[i] == '?':
regex += '[^/]'
i += 1
else:
regex += re.escape(pattern[i])
i += 1
return re.compile(regex + r'\Z')
def compile_filter(paths, workflow):
return [(p.startswith('!'), glob_to_regex(p.lstrip('!').replace('{workflow}', workflow))) for p in paths]
def included(compiled, path):
result = False
for negated, regex in compiled:
if regex.match(path):
result = not negated
return result
def read_filter(root, workflow):
"""Return the pull_request paths list of a workflow file."""
with open(os.path.join(root, workflow), encoding='utf-8') as f:
lines = f.read().splitlines()
paths = None
in_pull_request = False
for line in lines:
if re.match(r'^ pull_request:', line):
in_pull_request = True
continue
if in_pull_request:
if re.match(r'^ \S', line) or re.match(r'^\S', line):
break
if re.match(r'^ paths:\s*$', line):
paths = []
continue
m = re.match(r"^ - '(.*)'\s*(#.*)?$", line)
if paths is not None and m:
paths.append(m.group(1))
elif paths is not None and line.strip() and not line.strip().startswith('#'):
break
return paths
TOKEN = re.compile(r'(["\'/]?)([\w.-]+(?:/[\w.-]+)*)')
COMMENT_LINE = re.compile(r'^[ \t]*(//|#|/?\*|--).*$', re.MULTILINE)
class Mentions:
"""Finds the words and paths in a text that name an excluded file or directory."""
def __init__(self, files, excluded, is_excluded):
self.files = set(files)
self.excluded = excluded
self.dirs = {'/'.join(f.split('/')[:n]) for f in files for n in range(1, f.count('/') + 1)}
# an excluded directory: a new file in it would be excluded
self.excluded_dirs = {d for d in self.dirs if is_excluded(d + '/new_file')}
# base names that only excluded files have, e.g. 'ChangeLog.md' but not 'CMakeLists.txt'
self.names = {os.path.basename(f) for f in excluded} - {os.path.basename(f) for f in files if f not in excluded}
def find(self, text):
"""Yield (offset, mention) for every word or path in text that names an excluded file or directory."""
for m in TOKEN.finditer(text):
parts = m.group(2).rstrip('/').split('/')
# a single word names a directory only in a path context, e.g. 'docs' in os.path.join('docs', ...)
path_context = len(parts) > 1 or m.group(1)
for i in range(len(parts)):
path = '/'.join(parts[i:])
if path in self.files:
if path in self.excluded:
yield m.start(2), path
break
if path in self.dirs and path_context:
if path in self.excluded_dirs:
yield m.start(2), path + '/'
break
else:
if parts[-1] in self.names and '.' in parts[-1]:
yield m.start(2), parts[-1]
def known(path, mention):
return any(path == file and mention == known_mention for file, known_mention, _ in KNOWN_REFERENCES)
def check_workflows(root):
"""Return the errors in the filters of the workflows and in the targets they run."""
errors = []
for workflow in WORKFLOWS:
expected = [p.replace('{workflow}', workflow) for p in PATHS]
actual = read_filter(root, workflow)
if actual != expected:
errors.append('{}: the pull_request paths filter differs from tools/check_path_filters/check_path_filters.py\n'
' expected: {}\n actual: {}'.format(workflow, expected, actual))
with open(os.path.join(root, workflow), encoding='utf-8') as f:
text = f.read()
errors += ['{}: runs {}, which reads excluded files; run it in repository_checks.yml'.format(workflow, target)
for target in UNFILTERED_TARGETS if re.search(r'\b' + target + r'\b', text)]
return errors
def scanned_text(root, path):
"""Return the text of a kept file without comment lines and without the filter itself, or None to skip it."""
# third-party code and Markdown files read nothing; this script lists the excluded files
if path.startswith('tests/thirdparty/') or path.endswith('.md') or path == SELF:
return None
try:
with open(os.path.join(root, path), encoding='utf-8') as f:
# a mention in a comment line reads nothing; keep the line numbers
text = COMMENT_LINE.sub('', f.read())
except (UnicodeDecodeError, OSError):
return None
if path in WORKFLOWS:
# the filter itself lists the excluded files
for p in PATHS:
text = text.replace(" - '{}'".format(p.replace('{workflow}', path)), '')
return text
def check_mentions(root, files, excluded, finder):
"""Return an error for every mention of an excluded file in a kept file that KNOWN_REFERENCES does not list."""
errors = []
for path in sorted(set(files) - excluded):
text = scanned_text(root, path)
reported = set()
for offset, mention in finder.find(text or ''):
if mention not in reported and not known(path, mention):
reported.add(mention)
line = text.count('\n', 0, offset) + 1
errors.append('{}:{}: mentions excluded {}; include it in PATHS if a compiler job reads it, '
'or add it to KNOWN_REFERENCES'.format(path, line, mention))
return errors
def check(root):
files = git_files(root)
union = [compile_filter(PATHS, w) for w in WORKFLOWS]
def is_excluded(path):
return not any(included(c, path) for c in union)
excluded = {f for f in files if is_excluded(f)}
errors = check_workflows(root) + check_mentions(root, files, excluded, Mentions(files, excluded, is_excluded))
for e in errors:
print(e)
if not errors:
print('path filters OK: {} of {} tracked files skip the compiler workflows'.format(len(excluded), len(files)))
return 1 if errors else 0
def explain(changed):
for workflow in WORKFLOWS:
compiled = compile_filter(PATHS, workflow)
hits = [f for f in changed if included(compiled, f)]
print('{}: {}'.format(workflow, 'runs ({})'.format(', '.join(hits[:3]) + (' ...' if len(hits) > 3 else ''))
if hits else 'skipped'))
return 0
def main():
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument('--root', default=REPO_ROOT, help='repository root')
parser.add_argument('--changed', nargs='+', metavar='FILE', help='show which workflows these changed files run')
args = parser.parse_args()
return explain(args.changed) if args.changed else check(args.root)
if __name__ == '__main__':
sys.exit(main())
-27
View File
@@ -1,27 +0,0 @@
# ci_jobs
The compiler jobs of [`ubuntu.yml`](../../.github/workflows/ubuntu.yml) are split into three sets, chosen by each job's
`if:` condition (see [How CI runs](https://json.nlohmann.me/community/quality_assurance/#how-ci-runs)):
- **PR set**: every push to a pull request, and every push to `develop`, `master`, and `release/*`;
- **full set**: after the PR set passed, on pull requests with the label "🚀 ready to merge" and on every push;
- **nightly set**: every night on `develop`, on every push to `master` and `release/*`, and when started by hand.
A matrix whose entries belong to different sets is split into one job per set: the full-set job gets the suffix
`_full`, the nightly-set job `_nightly`, and both share their steps with the PR-set job through YAML anchors.
[`ci_jobs.py`](ci_jobs.py) checks that
- every job of `ubuntu.yml` has the `if:` condition of exactly one set (the conditions are in `SETS`),
- every full-set job waits for (`needs:`) exactly the PR set, and `nightly_report` for exactly the nightly set, and
- the job table in [`quality_assurance.md`](../../docs/mkdocs/docs/community/quality_assurance.md) is up to date.
```shell
python3 -m venv venv && venv/bin/pip install -r tools/ci_jobs/requirements.txt
venv/bin/python tools/ci_jobs/ci_jobs.py # check
venv/bin/python tools/ci_jobs/ci_jobs.py --markdown # print the job table for the documentation
venv/bin/python tools/ci_jobs/ci_jobs.py --compare origin/develop # list configurations added or removed since develop
```
Use `--compare` when changing the workflows: a configuration that disappears fails it, so no compiler, version, or
standard is dropped by accident when jobs are split, merged, or moved between sets.
-230
View File
@@ -1,230 +0,0 @@
#!/usr/bin/env python3
"""List and check the jobs of the compiler workflows and the set each job belongs to.
The jobs of a tiered workflow (ubuntu.yml) belong to one of three sets, chosen
by the job's `if:` condition:
- PR set: every pull request push and every push,
- full set: after the PR set passed, on pushes and on pull requests with the
"🚀 ready to merge" label,
- nightly set: nightly on develop, on pushes to master and release/*, and when
started by hand.
The jobs of the other workflows run on every push and pull request.
The script checks that every job of a tiered workflow belongs to exactly one
set, that the full set waits for the whole PR set, that the nightly report
waits for the whole nightly set, and that the job table in
docs/mkdocs/docs/community/quality_assurance.md is up to date.
--markdown print the job table for the documentation
--compare REF list the configurations (job and matrix values) added or
removed since the git revision REF; fails if one was removed
"""
import argparse
import collections
import itertools
import os
import re
import subprocess # nosec B404
import sys
import yaml
REPO_ROOT = os.path.normpath(os.path.join(sys.path[0], '..', '..'))
DOCS = 'docs/mkdocs/docs/community/quality_assurance.md'
BEGIN, END = '<!-- ci_jobs: begin -->', '<!-- ci_jobs: end -->'
WORKFLOWS = ['.github/workflows/ubuntu.yml', '.github/workflows/macos.yml', '.github/workflows/windows.yml']
TIERED = ['.github/workflows/ubuntu.yml']
# --compare also counts the jobs of these workflows, so a job moved between workflows is not a removed one
COMPARED = WORKFLOWS + ['.github/workflows/repository_checks.yml', '.github/workflows/check_amalgamation.yml']
LABEL = '🚀 ready to merge'
SETS = collections.OrderedDict([
('PR set', "${{ github.event_name != 'schedule' && (github.event.action != 'labeled' || "
"github.event.label.name == '" + LABEL + "') }}"),
('full set', "${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' || "
"contains(github.event.pull_request.labels.*.name, '" + LABEL + "') }}"),
('nightly set', "${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || "
"(github.event_name == 'push' && github.ref != 'refs/heads/develop') }}"),
])
UNTIERED = 'every push and PR'
REPORT_JOB = 'nightly_report'
# a job split by set keeps its name and gets one of these suffixes
SUFFIXES = {'full set': '_full', 'nightly set': '_nightly'}
def read(root, path, ref=None):
if ref is None:
with open(os.path.join(root, path), encoding='utf-8') as f:
return f.read()
# fixed command; ref is a git revision given on the command line
return subprocess.check_output(['git', 'show', '{}:{}'.format(ref, path)], cwd=root, # nosec B603, B607
stderr=subprocess.DEVNULL).decode('utf-8')
def add_include(combos, keys, inc):
"""Apply one include entry: extend the matching configurations, or add it as a new one."""
matching = [c for c in combos if all(c[k] == v for k, v in inc.items() if k in keys)]
for c in matching:
c.update({k: v for k, v in inc.items() if k not in keys})
if not matching:
combos.append(dict(inc))
def expand(matrix):
"""Return the configurations of a job's strategy.matrix like GitHub Actions does."""
if not matrix:
return [{}]
keys = [k for k in matrix if k not in ('include', 'exclude')]
combos = [dict(zip(keys, values)) for values in itertools.product(*(matrix[k] for k in keys))] if keys else []
for ex in matrix.get('exclude') or []:
combos = [c for c in combos if any(c.get(k) != v for k, v in ex.items())]
for inc in matrix.get('include') or []:
add_include(combos, keys, inc)
return combos
def jobs_of(root, workflow, ref=None):
"""Yield (job id, set, list of configurations) for every job of a workflow."""
data = yaml.safe_load(read(root, workflow, ref))
for job_id, job in data['jobs'].items():
condition = job.get('if')
job_set = UNTIERED if condition is None else next((s for s, c in SETS.items() if c == condition), None)
matrix = (job.get('strategy') or {}).get('matrix')
yield job_id, job_set, job, expand(matrix)
def base_id(job_id, job_set):
suffix = SUFFIXES.get(job_set)
return job_id[:-len(suffix)] if suffix and job_id.endswith(suffix) else job_id
def label(config):
return ', '.join(str(v) for k, v in config.items() if k != 'os' or len(config) == 1)
def configurations(root, ref=None):
"""Return a Counter of (job name without set suffix, configuration) over the compared workflows."""
result = collections.Counter()
for workflow in COMPARED:
try:
jobs = list(jobs_of(root, workflow, ref))
except (OSError, subprocess.CalledProcessError):
continue # the workflow does not exist in this revision
for job_id, job_set, _, configs in jobs:
if job_id == REPORT_JOB:
continue
for c in configs:
result[(base_id(job_id, job_set), tuple(sorted((k, str(v)) for k, v in c.items())))] += 1
return result
def markdown(root):
rows = []
order = list(SETS) + [UNTIERED]
for workflow in WORKFLOWS:
for job_id, job_set, _, configs in jobs_of(root, workflow):
if job_id == REPORT_JOB:
continue
labels = [label(c) for c in configs if c]
rows.append((order.index(job_set) if job_set in order else len(order), os.path.basename(workflow), job_id,
job_set or 'no set', '; '.join(labels) or '-'))
rows.sort(key=lambda r: (r[0], r[1]))
lines = ['| Set | Workflow | Job | Configurations |', '|-----|----------|-----|----------------|']
lines += ['| {} | {} | `{}` | {} |'.format(s, w, j, c) for _, w, j, s, c in rows]
return lines
def check_sets(workflow, jobs):
"""Return the errors in the sets of a tiered workflow's jobs and the jobs of each set."""
errors = []
by_set = collections.defaultdict(set)
for job_id, job_set, _, _ in jobs:
if job_id == REPORT_JOB:
continue
if job_set in SETS:
by_set[job_set].add(job_id)
else:
errors.append('{}: job {} has an if: condition of no set; use one of the conditions in '
'tools/ci_jobs/ci_jobs.py'.format(workflow, job_id))
return errors, by_set
def check_needs(workflow, jobs, by_set):
"""Return the errors in the needs: of a tiered workflow's jobs."""
required = {'full set': by_set['PR set'], 'PR set': set(), 'nightly set': set()}
errors = []
for job_id, job_set, job, _ in jobs:
needs = job.get('needs') or []
needs = {needs} if isinstance(needs, str) else set(needs)
expected = by_set['nightly set'] if job_id == REPORT_JOB else required.get(job_set, needs)
if needs != expected:
errors.append('{}: job {} must need exactly: {}'.format(workflow, job_id, ', '.join(sorted(expected)) or 'no job'))
return errors
def check_docs(root):
"""Return an error if the job table in the documentation is not up to date."""
text = read(root, DOCS)
block = re.search(re.escape(BEGIN) + r'\n(.*?)\n[ \t]*' + re.escape(END), text, re.S)
if not block:
return ['{}: markers {} and {} not found'.format(DOCS, BEGIN, END)]
indent = re.match(r'[ \t]*', block.group(1)).group(0)
if block.group(1) != '\n'.join(indent + line for line in markdown(root)):
return ['{}: the job table is out of date; replace it with the output of '
'python3 tools/ci_jobs/ci_jobs.py --markdown'.format(DOCS)]
return []
def check(root):
errors = []
for workflow in TIERED:
jobs = list(jobs_of(root, workflow))
set_errors, by_set = check_sets(workflow, jobs)
errors += set_errors + check_needs(workflow, jobs, by_set)
errors += check_docs(root)
for e in errors:
print(e)
if not errors:
counts = collections.Counter()
for workflow in WORKFLOWS:
for job_id, job_set, _, configs in jobs_of(root, workflow):
if job_id != REPORT_JOB:
counts[job_set] += len(configs)
print('CI jobs OK: ' + ', '.join('{} {}'.format(n, s) for s, n in counts.items()))
return 1 if errors else 0
def compare(root, ref):
before, after = configurations(root, ref), configurations(root)
removed, added = before - after, after - before
for title, diff in (('removed', removed), ('added', added)):
for job, config in sorted(diff.elements()):
print('{}: {} ({})'.format(title, job, ', '.join('{}={}'.format(k, v) for k, v in config)))
print('{} configurations before, {} after; {} removed, {} added'.format(
sum(before.values()), sum(after.values()), sum(removed.values()), sum(added.values())))
return 1 if removed else 0
def main():
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument('--root', default=REPO_ROOT, help='repository root')
group = parser.add_mutually_exclusive_group()
group.add_argument('--markdown', action='store_true', help='print the job table for the documentation')
group.add_argument('--compare', metavar='REF', help='list the configurations added or removed since REF')
args = parser.parse_args()
if args.markdown:
print('\n'.join(markdown(args.root)))
return 0
if args.compare:
return compare(args.root, args.compare)
return check(args.root)
if __name__ == '__main__':
sys.exit(main())
-1
View File
@@ -1 +0,0 @@
PyYAML==6.0.3