Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d587214ab2 | ||
|
|
bd442d5d07 | ||
|
|
087df7b0c2 | ||
|
|
eae3c8217b | ||
|
|
960c234cc6 | ||
|
|
b6e24d19a9 |
No files matched your search
@@ -154,6 +154,15 @@ 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
|
||||
|
||||
@@ -7,6 +7,25 @@ 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:
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
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 }}
|
||||
+422
-262
@@ -7,17 +7,62 @@ 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.ref || github.run_id }}
|
||||
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) || '' }}
|
||||
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:
|
||||
@@ -32,6 +77,7 @@ 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
|
||||
@@ -76,7 +122,342 @@ 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
|
||||
@@ -105,76 +486,16 @@ jobs:
|
||||
path: ${{ github.workspace }}/build/build_infer/infer-out/report.txt
|
||||
if-no-files-found: ignore
|
||||
|
||||
ci_static_analysis_ubuntu:
|
||||
ci_static_analysis_ubuntu_nightly:
|
||||
if: *nightly_set
|
||||
runs-on: ubuntu-26.04
|
||||
strategy:
|
||||
matrix:
|
||||
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 }}
|
||||
target: [ci_test_valgrind]
|
||||
steps: *static_analysis_ubuntu_steps
|
||||
|
||||
ci_test_coverage:
|
||||
if: *nightly_set
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
@@ -208,128 +529,8 @@ 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
|
||||
@@ -352,37 +553,8 @@ 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.
|
||||
@@ -402,6 +574,7 @@ jobs:
|
||||
run: cmake --build build --target ci_icpc
|
||||
|
||||
ci_icpx:
|
||||
if: *nightly_set
|
||||
runs-on: ubuntu-latest
|
||||
container: intel/oneapi-hpckit:latest
|
||||
steps:
|
||||
@@ -416,6 +589,7 @@ 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:
|
||||
@@ -429,54 +603,10 @@ 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:
|
||||
@@ -491,3 +621,33 @@ 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
|
||||
@@ -7,6 +7,25 @@ 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,6 +13,72 @@ 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"
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
<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">
|
||||
<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">
|
||||
<title id="nj-qa-title-w">Quality assurance pipeline</title>
|
||||
<desc id="nj-qa-desc-w">Every push or pull request starts these checks in parallel: Build matrix (every push / PR): 50+ compilers: G​CC, Clang, M​SVC, AppleClang, Intel, NVIDIA, ... C++11, 14, 17, 20, 23, and 26 libc++ and libstdc++ warnings: Clang -Weverything, G​CC 300+ warnings; Unit tests (doctest) (every push / PR): every public A​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​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​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​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">A push or pull request starts these checks; the badges say when each runs: Build matrix (every push / PR, before merge): 50+ compilers: G​CC, Clang, M​SVC, AppleClang, Intel, NVIDIA, ... C++11, 14, 17, 20, 23, and 26 libc++ and libstdc++ warnings: Clang -Weverything, G​CC 300+ warnings a subset on every push and PR, all before merge; Intel, NVIDIA, C​UDA, and ARM64 nightly; Unit tests (doctest) (every push / PR): every public A​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​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​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​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="594" rx="12"/>
|
||||
<rect class="nj-exceptions-groupbox" x="12" y="70" width="736" height="648" 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="184" rx="8"/>
|
||||
<rect class="nj-exceptions-box" x="26" y="110" width="228" height="238" 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​CC, Clang, M​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​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​UDA, and ARM64 nightly</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<rect class="nj-exceptions-box" x="266" y="110" width="228" height="184" rx="8"/>
|
||||
<rect class="nj-exceptions-box" x="266" y="110" width="228" height="238" 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>
|
||||
@@ -32,209 +37,218 @@
|
||||
<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="184" rx="8"/>
|
||||
<rect class="nj-exceptions-box" x="506" y="110" width="228" height="238" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="520" y="135">Coverage</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>
|
||||
<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>
|
||||
<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="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​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>
|
||||
<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​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>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<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​I</text>
|
||||
<text class="nj-exceptions-line" x="40" y="598">cpplint with 61 rules</text>
|
||||
<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​I</text>
|
||||
<text class="nj-exceptions-line" x="40" y="652">cpplint with 61 rules</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<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​EUSE</text>
|
||||
<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​EUSE</text>
|
||||
</g>
|
||||
<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>
|
||||
<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>
|
||||
</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">
|
||||
<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">
|
||||
<title id="nj-qa-title-n">Quality assurance pipeline</title>
|
||||
<desc id="nj-qa-desc-n">Every push or pull request starts these checks in parallel: Build matrix (every push / PR): 50+ compilers: G​CC, Clang, M​SVC, AppleClang, Intel, NVIDIA, ... C++11, 14, 17, 20, 23, and 26 libc++ and libstdc++ warnings: Clang -Weverything, G​CC 300+ warnings; Unit tests (doctest) (every push / PR): every public A​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​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​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​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">A push or pull request starts these checks; the badges say when each runs: Build matrix (every push / PR, before merge): 50+ compilers: G​CC, Clang, M​SVC, AppleClang, Intel, NVIDIA, ... C++11, 14, 17, 20, 23, and 26 libc++ and libstdc++ warnings: Clang -Weverything, G​CC 300+ warnings a subset on every push and PR, all before merge; Intel, NVIDIA, C​UDA, and ARM64 nightly; Unit tests (doctest) (every push / PR): every public A​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​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​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​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="1572" rx="12"/>
|
||||
<rect class="nj-exceptions-groupbox" x="12" y="70" width="336" height="1626" 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="184" rx="8"/>
|
||||
<rect class="nj-exceptions-box" x="26" y="110" width="308" height="238" 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​CC, Clang, M​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​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​UDA, and ARM64 nightly</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<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​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>
|
||||
<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​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>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<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​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>
|
||||
<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​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>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<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​I</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1256">cpplint with 61 rules</text>
|
||||
<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​I</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1310">cpplint with 61 rules</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<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​EUSE</text>
|
||||
<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​EUSE</text>
|
||||
</g>
|
||||
<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>
|
||||
<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>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
@@ -22,19 +22,24 @@ 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"], [
|
||||
("Build matrix", ["push", "merge"], [
|
||||
"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",
|
||||
@@ -44,7 +49,7 @@ LANES = [
|
||||
"custom number, string, array,",
|
||||
"and object types",
|
||||
]),
|
||||
("Coverage", ["push"], [
|
||||
("Coverage", ["nightly"], [
|
||||
"unit tests cover all lines",
|
||||
"of the code base",
|
||||
]),
|
||||
@@ -53,11 +58,11 @@ LANES = [
|
||||
"Clang Static Analyzer (52",
|
||||
"checkers), IWYU, Codacy",
|
||||
"CodeQL, Semgrep, Flawfinder",
|
||||
"(also scheduled)",
|
||||
"(also scheduled); Infer nightly",
|
||||
]),
|
||||
("Dynamic analysis", ["push"], [
|
||||
("Dynamic analysis", ["push", "nightly"], [
|
||||
"runtime assertions",
|
||||
"Valgrind (Memcheck)",
|
||||
"Valgrind (Memcheck), nightly",
|
||||
"address and undefined behavior",
|
||||
"sanitizers, integer overflow,",
|
||||
"nullability violations",
|
||||
@@ -178,7 +183,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 = ("Every push or pull request starts these checks in parallel: " +
|
||||
desc = ("A push or pull request starts these checks; the badges say when each runs: " +
|
||||
"; ".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">'
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
#include <nlohmann/detail/view/macro_scope.hpp>
|
||||
#include <nlohmann/detail/view/node.hpp>
|
||||
#include <nlohmann/detail/view/scan.hpp>
|
||||
#include <nlohmann/detail/view/simd.hpp>
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
namespace detail
|
||||
@@ -104,56 +103,12 @@ NLOHMANN_VIEW_NOINLINE FloatType float_value(const char* first, const node& n)
|
||||
return convert_float<FloatType>(first, last, dot, mantissa_end);
|
||||
}
|
||||
|
||||
#if NLOHMANN_VIEW_VECTOR
|
||||
/*!
|
||||
@brief the value of the first 16 digits of a run of digits with a point after
|
||||
int_digits of them (or none, if int_digits >= 16)
|
||||
|
||||
The 17 bytes at p are read at once: the bytes after the point move down by one,
|
||||
then pairs, quadruples, and octets of digits are combined by multiplications
|
||||
(after simdjson). The bytes [p, p + 32) must be readable. Bytes that are not
|
||||
digits (only in a damaged image loaded with image_check::bounds) give a wrong
|
||||
value, but no undefined behavior.
|
||||
*/
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t sixteen_digits(const unsigned char* p, unsigned int_digits) noexcept
|
||||
{
|
||||
#if NLOHMANN_VIEW_NEON
|
||||
const uint8x16_t low = vld1q_u8(p);
|
||||
const uint8x16_t index = vcombine_u8(vcreate_u8(0x0706050403020100u), vcreate_u8(0x0F0E0D0C0B0A0908u));
|
||||
const uint8x16_t before_point = vcltq_u8(index, vdupq_n_u8(static_cast<std::uint8_t>(int_digits)));
|
||||
const uint8x16_t digits = vsubq_u8(vbslq_u8(before_point, low, vextq_u8(low, vld1q_u8(p + 16), 1)), vdupq_n_u8('0'));
|
||||
const uint16x8_t twos = vpaddlq_u8(vmulq_u8(digits, vreinterpretq_u8_u16(vdupq_n_u16(0x010A))));
|
||||
const uint32x4_t fours = vpaddlq_u16(vmulq_u16(twos, vreinterpretq_u16_u32(vdupq_n_u32(0x00010064))));
|
||||
const uint64x2_t eights = vpaddlq_u32(vmulq_u32(fours, vreinterpretq_u32_u64(vdupq_n_u64(0x0000000100002710u))));
|
||||
return (vgetq_lane_u64(eights, 0) * 100000000u) + vgetq_lane_u64(eights, 1);
|
||||
#else
|
||||
// NOLINTBEGIN(portability-simd-intrinsics)
|
||||
const __m128i low = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i high = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p + 16))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i before_point = _mm_cmpgt_epi8(_mm_set1_epi8(static_cast<char>(int_digits)), _mm_setr_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15));
|
||||
const __m128i moved = _mm_or_si128(_mm_srli_si128(low, 1), _mm_slli_si128(high, 15));
|
||||
const __m128i digits = _mm_sub_epi8(_mm_or_si128(_mm_and_si128(before_point, low), _mm_andnot_si128(before_point, moved)), _mm_set1_epi8('0'));
|
||||
// (16-bit lanes: SSE2 has no multiply-add of bytes)
|
||||
const __m128i tens = _mm_setr_epi16(10, 1, 10, 1, 10, 1, 10, 1);
|
||||
const __m128i twos = _mm_packs_epi32(_mm_madd_epi16(_mm_unpacklo_epi8(digits, _mm_setzero_si128()), tens),
|
||||
_mm_madd_epi16(_mm_unpackhi_epi8(digits, _mm_setzero_si128()), tens));
|
||||
const __m128i fours = _mm_madd_epi16(twos, _mm_setr_epi16(100, 1, 100, 1, 100, 1, 100, 1));
|
||||
const __m128i eights = _mm_madd_epi16(_mm_packs_epi32(fours, fours), _mm_setr_epi16(10000, 1, 10000, 1, 10000, 1, 10000, 1));
|
||||
const auto upper = static_cast<std::uint32_t>(_mm_cvtsi128_si32(eights));
|
||||
const auto lower = static_cast<std::uint32_t>(_mm_cvtsi128_si32(_mm_srli_si128(eights, 4)));
|
||||
// NOLINTEND(portability-simd-intrinsics)
|
||||
return (std::uint64_t{upper} * 100000000u) + lower;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
/*!
|
||||
@brief the digits of a float token with at most 19 digits, from its layout
|
||||
|
||||
The digit layout recorded while parsing says where the integer digits, the
|
||||
fraction digits, and the exponent are, so the digits are read eight at a
|
||||
time without scanning (with SIMD, a token of 16 to 19 digits sixteen at a
|
||||
time).
|
||||
time without scanning.
|
||||
|
||||
@param[in] p first character of the token
|
||||
@param[in] e end of the token
|
||||
@@ -163,34 +118,15 @@ NLOHMANN_VIEW_ALWAYS_INLINE float_significand layout_decimal(const unsigned char
|
||||
{
|
||||
const bool negative = *p == '-';
|
||||
p += negative ? 1 : 0;
|
||||
std::uint64_t w = 0;
|
||||
std::uint64_t w = parse_upto19(p, int_digits, limit);
|
||||
p += int_digits;
|
||||
std::int64_t q = 0;
|
||||
#if NLOHMANN_VIEW_VECTOR
|
||||
const unsigned count = int_digits + frac_digits;
|
||||
if (count >= 16 && limit - p >= 32)
|
||||
if (frac_digits != 0)
|
||||
{
|
||||
// (the point, if any, follows the integer digits; the digits after
|
||||
// the first 16 are at most three)
|
||||
w = sixteen_digits(p, int_digits);
|
||||
for (unsigned i = 16; i < count; ++i)
|
||||
{
|
||||
w = (w * 10) + static_cast<std::uint64_t>(p[i + (i >= int_digits ? 1 : 0)] - '0');
|
||||
}
|
||||
p += count + (frac_digits != 0 ? 1 : 0);
|
||||
w = (w * int_pow10(frac_digits)) + parse_upto19(p + 1, frac_digits, limit);
|
||||
p += 1 + frac_digits;
|
||||
q = -static_cast<std::int64_t>(frac_digits);
|
||||
}
|
||||
else /* NOLINT(readability/braces) */
|
||||
#endif
|
||||
{
|
||||
w = parse_upto19(p, int_digits, limit);
|
||||
p += int_digits;
|
||||
if (frac_digits != 0)
|
||||
{
|
||||
w = (w * int_pow10(frac_digits)) + parse_upto19(p + 1, frac_digits, limit);
|
||||
p += 1 + frac_digits;
|
||||
q = -static_cast<std::int64_t>(frac_digits);
|
||||
}
|
||||
}
|
||||
if (p != e)
|
||||
{
|
||||
// [eE][+-]digits; huge exponents saturate (the parser rejected
|
||||
@@ -225,22 +161,12 @@ NLOHMANN_VIEW_ALWAYS_INLINE float_significand layout_decimal(const unsigned char
|
||||
The result is correctly rounded by the lexer's conversion
|
||||
(detail::decimal_to_float(): Clinger's fast path where both operands are
|
||||
exact, else the Eisel-Lemire algorithm, which needs no fallback for up to 19
|
||||
digits), so it is the value parse() produces. Tokens of 16 or more digits go
|
||||
to Eisel-Lemire directly: the fast path takes few of them (none of 17 digits),
|
||||
and its check is a branch that data like GeoJSON coordinates mispredicts.
|
||||
digits), so it is the value parse() produces.
|
||||
*/
|
||||
template<typename FloatType>
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE FloatType layout_float(const unsigned char* p, const unsigned char* e, unsigned int_digits, unsigned frac_digits, const unsigned char* limit) noexcept
|
||||
{
|
||||
const float_significand d = layout_decimal(p, e, int_digits, frac_digits, limit);
|
||||
if (int_digits + frac_digits >= 16)
|
||||
{
|
||||
using format = ieee_binary_format<std::numeric_limits<FloatType>::digits>;
|
||||
native_float_t<FloatType> value{};
|
||||
float_from_bits(eisel_lemire<format>(d.exponent, d.w) | (d.negative ? (std::uint64_t{1} << format::sign_bit()) : 0u), value);
|
||||
return value;
|
||||
}
|
||||
return decimal_to_float<FloatType>(d);
|
||||
return decimal_to_float<FloatType>(layout_decimal(p, e, int_digits, frac_digits, limit));
|
||||
}
|
||||
|
||||
/// the value of a float set by an edit: its token (the shortest round-trip
|
||||
@@ -259,80 +185,6 @@ NLOHMANN_VIEW_NOINLINE FloatType edited_float(const char* token, const node& n)
|
||||
return float_value<FloatType>(token, n);
|
||||
}
|
||||
|
||||
#if NLOHMANN_VIEW_VECTOR
|
||||
/// eisel_lemire() for the token w * 10^q of coordinate_float()
|
||||
template<typename Format>
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t coordinate_bits(std::int64_t q, std::uint64_t w, Format /*format*/) noexcept
|
||||
{
|
||||
return eisel_lemire<Format>(q, w);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief (binary64) eisel_lemire() without the cases that tokens of 16 or 17
|
||||
digits with 5 or more after the point cannot reach: a value of 0, a
|
||||
subnormal, an infinity, or a tie (q < -4)
|
||||
*/
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t coordinate_bits(std::int64_t q, std::uint64_t w, ieee_binary_format<53> format) noexcept
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(w == 0 || q > -5))
|
||||
{
|
||||
return eisel_lemire<ieee_binary_format<53>>(q, w);
|
||||
}
|
||||
static_cast<void>(format);
|
||||
const int lz = count_leading_zeros(w);
|
||||
const std::uint64_t normalized = w << static_cast<unsigned>(lz);
|
||||
const auto index = static_cast<std::size_t>(2 * (q - pow5_128_smallest_power));
|
||||
const uint128_parts product = full_multiplication(normalized, pow5_128()[index]);
|
||||
std::uint64_t high = product.high;
|
||||
if (NLOHMANN_VIEW_UNLIKELY((high & 0x1FFu) == 0x1FFu))
|
||||
{
|
||||
// (the lower bits may carry into the result)
|
||||
const std::uint64_t second = full_multiplication(normalized, pow5_128()[index + 1]).high;
|
||||
high += product.low + second < product.low ? 1u : 0u;
|
||||
}
|
||||
const auto upperbit = static_cast<unsigned>(high >> 63u);
|
||||
std::uint64_t mantissa = high >> (upperbit + 9);
|
||||
auto power2 = static_cast<std::uint64_t>(((217706 * q) >> 16) + 63 + static_cast<std::int64_t>(upperbit) - lz + 1023);
|
||||
mantissa = (mantissa + (mantissa & 1u)) >> 1u;
|
||||
const bool carry = mantissa >= (std::uint64_t{2} << 52u);
|
||||
mantissa = carry ? (std::uint64_t{1} << 52u) : mantissa;
|
||||
power2 += carry ? 1u : 0u;
|
||||
return (mantissa & ~(std::uint64_t{1} << 52u)) | (power2 << 52u);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief (binary32 and binary64) the value of a float token of the source with
|
||||
16 or 17 digits, a point, and no exponent, the shape of most coordinates
|
||||
written with full precision; whether the token of n has this shape
|
||||
|
||||
These tokens skip the general layout of layout_decimal(): no digits beyond
|
||||
the 17th, no exponent (and Eisel-Lemire directly, as in layout_float()).
|
||||
*/
|
||||
template<typename FloatType>
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE bool coordinate_float(const document_data& d, const node& n, FloatType& value) noexcept
|
||||
{
|
||||
const unsigned int_digits = n.extra & 0xFFu;
|
||||
const unsigned frac_digits = n.extra >> 8u;
|
||||
const unsigned count = int_digits + frac_digits;
|
||||
const auto* const first = reinterpret_cast<const unsigned char*>(d.src + n.off); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
const unsigned negative = first[0] == '-' ? 1u : 0u;
|
||||
if (count - 16u > 1u || frac_digits == 0 || n.len != negative + count + 1 || d.size - n.off < 33)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const unsigned char* const digits = first + negative;
|
||||
const std::uint64_t w = sixteen_digits(digits, int_digits);
|
||||
// (the 17th digit follows the point)
|
||||
const std::uint64_t w17 = (w * 10) + static_cast<std::uint64_t>(digits[17] - '0');
|
||||
using format = ieee_binary_format<std::numeric_limits<FloatType>::digits>;
|
||||
native_float_t<FloatType> result{};
|
||||
float_from_bits(coordinate_bits(-static_cast<std::int64_t>(frac_digits), count == 17 ? w17 : w, format{}) | (static_cast<std::uint64_t>(negative) << format::sign_bit()), result);
|
||||
value = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/// the value of the float token of a node, as parse() converts it; floats and
|
||||
/// doubles with at most 19 digits are converted from the digit layout
|
||||
template<typename FloatType>
|
||||
@@ -348,13 +200,6 @@ FloatType float_value(const document_data& d, const node& n)
|
||||
template<typename FloatType>
|
||||
FloatType float_value(const document_data& d, const node& n, std::true_type /*binary32 or binary64*/)
|
||||
{
|
||||
#if NLOHMANN_VIEW_VECTOR
|
||||
FloatType value {};
|
||||
if (coordinate_float(d, n, value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
#endif
|
||||
const unsigned int_digits = n.extra & 0xFFu;
|
||||
const unsigned frac_digits = n.extra >> 8u;
|
||||
if (NLOHMANN_VIEW_LIKELY(int_digits + frac_digits <= 19)) // (255 marks "many")
|
||||
|
||||
@@ -14,9 +14,7 @@
|
||||
#include <cstddef> // size_t
|
||||
#include <cstdint> // uint8_t, uint32_t
|
||||
#include <cstring> // memcpy, memset
|
||||
#include <functional> // less
|
||||
#include <limits> // numeric_limits
|
||||
#include <memory> // unique_ptr
|
||||
#include <type_traits> // integral_constant
|
||||
#include <vector> // vector
|
||||
|
||||
@@ -386,143 +384,6 @@ struct depth_counter<false>
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
@brief where the double x nearest to a token w * 10^q lies, for 10^15 <= w <
|
||||
10^17 and -17 <= q <= -1 (Eisel-Lemire, then a fixed-point position)
|
||||
|
||||
dump() writes the shortest decimal in the rounding interval of x (the closest
|
||||
one if there are several, see zmij::to_shortest()). Its digits are those of a
|
||||
multiple of 10^g of the token's last digit, with g = dec_exp + 1 - q for x's
|
||||
decimal exponent dec_exp: the multiple of 10^g nearest to x if it lies in the
|
||||
interval (at most one does), else the multiple of 10^(g - 1) nearest to x
|
||||
(see token_digits_1() and token_digits_2()). For g <= 0, the token itself is
|
||||
in the interval.
|
||||
|
||||
The position is exact in a 64-bit fixed-point number with 56 bits after the
|
||||
point (of the token's last digit): x / 10^q = 2c * 5^-q / 2^t for x = c * 2^e2
|
||||
and t = q + 1 - e2, with at most 56 bits after the point here, and |x / 10^q -
|
||||
w| is at most half the spacing of the doubles, below 12 units. So only the low
|
||||
64 bits of 2c * 5^-q are needed. c and e2 come from Eisel-Lemire's product
|
||||
directly (as eisel_lemire() rounds it: no tie is possible for q < -4, and its
|
||||
second product is added where it can carry into the result).
|
||||
|
||||
@param[out] offset x / 10^q - w + 32, with 56 bits after the point
|
||||
@param[out] radius half the spacing of the doubles at x, likewise (+ 1 if
|
||||
the interval is closed)
|
||||
@return g; 3 if the digits are not decided here (a power of two, whose
|
||||
interval is not symmetric, or out of range)
|
||||
*/
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE int locate_token(std::uint64_t w, int q, std::uint64_t& offset, std::uint64_t& radius) noexcept
|
||||
{
|
||||
const int lz = count_leading_zeros(w);
|
||||
const std::uint64_t normalized = w << static_cast<unsigned>(lz);
|
||||
const auto index = static_cast<std::size_t>(2 * (q - pow5_128_smallest_power));
|
||||
const uint128_parts product = full_multiplication(normalized, pow5_128()[index]);
|
||||
std::uint64_t high = product.high;
|
||||
if (NLOHMANN_VIEW_UNLIKELY((high & 0x1FFu) == 0x1FFu))
|
||||
{
|
||||
// (as eisel_lemire(): the lower bits may carry into the result)
|
||||
const std::uint64_t second = full_multiplication(normalized, pow5_128()[index + 1]).high;
|
||||
high += product.low + second < product.low ? 1u : 0u;
|
||||
}
|
||||
const auto upperbit = static_cast<unsigned>(high >> 63u);
|
||||
const std::uint64_t mantissa = high >> (upperbit + 9);
|
||||
const std::uint64_t c = (mantissa + (mantissa & 1u)) >> 1u;
|
||||
// e2 as eisel_lemire() computes the exponent (log2(10) ~ 217706 / 2^16)
|
||||
const int e2 = static_cast<int>((217706 * static_cast<std::int64_t>(q)) >> 16) + 63 + static_cast<int>(upperbit) - lz - 52;
|
||||
const int t = q + 1 - e2;
|
||||
const int g = ((e2 * 315653) >> 20) + 1 - q; // (dec_exp as in zmij::to_shortest(), for a regular x)
|
||||
static const std::array<std::uint64_t, 17> fives = {{1u, 5u, 25u, 125u, 625u, 3125u, 15625u, 78125u, 390625u, 1953125u, 9765625u, 48828125u, 244140625u, 1220703125u, 6103515625u, 30517578125u, 152587890625u}};
|
||||
const std::uint64_t five = fives[static_cast<std::size_t>(-q)]; // 5^-q
|
||||
const unsigned s = static_cast<unsigned>(56 - t) & 63u;
|
||||
offset = ((2 * c * five) << s) - (w << 56u) + (std::uint64_t{32} << 56u);
|
||||
radius = (five << s) + ((c & 1u) == 0 ? 1u : 0u);
|
||||
// (c = 2^52: a power of two; c = 2^53: rounded up to one. With 8 to 16
|
||||
// fraction digits, as the batch's tokens have, t is in [15, 40] and g at
|
||||
// most 2. x lies halfway between two candidates only if its distance from
|
||||
// the token is a whole unit (g == 2) or a half (g == 1): such tokens are
|
||||
// left to the full conversion)
|
||||
const bool decided = c - ((std::uint64_t{1} << 52u) + 1) < (std::uint64_t{1} << 52u) - 1
|
||||
&& (offset & ((std::uint64_t{1} << 55u) - 1)) != 0;
|
||||
return decided ? g : 3;
|
||||
}
|
||||
|
||||
/// all bits set if b, else none
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t mask_if(bool b) noexcept
|
||||
{
|
||||
return std::uint64_t{0} - static_cast<std::uint64_t>(b);
|
||||
}
|
||||
|
||||
/// a if b, else c, without a branch (the conditions of token_digits_1/2() are
|
||||
/// as good as random, and GCC compiled their ?: as branches: one misprediction
|
||||
/// per float of canada.json on x86-64)
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t select_if(bool b, std::uint64_t a, std::uint64_t c) noexcept
|
||||
{
|
||||
return c ^ ((a ^ c) & mask_if(b));
|
||||
}
|
||||
|
||||
/// the distance of a position (see locate_token()) from the biased point 32
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t biased_distance(std::uint64_t position) noexcept
|
||||
{
|
||||
constexpr std::uint64_t center = std::uint64_t{32} << 56u;
|
||||
// |position - center|, without a branch: negated (complemented, plus 1) if below
|
||||
const std::uint64_t below = mask_if(position < center);
|
||||
return ((position - center) ^ below) - below;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief (g == 1) the digits dump() writes for a token, from the position of its
|
||||
double (see locate_token()): the multiple of 10 nearest to x if it is in the
|
||||
interval, else w rounded to x (an exact tie between two candidates, like
|
||||
754570942.33984373, whose double is 754570942.33984375, is excluded by
|
||||
locate_token())
|
||||
|
||||
With |x - w| at most the radius, which is below half the spacing of the
|
||||
candidates, the multiple of 10 nearest to x is w - r10 or the one above it.
|
||||
*/
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t token_digits_1(std::uint64_t w, std::uint64_t offset, std::uint64_t radius, std::uint64_t r10) noexcept
|
||||
{
|
||||
constexpr std::uint64_t one = std::uint64_t{1} << 56u;
|
||||
const std::uint64_t from = offset + (r10 * one); // x - (w - r10) (+ 32)
|
||||
const bool up = from > 37 * one; // (nearer to the multiple above)
|
||||
const bool shorter = biased_distance(from - (mask_if(up) & (10 * one))) < radius;
|
||||
const std::uint64_t a = w - r10 + (mask_if(up) & 10u);
|
||||
const std::uint64_t b = w + ((offset + (one / 2)) >> 56u) - 32; // w rounded to x
|
||||
return select_if(shorter, a, b);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief (g == 2) the digits dump() writes for a token, as token_digits_1(): the
|
||||
multiple of 100 nearest to x if it is in the interval, else the multiple of 10
|
||||
nearest to x
|
||||
*/
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t token_digits_2(std::uint64_t w, std::uint64_t offset, std::uint64_t radius, std::uint64_t r10, std::uint64_t r100) noexcept
|
||||
{
|
||||
constexpr std::uint64_t one = std::uint64_t{1} << 56u;
|
||||
const std::uint64_t from = offset + (r100 * one);
|
||||
const bool up = from > 82 * one;
|
||||
const bool shorter = biased_distance(from - (mask_if(up) & (100 * one))) < radius;
|
||||
const std::uint64_t a = w - r100 + (mask_if(up) & 100u);
|
||||
// x - (w - r10), in [-12, 21]: the multiple of 10 nearest to x is that one,
|
||||
// or one or two above, or one below
|
||||
const std::uint64_t y = offset + (r10 * one);
|
||||
const std::uint64_t steps = static_cast<std::uint64_t>(y > 37 * one) + static_cast<std::uint64_t>(y > 47 * one);
|
||||
const bool down = y < 27 * one;
|
||||
const std::uint64_t b = w - r10 + (10 * steps) - (mask_if(down) & 10u);
|
||||
return select_if(shorter, a, b);
|
||||
}
|
||||
|
||||
/// the four ASCII digits of x < 10^4, the first in the lowest byte (as
|
||||
/// little-endian memory holds them)
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint32_t four_digit_text(std::uint32_t x) noexcept
|
||||
{
|
||||
const std::uint32_t ab = (x * 5243u) >> 19u; // x / 100
|
||||
std::uint32_t v = ab | ((x - (ab * 100u)) << 16u);
|
||||
const std::uint32_t a = ((v * 103u) >> 10u) & 0x000F000Fu; // / 10 per 16-bit lane
|
||||
v = a | ((v - (a * 10u)) << 8u);
|
||||
return v + 0x30303030u;
|
||||
}
|
||||
|
||||
/// how the view's dump() writes a value
|
||||
struct dump_style
|
||||
{
|
||||
@@ -850,7 +711,6 @@ class view_serializer
|
||||
std::array<frame, 32> buffer; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::vector<frame> heap;
|
||||
small_stack<frame> stack(buffer.data(), buffer.size(), heap);
|
||||
float_batch batch;
|
||||
const char* const src = m_doc.src;
|
||||
const char* const src_end = src + m_doc.size;
|
||||
char* w = m_out.cursor();
|
||||
@@ -990,7 +850,7 @@ class view_serializer
|
||||
else if (enabled(fast_double::value))
|
||||
{
|
||||
room(64);
|
||||
w = write_double_ahead(w, src, n, batch);
|
||||
w = write_double_at(w, *n);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1015,20 +875,6 @@ class view_serializer
|
||||
case value_t::array:
|
||||
{
|
||||
const bool object = n->kind == node_kind::object;
|
||||
// (read-only documents, compact output) a pair of floats,
|
||||
// like the coordinates of GeoJSON, without a frame
|
||||
if (enabled(!Editable && !Pretty && !SourceNumbers && fast_double::value) && n->next == 3 && n->len == 2
|
||||
&& n[1].kind == node_kind::number_float && n[2].kind == node_kind::number_float)
|
||||
{
|
||||
room(2 * 64);
|
||||
*w++ = '[';
|
||||
w = write_double_ahead(w, src, n + 1, batch);
|
||||
*w++ = ',';
|
||||
w = write_double_ahead(w, src, n + 2, batch);
|
||||
*w++ = ']';
|
||||
n += 2; // (the element before the next node)
|
||||
break;
|
||||
}
|
||||
room(8);
|
||||
if (n->len == 0)
|
||||
{
|
||||
@@ -1138,254 +984,6 @@ class view_serializer
|
||||
}
|
||||
}
|
||||
|
||||
/// (float_batch) the mark of digits whose rounding carried beyond the
|
||||
/// token's last four (see mark())
|
||||
static constexpr std::uint64_t carried() noexcept
|
||||
{
|
||||
return std::uint64_t{1} << 63u;
|
||||
}
|
||||
|
||||
/// floats of 16 or 17 digits ahead of the walk, converted together (see
|
||||
/// convert_ahead()), with their digits as mark() keeps them
|
||||
struct float_batch
|
||||
{
|
||||
static constexpr std::size_t capacity() noexcept
|
||||
{
|
||||
return 256;
|
||||
}
|
||||
/// (on the heap, allocated by the first conversion: a frame of more
|
||||
/// than a page made every dump of a small document slower)
|
||||
struct entries
|
||||
{
|
||||
std::array<const node*, 256> at; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint64_t, 256> digits; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
};
|
||||
std::unique_ptr<entries> storage{};
|
||||
const node** at = nullptr;
|
||||
std::uint64_t* digits = nullptr;
|
||||
std::size_t count = 0;
|
||||
std::size_t next = 0;
|
||||
const node* scanned = nullptr; ///< where the last scan of the node array ended
|
||||
};
|
||||
|
||||
/// (doubles) the float at n as dump() writes it, at w (64 bytes of room):
|
||||
/// from the batch if it holds n
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE char* write_double_ahead(char* w, const char* src, const node* n, float_batch& batch)
|
||||
{
|
||||
#if NLOHMANN_VIEW_VECTOR && NLOHMANN_VIEW_LITTLE_ENDIAN
|
||||
// (digits 0: not written by the batch; carried(): rounding carried
|
||||
// beyond the last four digits)
|
||||
if (batch.next != batch.count && batch.at[batch.next] == n && batch.digits[batch.next] - 1 < carried() - 1)
|
||||
{
|
||||
return write_token_digits(w, src, *n, batch.digits[batch.next++]);
|
||||
}
|
||||
return write_double_beside(w, n, batch);
|
||||
#else
|
||||
static_cast<void>(src);
|
||||
static_cast<void>(batch);
|
||||
return write_double_at(w, *n);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if NLOHMANN_VIEW_VECTOR && NLOHMANN_VIEW_LITTLE_ENDIAN
|
||||
/// write_double_ahead() for a float that is not the batch's next one (out
|
||||
/// of line: the walk is faster with less code in its loop)
|
||||
NLOHMANN_VIEW_NOINLINE char* write_double_beside(char* w, const node* n, float_batch& batch)
|
||||
{
|
||||
// (an editable document's walk can skip nodes of the array)
|
||||
const std::less<const node*> before{}; // NOLINT(modernize-use-transparent-functors) C++11 has no std::less<>
|
||||
while (batch.next != batch.count && before(batch.at[batch.next], n))
|
||||
{
|
||||
++batch.next;
|
||||
}
|
||||
if ((batch.next != batch.count && batch.at[batch.next] == n) || convert_ahead(n, batch))
|
||||
{
|
||||
const std::uint64_t digits = batch.digits[batch.next++];
|
||||
if ((digits & carried()) != 0)
|
||||
{
|
||||
return write_carried_digits(w, *n, digits & ~carried());
|
||||
}
|
||||
if (digits != 0)
|
||||
{
|
||||
return write_token_digits(w, m_doc.src, *n, digits);
|
||||
}
|
||||
}
|
||||
return write_double_at(w, *n);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief convert the floats of 16 or 17 digits among the next nodes of the
|
||||
array at once; whether n is the first of them
|
||||
|
||||
Such tokens (like the coordinates of GeoJSON, written with 17 digits) are
|
||||
written as the token with its last four digits replaced
|
||||
(write_token_digits()). Each step (the digits, locate_token(),
|
||||
token_digits_1/2()) is a long chain of dependent operations; one loop per
|
||||
step over many floats lets the processor overlap the floats, which it does
|
||||
not across the steps of one float (on Apple M1, one loop for the digits and
|
||||
locate_token() made the dump of canada.json 12 % slower). Each node of the array
|
||||
is scanned once; a node that the walk does not reach in order (in an
|
||||
editable document) is written without the batch.
|
||||
*/
|
||||
NLOHMANN_VIEW_NOINLINE bool convert_ahead(const node* n, float_batch& batch)
|
||||
{
|
||||
const std::less<const node*> before{}; // NOLINT(modernize-use-transparent-functors) C++11 has no std::less<>
|
||||
const node* const last = m_doc.nodes + m_doc.node_count;
|
||||
if (before(n, batch.scanned) || before(n, m_doc.nodes) || !before(n, last))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const char* const src = m_doc.src;
|
||||
const char* const src_end = src + m_doc.size;
|
||||
const node* const stop = last - n > 1024 ? n + 1024 : last;
|
||||
batch.count = 0;
|
||||
batch.next = 0;
|
||||
// the digits of each token (and its last four from the text)
|
||||
std::array<std::uint64_t, 256> digits; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<int, 256> exponent; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint32_t, 256> last_four; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
const node* p = n;
|
||||
if (!batch.storage)
|
||||
{
|
||||
batch.storage.reset(new typename float_batch::entries); // NOLINT(cppcoreguidelines-owning-memory,modernize-make-unique): C++11
|
||||
batch.at = batch.storage->at.data();
|
||||
batch.digits = batch.storage->digits.data();
|
||||
}
|
||||
for (; p != stop && batch.count != float_batch::capacity(); ++p)
|
||||
{
|
||||
// a token of the source: 16 or 17 digits, at least 8 after the
|
||||
// point, no exponent, 32 bytes to read
|
||||
const unsigned frac_digits = p->extra >> 8u;
|
||||
const unsigned int_digits = p->extra & 0xFFu;
|
||||
const unsigned count = int_digits + frac_digits;
|
||||
if (p->kind == node_kind::number_float && (p->flags & node_flags::storage) == 0
|
||||
&& count - 16u <= 1u && frac_digits >= 8 && src_end - (src + p->off) >= 33)
|
||||
{
|
||||
const auto* const token = reinterpret_cast<const unsigned char*>(src + p->off); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
const unsigned sign = token[0] == '-' ? 1 : 0;
|
||||
const unsigned char* const first = token + sign;
|
||||
const std::uint64_t w = sixteen_digits(first, int_digits);
|
||||
// (the 17th digit follows the point; a token with an exponent
|
||||
// gets digits outside the range of locate_token(), and is not
|
||||
// written by the batch. A token 0.xxx with 16 significant
|
||||
// digits is: its double is written as "0." and its digits too)
|
||||
const std::uint64_t w17 = (w * 10) + static_cast<std::uint64_t>(first[17] - '0');
|
||||
const std::size_t i = batch.count++;
|
||||
batch.at[i] = p;
|
||||
if (p->len != sign + count + 1)
|
||||
{
|
||||
digits[i] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
digits[i] = count == 17 ? w17 : w;
|
||||
}
|
||||
exponent[i] = -static_cast<int>(frac_digits);
|
||||
// (byte 0: the last digit, byte 1: the last two, bits 16-29: the last four)
|
||||
std::uint32_t four = 0;
|
||||
std::memcpy(&four, token + p->len - 4, 4);
|
||||
four -= 0x30303030u;
|
||||
const std::uint32_t pairs = ((four & 0x0F0F0F0Fu) * 2561u) >> 8u; // (byte 0, 2: the pairs)
|
||||
last_four[i] = (four >> 24u) | ((pairs >> 16u & 0xFFu) << 8u) | ((((pairs & 0x00FF00FFu) * 6553601u) >> 16u) << 16u);
|
||||
}
|
||||
}
|
||||
batch.scanned = p;
|
||||
// the position of each double, and the floats of either kind
|
||||
std::array<std::uint64_t, 256> offset; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint64_t, 256> radius; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint8_t, 256> kind_1; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint8_t, 256> kind_2; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::size_t count_1 = 0;
|
||||
std::size_t count_2 = 0;
|
||||
for (std::size_t i = 0; i != batch.count; ++i)
|
||||
{
|
||||
const std::uint64_t w = digits[i];
|
||||
// (the range excludes the values of bytes that are not digits, only
|
||||
// in a damaged image loaded with image_check::bounds)
|
||||
const int g = w - 1000000000000000u < 99000000000000000u ? locate_token(w, exponent[i], offset[i], radius[i]) : 3;
|
||||
batch.digits[i] = mask_if(g <= 0) & ((last_four[i] >> 16u) + 1);
|
||||
kind_1[count_1] = static_cast<std::uint8_t>(i);
|
||||
count_1 += static_cast<std::size_t>(g == 1);
|
||||
kind_2[count_2] = static_cast<std::uint8_t>(i);
|
||||
count_2 += static_cast<std::size_t>(g == 2);
|
||||
}
|
||||
// the digits to write, in one loop per kind (see mark())
|
||||
for (std::size_t k = 0; k != count_1; ++k)
|
||||
{
|
||||
const std::size_t i = kind_1[k];
|
||||
const std::uint64_t s = token_digits_1(digits[i], offset[i], radius[i], last_four[i] & 0xFFu);
|
||||
batch.digits[i] = mark(digits[i], s, last_four[i] >> 16u);
|
||||
}
|
||||
for (std::size_t k = 0; k != count_2; ++k)
|
||||
{
|
||||
const std::size_t i = kind_2[k];
|
||||
const std::uint64_t s = token_digits_2(digits[i], offset[i], radius[i], last_four[i] & 0xFFu, (last_four[i] >> 8u) & 0xFFu);
|
||||
batch.digits[i] = mark(digits[i], s, last_four[i] >> 16u);
|
||||
}
|
||||
return batch.count != 0 && batch.at[0] == n;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief a float token of 16 or 17 digits as dump() writes it, at w (64 bytes
|
||||
of room), from the digits of its double (see locate_token()) whose
|
||||
rounding carried beyond the token's last four digits
|
||||
|
||||
(like 43.42027299999999 -> 43.420273): the digits go to the writer of
|
||||
Zmij's results as its 15 or 16 leading digits and one more, which omits
|
||||
the trailing zeros.
|
||||
*/
|
||||
NLOHMANN_VIEW_NOINLINE char* write_carried_digits(char* w, const node& n, std::uint64_t digits) const noexcept
|
||||
{
|
||||
const int exponent = -(n.extra >> 8u);
|
||||
const std::uint64_t leading = digits / 10;
|
||||
const auto last = static_cast<unsigned char>(digits - (leading * 10));
|
||||
const bool seventeen = digits >= 10000000000000000u;
|
||||
*w = '-';
|
||||
w += m_doc.src[n.off] == '-' ? 1 : 0;
|
||||
return ::nlohmann::detail::dtoa_impl::write_shortest(w, seventeen ? ::nlohmann::detail::zmij::shortest_decimal{leading, exponent, last, last != 0}
|
||||
: ::nlohmann::detail::zmij::shortest_decimal{digits, exponent - 1, 0, false});
|
||||
}
|
||||
|
||||
/// the digits s of a token with the digits w and the last four digits
|
||||
/// r4, as the batch keeps them: the new last four digits + 1 (only those
|
||||
/// are written over the token's), or s marked if it differs from w beyond
|
||||
/// them (0 stays for a float the batch does not write)
|
||||
static std::uint64_t mark(std::uint64_t w, std::uint64_t s, std::uint64_t r4) noexcept
|
||||
{
|
||||
const std::uint64_t last = s - (w - r4);
|
||||
return last < 10000 ? last + 1 : s | carried();
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief a float token of 16 or 17 digits as dump() writes it, at w (64 bytes
|
||||
of room): the token with its last four digits replaced by digits - 1 (see
|
||||
mark(); the others are the token's), and without trailing zeros after the
|
||||
point, but one
|
||||
*/
|
||||
char* write_token_digits(char* w, const char* src, const node& n, std::uint64_t digits)
|
||||
{
|
||||
const char* const token = src + n.off;
|
||||
std::memcpy(w, token, 32);
|
||||
char* const end = w + n.len;
|
||||
const std::uint32_t last = four_digit_text(static_cast<std::uint32_t>(digits - 1));
|
||||
std::memcpy(end - 4, &last, 4);
|
||||
// (the token's text before the new digits, from the source: reading
|
||||
// the output back would wait for the stores)
|
||||
std::uint64_t before = 0;
|
||||
std::memcpy(&before, token + n.len - 12, 8);
|
||||
const std::uint32_t last_zeros = last ^ 0x30303030u;
|
||||
const std::uint64_t before_zeros = before ^ 0x3030303030303030u;
|
||||
if (NLOHMANN_VIEW_UNLIKELY(last_zeros == 0 && before_zeros == 0))
|
||||
{
|
||||
return write_double_at(w, n); // (twelve or more trailing zeros)
|
||||
}
|
||||
const unsigned zeros = last_zeros != 0 ? (static_cast<unsigned>(count_leading_zeros(last_zeros)) / 8) - 4
|
||||
: 4 + (static_cast<unsigned>(count_leading_zeros(before_zeros)) / 8);
|
||||
const unsigned frac_digits = n.extra >> 8u;
|
||||
return end - (zeros < frac_digits ? zeros : frac_digits - 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// a float node as dump() writes it
|
||||
void write_float_node(const node& n)
|
||||
{
|
||||
|
||||
Generated
+9
-567
@@ -4682,8 +4682,6 @@ NLOHMANN_JSON_NAMESPACE_END
|
||||
|
||||
// #include <nlohmann/detail/view/scan.hpp>
|
||||
|
||||
// #include <nlohmann/detail/view/simd.hpp>
|
||||
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
namespace detail
|
||||
@@ -4768,56 +4766,12 @@ NLOHMANN_VIEW_NOINLINE FloatType float_value(const char* first, const node& n)
|
||||
return convert_float<FloatType>(first, last, dot, mantissa_end);
|
||||
}
|
||||
|
||||
#if NLOHMANN_VIEW_VECTOR
|
||||
/*!
|
||||
@brief the value of the first 16 digits of a run of digits with a point after
|
||||
int_digits of them (or none, if int_digits >= 16)
|
||||
|
||||
The 17 bytes at p are read at once: the bytes after the point move down by one,
|
||||
then pairs, quadruples, and octets of digits are combined by multiplications
|
||||
(after simdjson). The bytes [p, p + 32) must be readable. Bytes that are not
|
||||
digits (only in a damaged image loaded with image_check::bounds) give a wrong
|
||||
value, but no undefined behavior.
|
||||
*/
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t sixteen_digits(const unsigned char* p, unsigned int_digits) noexcept
|
||||
{
|
||||
#if NLOHMANN_VIEW_NEON
|
||||
const uint8x16_t low = vld1q_u8(p);
|
||||
const uint8x16_t index = vcombine_u8(vcreate_u8(0x0706050403020100u), vcreate_u8(0x0F0E0D0C0B0A0908u));
|
||||
const uint8x16_t before_point = vcltq_u8(index, vdupq_n_u8(static_cast<std::uint8_t>(int_digits)));
|
||||
const uint8x16_t digits = vsubq_u8(vbslq_u8(before_point, low, vextq_u8(low, vld1q_u8(p + 16), 1)), vdupq_n_u8('0'));
|
||||
const uint16x8_t twos = vpaddlq_u8(vmulq_u8(digits, vreinterpretq_u8_u16(vdupq_n_u16(0x010A))));
|
||||
const uint32x4_t fours = vpaddlq_u16(vmulq_u16(twos, vreinterpretq_u16_u32(vdupq_n_u32(0x00010064))));
|
||||
const uint64x2_t eights = vpaddlq_u32(vmulq_u32(fours, vreinterpretq_u32_u64(vdupq_n_u64(0x0000000100002710u))));
|
||||
return (vgetq_lane_u64(eights, 0) * 100000000u) + vgetq_lane_u64(eights, 1);
|
||||
#else
|
||||
// NOLINTBEGIN(portability-simd-intrinsics)
|
||||
const __m128i low = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i high = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p + 16))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i before_point = _mm_cmpgt_epi8(_mm_set1_epi8(static_cast<char>(int_digits)), _mm_setr_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15));
|
||||
const __m128i moved = _mm_or_si128(_mm_srli_si128(low, 1), _mm_slli_si128(high, 15));
|
||||
const __m128i digits = _mm_sub_epi8(_mm_or_si128(_mm_and_si128(before_point, low), _mm_andnot_si128(before_point, moved)), _mm_set1_epi8('0'));
|
||||
// (16-bit lanes: SSE2 has no multiply-add of bytes)
|
||||
const __m128i tens = _mm_setr_epi16(10, 1, 10, 1, 10, 1, 10, 1);
|
||||
const __m128i twos = _mm_packs_epi32(_mm_madd_epi16(_mm_unpacklo_epi8(digits, _mm_setzero_si128()), tens),
|
||||
_mm_madd_epi16(_mm_unpackhi_epi8(digits, _mm_setzero_si128()), tens));
|
||||
const __m128i fours = _mm_madd_epi16(twos, _mm_setr_epi16(100, 1, 100, 1, 100, 1, 100, 1));
|
||||
const __m128i eights = _mm_madd_epi16(_mm_packs_epi32(fours, fours), _mm_setr_epi16(10000, 1, 10000, 1, 10000, 1, 10000, 1));
|
||||
const auto upper = static_cast<std::uint32_t>(_mm_cvtsi128_si32(eights));
|
||||
const auto lower = static_cast<std::uint32_t>(_mm_cvtsi128_si32(_mm_srli_si128(eights, 4)));
|
||||
// NOLINTEND(portability-simd-intrinsics)
|
||||
return (std::uint64_t{upper} * 100000000u) + lower;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
/*!
|
||||
@brief the digits of a float token with at most 19 digits, from its layout
|
||||
|
||||
The digit layout recorded while parsing says where the integer digits, the
|
||||
fraction digits, and the exponent are, so the digits are read eight at a
|
||||
time without scanning (with SIMD, a token of 16 to 19 digits sixteen at a
|
||||
time).
|
||||
time without scanning.
|
||||
|
||||
@param[in] p first character of the token
|
||||
@param[in] e end of the token
|
||||
@@ -4827,34 +4781,15 @@ NLOHMANN_VIEW_ALWAYS_INLINE float_significand layout_decimal(const unsigned char
|
||||
{
|
||||
const bool negative = *p == '-';
|
||||
p += negative ? 1 : 0;
|
||||
std::uint64_t w = 0;
|
||||
std::uint64_t w = parse_upto19(p, int_digits, limit);
|
||||
p += int_digits;
|
||||
std::int64_t q = 0;
|
||||
#if NLOHMANN_VIEW_VECTOR
|
||||
const unsigned count = int_digits + frac_digits;
|
||||
if (count >= 16 && limit - p >= 32)
|
||||
if (frac_digits != 0)
|
||||
{
|
||||
// (the point, if any, follows the integer digits; the digits after
|
||||
// the first 16 are at most three)
|
||||
w = sixteen_digits(p, int_digits);
|
||||
for (unsigned i = 16; i < count; ++i)
|
||||
{
|
||||
w = (w * 10) + static_cast<std::uint64_t>(p[i + (i >= int_digits ? 1 : 0)] - '0');
|
||||
}
|
||||
p += count + (frac_digits != 0 ? 1 : 0);
|
||||
w = (w * int_pow10(frac_digits)) + parse_upto19(p + 1, frac_digits, limit);
|
||||
p += 1 + frac_digits;
|
||||
q = -static_cast<std::int64_t>(frac_digits);
|
||||
}
|
||||
else /* NOLINT(readability/braces) */
|
||||
#endif
|
||||
{
|
||||
w = parse_upto19(p, int_digits, limit);
|
||||
p += int_digits;
|
||||
if (frac_digits != 0)
|
||||
{
|
||||
w = (w * int_pow10(frac_digits)) + parse_upto19(p + 1, frac_digits, limit);
|
||||
p += 1 + frac_digits;
|
||||
q = -static_cast<std::int64_t>(frac_digits);
|
||||
}
|
||||
}
|
||||
if (p != e)
|
||||
{
|
||||
// [eE][+-]digits; huge exponents saturate (the parser rejected
|
||||
@@ -4889,22 +4824,12 @@ NLOHMANN_VIEW_ALWAYS_INLINE float_significand layout_decimal(const unsigned char
|
||||
The result is correctly rounded by the lexer's conversion
|
||||
(detail::decimal_to_float(): Clinger's fast path where both operands are
|
||||
exact, else the Eisel-Lemire algorithm, which needs no fallback for up to 19
|
||||
digits), so it is the value parse() produces. Tokens of 16 or more digits go
|
||||
to Eisel-Lemire directly: the fast path takes few of them (none of 17 digits),
|
||||
and its check is a branch that data like GeoJSON coordinates mispredicts.
|
||||
digits), so it is the value parse() produces.
|
||||
*/
|
||||
template<typename FloatType>
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE FloatType layout_float(const unsigned char* p, const unsigned char* e, unsigned int_digits, unsigned frac_digits, const unsigned char* limit) noexcept
|
||||
{
|
||||
const float_significand d = layout_decimal(p, e, int_digits, frac_digits, limit);
|
||||
if (int_digits + frac_digits >= 16)
|
||||
{
|
||||
using format = ieee_binary_format<std::numeric_limits<FloatType>::digits>;
|
||||
native_float_t<FloatType> value{};
|
||||
float_from_bits(eisel_lemire<format>(d.exponent, d.w) | (d.negative ? (std::uint64_t{1} << format::sign_bit()) : 0u), value);
|
||||
return value;
|
||||
}
|
||||
return decimal_to_float<FloatType>(d);
|
||||
return decimal_to_float<FloatType>(layout_decimal(p, e, int_digits, frac_digits, limit));
|
||||
}
|
||||
|
||||
/// the value of a float set by an edit: its token (the shortest round-trip
|
||||
@@ -4923,80 +4848,6 @@ NLOHMANN_VIEW_NOINLINE FloatType edited_float(const char* token, const node& n)
|
||||
return float_value<FloatType>(token, n);
|
||||
}
|
||||
|
||||
#if NLOHMANN_VIEW_VECTOR
|
||||
/// eisel_lemire() for the token w * 10^q of coordinate_float()
|
||||
template<typename Format>
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t coordinate_bits(std::int64_t q, std::uint64_t w, Format /*format*/) noexcept
|
||||
{
|
||||
return eisel_lemire<Format>(q, w);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief (binary64) eisel_lemire() without the cases that tokens of 16 or 17
|
||||
digits with 5 or more after the point cannot reach: a value of 0, a
|
||||
subnormal, an infinity, or a tie (q < -4)
|
||||
*/
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t coordinate_bits(std::int64_t q, std::uint64_t w, ieee_binary_format<53> format) noexcept
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(w == 0 || q > -5))
|
||||
{
|
||||
return eisel_lemire<ieee_binary_format<53>>(q, w);
|
||||
}
|
||||
static_cast<void>(format);
|
||||
const int lz = count_leading_zeros(w);
|
||||
const std::uint64_t normalized = w << static_cast<unsigned>(lz);
|
||||
const auto index = static_cast<std::size_t>(2 * (q - pow5_128_smallest_power));
|
||||
const uint128_parts product = full_multiplication(normalized, pow5_128()[index]);
|
||||
std::uint64_t high = product.high;
|
||||
if (NLOHMANN_VIEW_UNLIKELY((high & 0x1FFu) == 0x1FFu))
|
||||
{
|
||||
// (the lower bits may carry into the result)
|
||||
const std::uint64_t second = full_multiplication(normalized, pow5_128()[index + 1]).high;
|
||||
high += product.low + second < product.low ? 1u : 0u;
|
||||
}
|
||||
const auto upperbit = static_cast<unsigned>(high >> 63u);
|
||||
std::uint64_t mantissa = high >> (upperbit + 9);
|
||||
auto power2 = static_cast<std::uint64_t>(((217706 * q) >> 16) + 63 + static_cast<std::int64_t>(upperbit) - lz + 1023);
|
||||
mantissa = (mantissa + (mantissa & 1u)) >> 1u;
|
||||
const bool carry = mantissa >= (std::uint64_t{2} << 52u);
|
||||
mantissa = carry ? (std::uint64_t{1} << 52u) : mantissa;
|
||||
power2 += carry ? 1u : 0u;
|
||||
return (mantissa & ~(std::uint64_t{1} << 52u)) | (power2 << 52u);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief (binary32 and binary64) the value of a float token of the source with
|
||||
16 or 17 digits, a point, and no exponent, the shape of most coordinates
|
||||
written with full precision; whether the token of n has this shape
|
||||
|
||||
These tokens skip the general layout of layout_decimal(): no digits beyond
|
||||
the 17th, no exponent (and Eisel-Lemire directly, as in layout_float()).
|
||||
*/
|
||||
template<typename FloatType>
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE bool coordinate_float(const document_data& d, const node& n, FloatType& value) noexcept
|
||||
{
|
||||
const unsigned int_digits = n.extra & 0xFFu;
|
||||
const unsigned frac_digits = n.extra >> 8u;
|
||||
const unsigned count = int_digits + frac_digits;
|
||||
const auto* const first = reinterpret_cast<const unsigned char*>(d.src + n.off); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
const unsigned negative = first[0] == '-' ? 1u : 0u;
|
||||
if (count - 16u > 1u || frac_digits == 0 || n.len != negative + count + 1 || d.size - n.off < 33)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const unsigned char* const digits = first + negative;
|
||||
const std::uint64_t w = sixteen_digits(digits, int_digits);
|
||||
// (the 17th digit follows the point)
|
||||
const std::uint64_t w17 = (w * 10) + static_cast<std::uint64_t>(digits[17] - '0');
|
||||
using format = ieee_binary_format<std::numeric_limits<FloatType>::digits>;
|
||||
native_float_t<FloatType> result{};
|
||||
float_from_bits(coordinate_bits(-static_cast<std::int64_t>(frac_digits), count == 17 ? w17 : w, format{}) | (static_cast<std::uint64_t>(negative) << format::sign_bit()), result);
|
||||
value = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/// the value of the float token of a node, as parse() converts it; floats and
|
||||
/// doubles with at most 19 digits are converted from the digit layout
|
||||
template<typename FloatType>
|
||||
@@ -5012,13 +4863,6 @@ FloatType float_value(const document_data& d, const node& n)
|
||||
template<typename FloatType>
|
||||
FloatType float_value(const document_data& d, const node& n, std::true_type /*binary32 or binary64*/)
|
||||
{
|
||||
#if NLOHMANN_VIEW_VECTOR
|
||||
FloatType value {};
|
||||
if (coordinate_float(d, n, value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
#endif
|
||||
const unsigned int_digits = n.extra & 0xFFu;
|
||||
const unsigned frac_digits = n.extra >> 8u;
|
||||
if (NLOHMANN_VIEW_LIKELY(int_digits + frac_digits <= 19)) // (255 marks "many")
|
||||
@@ -6588,9 +6432,7 @@ NLOHMANN_JSON_NAMESPACE_END
|
||||
#include <cstddef> // size_t
|
||||
#include <cstdint> // uint8_t, uint32_t
|
||||
#include <cstring> // memcpy, memset
|
||||
#include <functional> // less
|
||||
#include <limits> // numeric_limits
|
||||
#include <memory> // unique_ptr
|
||||
#include <type_traits> // integral_constant
|
||||
#include <vector> // vector
|
||||
|
||||
@@ -6965,143 +6807,6 @@ struct depth_counter<false>
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
@brief where the double x nearest to a token w * 10^q lies, for 10^15 <= w <
|
||||
10^17 and -17 <= q <= -1 (Eisel-Lemire, then a fixed-point position)
|
||||
|
||||
dump() writes the shortest decimal in the rounding interval of x (the closest
|
||||
one if there are several, see zmij::to_shortest()). Its digits are those of a
|
||||
multiple of 10^g of the token's last digit, with g = dec_exp + 1 - q for x's
|
||||
decimal exponent dec_exp: the multiple of 10^g nearest to x if it lies in the
|
||||
interval (at most one does), else the multiple of 10^(g - 1) nearest to x
|
||||
(see token_digits_1() and token_digits_2()). For g <= 0, the token itself is
|
||||
in the interval.
|
||||
|
||||
The position is exact in a 64-bit fixed-point number with 56 bits after the
|
||||
point (of the token's last digit): x / 10^q = 2c * 5^-q / 2^t for x = c * 2^e2
|
||||
and t = q + 1 - e2, with at most 56 bits after the point here, and |x / 10^q -
|
||||
w| is at most half the spacing of the doubles, below 12 units. So only the low
|
||||
64 bits of 2c * 5^-q are needed. c and e2 come from Eisel-Lemire's product
|
||||
directly (as eisel_lemire() rounds it: no tie is possible for q < -4, and its
|
||||
second product is added where it can carry into the result).
|
||||
|
||||
@param[out] offset x / 10^q - w + 32, with 56 bits after the point
|
||||
@param[out] radius half the spacing of the doubles at x, likewise (+ 1 if
|
||||
the interval is closed)
|
||||
@return g; 3 if the digits are not decided here (a power of two, whose
|
||||
interval is not symmetric, or out of range)
|
||||
*/
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE int locate_token(std::uint64_t w, int q, std::uint64_t& offset, std::uint64_t& radius) noexcept
|
||||
{
|
||||
const int lz = count_leading_zeros(w);
|
||||
const std::uint64_t normalized = w << static_cast<unsigned>(lz);
|
||||
const auto index = static_cast<std::size_t>(2 * (q - pow5_128_smallest_power));
|
||||
const uint128_parts product = full_multiplication(normalized, pow5_128()[index]);
|
||||
std::uint64_t high = product.high;
|
||||
if (NLOHMANN_VIEW_UNLIKELY((high & 0x1FFu) == 0x1FFu))
|
||||
{
|
||||
// (as eisel_lemire(): the lower bits may carry into the result)
|
||||
const std::uint64_t second = full_multiplication(normalized, pow5_128()[index + 1]).high;
|
||||
high += product.low + second < product.low ? 1u : 0u;
|
||||
}
|
||||
const auto upperbit = static_cast<unsigned>(high >> 63u);
|
||||
const std::uint64_t mantissa = high >> (upperbit + 9);
|
||||
const std::uint64_t c = (mantissa + (mantissa & 1u)) >> 1u;
|
||||
// e2 as eisel_lemire() computes the exponent (log2(10) ~ 217706 / 2^16)
|
||||
const int e2 = static_cast<int>((217706 * static_cast<std::int64_t>(q)) >> 16) + 63 + static_cast<int>(upperbit) - lz - 52;
|
||||
const int t = q + 1 - e2;
|
||||
const int g = ((e2 * 315653) >> 20) + 1 - q; // (dec_exp as in zmij::to_shortest(), for a regular x)
|
||||
static const std::array<std::uint64_t, 17> fives = {{1u, 5u, 25u, 125u, 625u, 3125u, 15625u, 78125u, 390625u, 1953125u, 9765625u, 48828125u, 244140625u, 1220703125u, 6103515625u, 30517578125u, 152587890625u}};
|
||||
const std::uint64_t five = fives[static_cast<std::size_t>(-q)]; // 5^-q
|
||||
const unsigned s = static_cast<unsigned>(56 - t) & 63u;
|
||||
offset = ((2 * c * five) << s) - (w << 56u) + (std::uint64_t{32} << 56u);
|
||||
radius = (five << s) + ((c & 1u) == 0 ? 1u : 0u);
|
||||
// (c = 2^52: a power of two; c = 2^53: rounded up to one. With 8 to 16
|
||||
// fraction digits, as the batch's tokens have, t is in [15, 40] and g at
|
||||
// most 2. x lies halfway between two candidates only if its distance from
|
||||
// the token is a whole unit (g == 2) or a half (g == 1): such tokens are
|
||||
// left to the full conversion)
|
||||
const bool decided = c - ((std::uint64_t{1} << 52u) + 1) < (std::uint64_t{1} << 52u) - 1
|
||||
&& (offset & ((std::uint64_t{1} << 55u) - 1)) != 0;
|
||||
return decided ? g : 3;
|
||||
}
|
||||
|
||||
/// all bits set if b, else none
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t mask_if(bool b) noexcept
|
||||
{
|
||||
return std::uint64_t{0} - static_cast<std::uint64_t>(b);
|
||||
}
|
||||
|
||||
/// a if b, else c, without a branch (the conditions of token_digits_1/2() are
|
||||
/// as good as random, and GCC compiled their ?: as branches: one misprediction
|
||||
/// per float of canada.json on x86-64)
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t select_if(bool b, std::uint64_t a, std::uint64_t c) noexcept
|
||||
{
|
||||
return c ^ ((a ^ c) & mask_if(b));
|
||||
}
|
||||
|
||||
/// the distance of a position (see locate_token()) from the biased point 32
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t biased_distance(std::uint64_t position) noexcept
|
||||
{
|
||||
constexpr std::uint64_t center = std::uint64_t{32} << 56u;
|
||||
// |position - center|, without a branch: negated (complemented, plus 1) if below
|
||||
const std::uint64_t below = mask_if(position < center);
|
||||
return ((position - center) ^ below) - below;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief (g == 1) the digits dump() writes for a token, from the position of its
|
||||
double (see locate_token()): the multiple of 10 nearest to x if it is in the
|
||||
interval, else w rounded to x (an exact tie between two candidates, like
|
||||
754570942.33984373, whose double is 754570942.33984375, is excluded by
|
||||
locate_token())
|
||||
|
||||
With |x - w| at most the radius, which is below half the spacing of the
|
||||
candidates, the multiple of 10 nearest to x is w - r10 or the one above it.
|
||||
*/
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t token_digits_1(std::uint64_t w, std::uint64_t offset, std::uint64_t radius, std::uint64_t r10) noexcept
|
||||
{
|
||||
constexpr std::uint64_t one = std::uint64_t{1} << 56u;
|
||||
const std::uint64_t from = offset + (r10 * one); // x - (w - r10) (+ 32)
|
||||
const bool up = from > 37 * one; // (nearer to the multiple above)
|
||||
const bool shorter = biased_distance(from - (mask_if(up) & (10 * one))) < radius;
|
||||
const std::uint64_t a = w - r10 + (mask_if(up) & 10u);
|
||||
const std::uint64_t b = w + ((offset + (one / 2)) >> 56u) - 32; // w rounded to x
|
||||
return select_if(shorter, a, b);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief (g == 2) the digits dump() writes for a token, as token_digits_1(): the
|
||||
multiple of 100 nearest to x if it is in the interval, else the multiple of 10
|
||||
nearest to x
|
||||
*/
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t token_digits_2(std::uint64_t w, std::uint64_t offset, std::uint64_t radius, std::uint64_t r10, std::uint64_t r100) noexcept
|
||||
{
|
||||
constexpr std::uint64_t one = std::uint64_t{1} << 56u;
|
||||
const std::uint64_t from = offset + (r100 * one);
|
||||
const bool up = from > 82 * one;
|
||||
const bool shorter = biased_distance(from - (mask_if(up) & (100 * one))) < radius;
|
||||
const std::uint64_t a = w - r100 + (mask_if(up) & 100u);
|
||||
// x - (w - r10), in [-12, 21]: the multiple of 10 nearest to x is that one,
|
||||
// or one or two above, or one below
|
||||
const std::uint64_t y = offset + (r10 * one);
|
||||
const std::uint64_t steps = static_cast<std::uint64_t>(y > 37 * one) + static_cast<std::uint64_t>(y > 47 * one);
|
||||
const bool down = y < 27 * one;
|
||||
const std::uint64_t b = w - r10 + (10 * steps) - (mask_if(down) & 10u);
|
||||
return select_if(shorter, a, b);
|
||||
}
|
||||
|
||||
/// the four ASCII digits of x < 10^4, the first in the lowest byte (as
|
||||
/// little-endian memory holds them)
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint32_t four_digit_text(std::uint32_t x) noexcept
|
||||
{
|
||||
const std::uint32_t ab = (x * 5243u) >> 19u; // x / 100
|
||||
std::uint32_t v = ab | ((x - (ab * 100u)) << 16u);
|
||||
const std::uint32_t a = ((v * 103u) >> 10u) & 0x000F000Fu; // / 10 per 16-bit lane
|
||||
v = a | ((v - (a * 10u)) << 8u);
|
||||
return v + 0x30303030u;
|
||||
}
|
||||
|
||||
/// how the view's dump() writes a value
|
||||
struct dump_style
|
||||
{
|
||||
@@ -7429,7 +7134,6 @@ class view_serializer
|
||||
std::array<frame, 32> buffer; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::vector<frame> heap;
|
||||
small_stack<frame> stack(buffer.data(), buffer.size(), heap);
|
||||
float_batch batch;
|
||||
const char* const src = m_doc.src;
|
||||
const char* const src_end = src + m_doc.size;
|
||||
char* w = m_out.cursor();
|
||||
@@ -7569,7 +7273,7 @@ class view_serializer
|
||||
else if (enabled(fast_double::value))
|
||||
{
|
||||
room(64);
|
||||
w = write_double_ahead(w, src, n, batch);
|
||||
w = write_double_at(w, *n);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -7594,20 +7298,6 @@ class view_serializer
|
||||
case value_t::array:
|
||||
{
|
||||
const bool object = n->kind == node_kind::object;
|
||||
// (read-only documents, compact output) a pair of floats,
|
||||
// like the coordinates of GeoJSON, without a frame
|
||||
if (enabled(!Editable && !Pretty && !SourceNumbers && fast_double::value) && n->next == 3 && n->len == 2
|
||||
&& n[1].kind == node_kind::number_float && n[2].kind == node_kind::number_float)
|
||||
{
|
||||
room(2 * 64);
|
||||
*w++ = '[';
|
||||
w = write_double_ahead(w, src, n + 1, batch);
|
||||
*w++ = ',';
|
||||
w = write_double_ahead(w, src, n + 2, batch);
|
||||
*w++ = ']';
|
||||
n += 2; // (the element before the next node)
|
||||
break;
|
||||
}
|
||||
room(8);
|
||||
if (n->len == 0)
|
||||
{
|
||||
@@ -7717,254 +7407,6 @@ class view_serializer
|
||||
}
|
||||
}
|
||||
|
||||
/// (float_batch) the mark of digits whose rounding carried beyond the
|
||||
/// token's last four (see mark())
|
||||
static constexpr std::uint64_t carried() noexcept
|
||||
{
|
||||
return std::uint64_t{1} << 63u;
|
||||
}
|
||||
|
||||
/// floats of 16 or 17 digits ahead of the walk, converted together (see
|
||||
/// convert_ahead()), with their digits as mark() keeps them
|
||||
struct float_batch
|
||||
{
|
||||
static constexpr std::size_t capacity() noexcept
|
||||
{
|
||||
return 256;
|
||||
}
|
||||
/// (on the heap, allocated by the first conversion: a frame of more
|
||||
/// than a page made every dump of a small document slower)
|
||||
struct entries
|
||||
{
|
||||
std::array<const node*, 256> at; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint64_t, 256> digits; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
};
|
||||
std::unique_ptr<entries> storage{};
|
||||
const node** at = nullptr;
|
||||
std::uint64_t* digits = nullptr;
|
||||
std::size_t count = 0;
|
||||
std::size_t next = 0;
|
||||
const node* scanned = nullptr; ///< where the last scan of the node array ended
|
||||
};
|
||||
|
||||
/// (doubles) the float at n as dump() writes it, at w (64 bytes of room):
|
||||
/// from the batch if it holds n
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE char* write_double_ahead(char* w, const char* src, const node* n, float_batch& batch)
|
||||
{
|
||||
#if NLOHMANN_VIEW_VECTOR && NLOHMANN_VIEW_LITTLE_ENDIAN
|
||||
// (digits 0: not written by the batch; carried(): rounding carried
|
||||
// beyond the last four digits)
|
||||
if (batch.next != batch.count && batch.at[batch.next] == n && batch.digits[batch.next] - 1 < carried() - 1)
|
||||
{
|
||||
return write_token_digits(w, src, *n, batch.digits[batch.next++]);
|
||||
}
|
||||
return write_double_beside(w, n, batch);
|
||||
#else
|
||||
static_cast<void>(src);
|
||||
static_cast<void>(batch);
|
||||
return write_double_at(w, *n);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if NLOHMANN_VIEW_VECTOR && NLOHMANN_VIEW_LITTLE_ENDIAN
|
||||
/// write_double_ahead() for a float that is not the batch's next one (out
|
||||
/// of line: the walk is faster with less code in its loop)
|
||||
NLOHMANN_VIEW_NOINLINE char* write_double_beside(char* w, const node* n, float_batch& batch)
|
||||
{
|
||||
// (an editable document's walk can skip nodes of the array)
|
||||
const std::less<const node*> before{}; // NOLINT(modernize-use-transparent-functors) C++11 has no std::less<>
|
||||
while (batch.next != batch.count && before(batch.at[batch.next], n))
|
||||
{
|
||||
++batch.next;
|
||||
}
|
||||
if ((batch.next != batch.count && batch.at[batch.next] == n) || convert_ahead(n, batch))
|
||||
{
|
||||
const std::uint64_t digits = batch.digits[batch.next++];
|
||||
if ((digits & carried()) != 0)
|
||||
{
|
||||
return write_carried_digits(w, *n, digits & ~carried());
|
||||
}
|
||||
if (digits != 0)
|
||||
{
|
||||
return write_token_digits(w, m_doc.src, *n, digits);
|
||||
}
|
||||
}
|
||||
return write_double_at(w, *n);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief convert the floats of 16 or 17 digits among the next nodes of the
|
||||
array at once; whether n is the first of them
|
||||
|
||||
Such tokens (like the coordinates of GeoJSON, written with 17 digits) are
|
||||
written as the token with its last four digits replaced
|
||||
(write_token_digits()). Each step (the digits, locate_token(),
|
||||
token_digits_1/2()) is a long chain of dependent operations; one loop per
|
||||
step over many floats lets the processor overlap the floats, which it does
|
||||
not across the steps of one float (on Apple M1, one loop for the digits and
|
||||
locate_token() made the dump of canada.json 12 % slower). Each node of the array
|
||||
is scanned once; a node that the walk does not reach in order (in an
|
||||
editable document) is written without the batch.
|
||||
*/
|
||||
NLOHMANN_VIEW_NOINLINE bool convert_ahead(const node* n, float_batch& batch)
|
||||
{
|
||||
const std::less<const node*> before{}; // NOLINT(modernize-use-transparent-functors) C++11 has no std::less<>
|
||||
const node* const last = m_doc.nodes + m_doc.node_count;
|
||||
if (before(n, batch.scanned) || before(n, m_doc.nodes) || !before(n, last))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const char* const src = m_doc.src;
|
||||
const char* const src_end = src + m_doc.size;
|
||||
const node* const stop = last - n > 1024 ? n + 1024 : last;
|
||||
batch.count = 0;
|
||||
batch.next = 0;
|
||||
// the digits of each token (and its last four from the text)
|
||||
std::array<std::uint64_t, 256> digits; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<int, 256> exponent; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint32_t, 256> last_four; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
const node* p = n;
|
||||
if (!batch.storage)
|
||||
{
|
||||
batch.storage.reset(new typename float_batch::entries); // NOLINT(cppcoreguidelines-owning-memory,modernize-make-unique): C++11
|
||||
batch.at = batch.storage->at.data();
|
||||
batch.digits = batch.storage->digits.data();
|
||||
}
|
||||
for (; p != stop && batch.count != float_batch::capacity(); ++p)
|
||||
{
|
||||
// a token of the source: 16 or 17 digits, at least 8 after the
|
||||
// point, no exponent, 32 bytes to read
|
||||
const unsigned frac_digits = p->extra >> 8u;
|
||||
const unsigned int_digits = p->extra & 0xFFu;
|
||||
const unsigned count = int_digits + frac_digits;
|
||||
if (p->kind == node_kind::number_float && (p->flags & node_flags::storage) == 0
|
||||
&& count - 16u <= 1u && frac_digits >= 8 && src_end - (src + p->off) >= 33)
|
||||
{
|
||||
const auto* const token = reinterpret_cast<const unsigned char*>(src + p->off); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
const unsigned sign = token[0] == '-' ? 1 : 0;
|
||||
const unsigned char* const first = token + sign;
|
||||
const std::uint64_t w = sixteen_digits(first, int_digits);
|
||||
// (the 17th digit follows the point; a token with an exponent
|
||||
// gets digits outside the range of locate_token(), and is not
|
||||
// written by the batch. A token 0.xxx with 16 significant
|
||||
// digits is: its double is written as "0." and its digits too)
|
||||
const std::uint64_t w17 = (w * 10) + static_cast<std::uint64_t>(first[17] - '0');
|
||||
const std::size_t i = batch.count++;
|
||||
batch.at[i] = p;
|
||||
if (p->len != sign + count + 1)
|
||||
{
|
||||
digits[i] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
digits[i] = count == 17 ? w17 : w;
|
||||
}
|
||||
exponent[i] = -static_cast<int>(frac_digits);
|
||||
// (byte 0: the last digit, byte 1: the last two, bits 16-29: the last four)
|
||||
std::uint32_t four = 0;
|
||||
std::memcpy(&four, token + p->len - 4, 4);
|
||||
four -= 0x30303030u;
|
||||
const std::uint32_t pairs = ((four & 0x0F0F0F0Fu) * 2561u) >> 8u; // (byte 0, 2: the pairs)
|
||||
last_four[i] = (four >> 24u) | ((pairs >> 16u & 0xFFu) << 8u) | ((((pairs & 0x00FF00FFu) * 6553601u) >> 16u) << 16u);
|
||||
}
|
||||
}
|
||||
batch.scanned = p;
|
||||
// the position of each double, and the floats of either kind
|
||||
std::array<std::uint64_t, 256> offset; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint64_t, 256> radius; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint8_t, 256> kind_1; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint8_t, 256> kind_2; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::size_t count_1 = 0;
|
||||
std::size_t count_2 = 0;
|
||||
for (std::size_t i = 0; i != batch.count; ++i)
|
||||
{
|
||||
const std::uint64_t w = digits[i];
|
||||
// (the range excludes the values of bytes that are not digits, only
|
||||
// in a damaged image loaded with image_check::bounds)
|
||||
const int g = w - 1000000000000000u < 99000000000000000u ? locate_token(w, exponent[i], offset[i], radius[i]) : 3;
|
||||
batch.digits[i] = mask_if(g <= 0) & ((last_four[i] >> 16u) + 1);
|
||||
kind_1[count_1] = static_cast<std::uint8_t>(i);
|
||||
count_1 += static_cast<std::size_t>(g == 1);
|
||||
kind_2[count_2] = static_cast<std::uint8_t>(i);
|
||||
count_2 += static_cast<std::size_t>(g == 2);
|
||||
}
|
||||
// the digits to write, in one loop per kind (see mark())
|
||||
for (std::size_t k = 0; k != count_1; ++k)
|
||||
{
|
||||
const std::size_t i = kind_1[k];
|
||||
const std::uint64_t s = token_digits_1(digits[i], offset[i], radius[i], last_four[i] & 0xFFu);
|
||||
batch.digits[i] = mark(digits[i], s, last_four[i] >> 16u);
|
||||
}
|
||||
for (std::size_t k = 0; k != count_2; ++k)
|
||||
{
|
||||
const std::size_t i = kind_2[k];
|
||||
const std::uint64_t s = token_digits_2(digits[i], offset[i], radius[i], last_four[i] & 0xFFu, (last_four[i] >> 8u) & 0xFFu);
|
||||
batch.digits[i] = mark(digits[i], s, last_four[i] >> 16u);
|
||||
}
|
||||
return batch.count != 0 && batch.at[0] == n;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief a float token of 16 or 17 digits as dump() writes it, at w (64 bytes
|
||||
of room), from the digits of its double (see locate_token()) whose
|
||||
rounding carried beyond the token's last four digits
|
||||
|
||||
(like 43.42027299999999 -> 43.420273): the digits go to the writer of
|
||||
Zmij's results as its 15 or 16 leading digits and one more, which omits
|
||||
the trailing zeros.
|
||||
*/
|
||||
NLOHMANN_VIEW_NOINLINE char* write_carried_digits(char* w, const node& n, std::uint64_t digits) const noexcept
|
||||
{
|
||||
const int exponent = -(n.extra >> 8u);
|
||||
const std::uint64_t leading = digits / 10;
|
||||
const auto last = static_cast<unsigned char>(digits - (leading * 10));
|
||||
const bool seventeen = digits >= 10000000000000000u;
|
||||
*w = '-';
|
||||
w += m_doc.src[n.off] == '-' ? 1 : 0;
|
||||
return ::nlohmann::detail::dtoa_impl::write_shortest(w, seventeen ? ::nlohmann::detail::zmij::shortest_decimal{leading, exponent, last, last != 0}
|
||||
: ::nlohmann::detail::zmij::shortest_decimal{digits, exponent - 1, 0, false});
|
||||
}
|
||||
|
||||
/// the digits s of a token with the digits w and the last four digits
|
||||
/// r4, as the batch keeps them: the new last four digits + 1 (only those
|
||||
/// are written over the token's), or s marked if it differs from w beyond
|
||||
/// them (0 stays for a float the batch does not write)
|
||||
static std::uint64_t mark(std::uint64_t w, std::uint64_t s, std::uint64_t r4) noexcept
|
||||
{
|
||||
const std::uint64_t last = s - (w - r4);
|
||||
return last < 10000 ? last + 1 : s | carried();
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief a float token of 16 or 17 digits as dump() writes it, at w (64 bytes
|
||||
of room): the token with its last four digits replaced by digits - 1 (see
|
||||
mark(); the others are the token's), and without trailing zeros after the
|
||||
point, but one
|
||||
*/
|
||||
char* write_token_digits(char* w, const char* src, const node& n, std::uint64_t digits)
|
||||
{
|
||||
const char* const token = src + n.off;
|
||||
std::memcpy(w, token, 32);
|
||||
char* const end = w + n.len;
|
||||
const std::uint32_t last = four_digit_text(static_cast<std::uint32_t>(digits - 1));
|
||||
std::memcpy(end - 4, &last, 4);
|
||||
// (the token's text before the new digits, from the source: reading
|
||||
// the output back would wait for the stores)
|
||||
std::uint64_t before = 0;
|
||||
std::memcpy(&before, token + n.len - 12, 8);
|
||||
const std::uint32_t last_zeros = last ^ 0x30303030u;
|
||||
const std::uint64_t before_zeros = before ^ 0x3030303030303030u;
|
||||
if (NLOHMANN_VIEW_UNLIKELY(last_zeros == 0 && before_zeros == 0))
|
||||
{
|
||||
return write_double_at(w, n); // (twelve or more trailing zeros)
|
||||
}
|
||||
const unsigned zeros = last_zeros != 0 ? (static_cast<unsigned>(count_leading_zeros(last_zeros)) / 8) - 4
|
||||
: 4 + (static_cast<unsigned>(count_leading_zeros(before_zeros)) / 8);
|
||||
const unsigned frac_digits = n.extra >> 8u;
|
||||
return end - (zeros < frac_digits ? zeros : frac_digits - 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// a float node as dump() writes it
|
||||
void write_float_node(const node& n)
|
||||
{
|
||||
|
||||
@@ -35,9 +35,9 @@ using json_view_test::has_duplicate_keys;
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// These tests were split off unit-json_view.cpp, whose object file got too large for the MinGW
|
||||
// linker (see json_view_test_helpers.hpp). Like that file, this one is also built
|
||||
// with C++17, as it mentions JSON_HAS_CPP_17.
|
||||
// These tests were split off unit-json_view.cpp, whose object file got too large
|
||||
// for the MinGW linker (see json_view_test_helpers.hpp). Like that file, this one
|
||||
// is also built with C++17, as it mentions JSON_HAS_CPP_17.
|
||||
|
||||
TEST_CASE("json_view dump")
|
||||
{
|
||||
@@ -150,88 +150,6 @@ TEST_CASE("json_view dump")
|
||||
CHECK(many_doc.root().dump() == json::parse(many_tokens).dump());
|
||||
}
|
||||
|
||||
// tokens of 16 and 17 digits without an exponent (like GeoJSON
|
||||
// coordinates), which dump() converts in batches: doubles exactly
|
||||
// halfway between two candidates for the shortest digits, random
|
||||
// digits, and digits near a shorter decimal (...99999997,
|
||||
// ...00000003), whose rounding carries; in pairs and among other
|
||||
// values, so that the batches are interrupted; the last token near the
|
||||
// end of the text
|
||||
{
|
||||
std::mt19937_64 tokens(5627); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
const auto draw = [&tokens](std::size_t n)
|
||||
{
|
||||
const std::uint64_t r = tokens() % n;
|
||||
return static_cast<std::size_t>(r);
|
||||
};
|
||||
std::string coordinates = "[754570942.33984373,257769445.56835938,858106901.94921875,97944964.876953125,"
|
||||
"[0.5,1e-7],[1.5,-0.0,2.5],[1E2,12.5,0.1,3.25,4.5,5.75,6.125,7.0,8.5],[],[1.5],[1,2.5],[2.5,\"x\"],[[1.5],2.5],";
|
||||
for (int i = 0; i < 20000; ++i)
|
||||
{
|
||||
const std::size_t length = 16 + draw(2);
|
||||
std::string digits(1, static_cast<char>('1' + draw(9)));
|
||||
const std::size_t kept = draw(3) == 0 ? length : 2 + draw(length - 3);
|
||||
for (std::size_t k = 1; k < kept; ++k)
|
||||
{
|
||||
digits += static_cast<char>('0' + draw(10));
|
||||
}
|
||||
digits += std::string(length - kept, draw(2) == 0 ? '9' : '0');
|
||||
digits.back() = static_cast<char>('0' + draw(10));
|
||||
const std::size_t point = 1 + draw(9);
|
||||
const std::string token = (draw(2) == 0 ? "-" : "") + digits.substr(0, point) + "." + digits.substr(point);
|
||||
if (i != 0)
|
||||
{
|
||||
coordinates += ',';
|
||||
}
|
||||
if (draw(8) == 0)
|
||||
{
|
||||
coordinates += R"({"x":)";
|
||||
coordinates += token;
|
||||
coordinates += R"(,"s":"a"})";
|
||||
}
|
||||
else
|
||||
{
|
||||
coordinates += '[';
|
||||
coordinates += token;
|
||||
coordinates += ',';
|
||||
coordinates += token;
|
||||
coordinates += ']';
|
||||
}
|
||||
}
|
||||
coordinates += ",-65.613616999999977]";
|
||||
const json_document parsed = json_document::parse(coordinates);
|
||||
const ordered_json j = ordered_json::parse(coordinates);
|
||||
CHECK(parsed.root().dump() == j.dump());
|
||||
CHECK(parsed.root().dump(1) == j.dump(1));
|
||||
CHECK(parsed.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(parsed.root().back().get<double>() == j.back().get<double>());
|
||||
CHECK(parsed.root().back().dump() == "-65.61361699999998");
|
||||
std::size_t i = 0;
|
||||
for (const json_view v : parsed.root())
|
||||
{
|
||||
const ordered_json& e = j[i++];
|
||||
CHECK(v.dump() == e.dump());
|
||||
if (v.is_array() && !v.empty() && v[0].is_number())
|
||||
{
|
||||
CHECK(v[0].get<double>() == e[0].get<double>());
|
||||
}
|
||||
}
|
||||
|
||||
// an editable document: edits move some arrays, so that the walk
|
||||
// leaves the order of the parsed nodes
|
||||
ordered_json_editable_document editable = ordered_json_editable_document::parse(coordinates);
|
||||
ordered_json expected = j;
|
||||
for (std::size_t k = 0; k < 20000; k += 97)
|
||||
{
|
||||
if (expected[k].is_array())
|
||||
{
|
||||
editable.push_back(editable.root()[k], ordered_json(1.5));
|
||||
expected[k].push_back(1.5);
|
||||
}
|
||||
}
|
||||
CHECK(editable.root().dump() == expected.dump());
|
||||
}
|
||||
|
||||
// random doubles, written as parse() and dump() would
|
||||
std::mt19937_64 rng(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
std::string many = "[";
|
||||
@@ -253,4 +171,768 @@ TEST_CASE("json_view dump")
|
||||
const nlohmann::basic_json_document<json_float> float_document = nlohmann::basic_json_document<json_float>::parse("[0.1, 1.5e10, 3.4028235e38]");
|
||||
CHECK(float_document.root().dump() == json_float::parse("[0.1, 1.5e10, 3.4028235e38]").dump());
|
||||
}
|
||||
|
||||
SECTION("members in document order, all of them")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"b": 1, "a": 2, "b": 3})");
|
||||
CHECK(d.root().dump() == R"({"b":1,"a":2,"b":3})");
|
||||
CHECK(d.root().dump(1) == "{\n \"b\": 1,\n \"a\": 2,\n \"b\": 3\n}");
|
||||
// dump() writes all members, though the lookup finds the first
|
||||
CHECK(d.root()["b"].dump() == "1");
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||
const json_document deep_document = json_document::parse(deep);
|
||||
CHECK(deep_document.root().dump() == deep);
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii: a character that needs an escape at each position of a string")
|
||||
{
|
||||
// string lengths around the width of the vector steps, in a string value, a key, and a value near the end of the text
|
||||
for (const char* const special :
|
||||
{
|
||||
"\xC3\xA9", "\xE4\xB8\xAD", "\xF0\x9F\x98\x80", "\x7F", "\\n", "\\u0001", "\\\"", "\\\\"
|
||||
})
|
||||
{
|
||||
for (std::size_t length = 0; length <= 40; ++length)
|
||||
{
|
||||
for (std::size_t position = 0; position <= length; ++position)
|
||||
{
|
||||
std::string content(length, 'a');
|
||||
content.insert(position, special);
|
||||
const std::array<std::string, 3> texts =
|
||||
{
|
||||
{
|
||||
"\"" + content + "\"",
|
||||
"[\"" + content + "\"]",
|
||||
"{\"" + content + "\":1}",
|
||||
}
|
||||
};
|
||||
for (const std::string& text : texts)
|
||||
{
|
||||
CAPTURE(text)
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii: every character of the Basic Multilingual Plane and some beyond")
|
||||
{
|
||||
// (the two- and three-byte sequences, including the ranges that need a check: E0, ED, EF BF BF)
|
||||
std::string text = "[";
|
||||
std::size_t in_chunk = 0;
|
||||
const auto append = [&](std::uint32_t cp)
|
||||
{
|
||||
if (cp < 0x80)
|
||||
{
|
||||
if (cp < 0x20 || cp == '"' || cp == '\\')
|
||||
{
|
||||
return;
|
||||
}
|
||||
text += static_cast<char>(cp);
|
||||
}
|
||||
else if (cp < 0x800)
|
||||
{
|
||||
text += static_cast<char>(0xC0 | (cp >> 6));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else if (cp < 0x10000)
|
||||
{
|
||||
text += static_cast<char>(0xE0 | (cp >> 12));
|
||||
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else
|
||||
{
|
||||
text += static_cast<char>(0xF0 | (cp >> 18));
|
||||
text += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
||||
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
};
|
||||
for (std::uint32_t cp = 0x20; cp < 0x10400; ++cp)
|
||||
{
|
||||
if (cp >= 0xD800 && cp < 0xE000)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (in_chunk == 0)
|
||||
{
|
||||
text += text.size() > 1 ? ",\"" : "\"";
|
||||
}
|
||||
append(cp);
|
||||
if (++in_chunk == 37)
|
||||
{
|
||||
text += "\"";
|
||||
in_chunk = 0;
|
||||
}
|
||||
}
|
||||
text += in_chunk != 0 ? "\"]" : "]";
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root().dump(2) == j.dump(2));
|
||||
}
|
||||
|
||||
SECTION("deep nesting, indented")
|
||||
{
|
||||
// indentations of up to and beyond the width of the writer's fixed-size copy
|
||||
for (const int depth :
|
||||
{
|
||||
1, 5, 20, 70, 300
|
||||
})
|
||||
{
|
||||
std::string text;
|
||||
for (int i = 0; i < depth; ++i)
|
||||
{
|
||||
text += i % 2 == 0 ? "[1," : R"({"k":2,"\u00e9":)";
|
||||
}
|
||||
text += "[]";
|
||||
for (int i = depth - 1; i >= 0; --i)
|
||||
{
|
||||
text += i % 2 == 0 ? "]" : "}";
|
||||
}
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
for (const int indent :
|
||||
{
|
||||
0, 1, 2, 3, 8
|
||||
})
|
||||
{
|
||||
CHECK(d.root().dump(indent) == j.dump(indent));
|
||||
CHECK(d.root().dump(indent, '\t') == j.dump(indent, '\t'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("the output buffer of a small value is small")
|
||||
{
|
||||
// an escaped key after the value: its node lies in the arena, so the
|
||||
// source extent of the value cannot be read from the next node
|
||||
const std::string big(100000, 'a');
|
||||
const std::string text = R"({"small":1,"list":[1,2,3],"k\n":")" + big + R"("})";
|
||||
const json_document d = json_document::parse(text);
|
||||
const auto small = d.root()["small"].dump();
|
||||
CHECK(small == "1");
|
||||
CHECK(small.capacity() < 4096);
|
||||
const auto list = d.root()["list"].dump();
|
||||
CHECK(list == "[1,2,3]");
|
||||
CHECK(list.capacity() < 4096);
|
||||
CHECK(d.root()["list"].dump(2).capacity() < 4096);
|
||||
|
||||
// the whole document and the large value are unaffected
|
||||
CHECK(d.root().dump() == ordered_json::parse(text).dump());
|
||||
CHECK(d.root()["k\n"].dump() == "\"" + big + "\"");
|
||||
}
|
||||
|
||||
SECTION("output that outgrows the estimate")
|
||||
{
|
||||
// ensure_ascii writes six bytes for each two-byte character
|
||||
std::string chars;
|
||||
for (int i = 0; i < 5000; ++i)
|
||||
{
|
||||
chars += "\xC3\xA9";
|
||||
}
|
||||
const json_document d = json_document::parse(R"({"a":")" + chars + R"(","k\n":1})");
|
||||
const json expected = json::parse("\"" + chars + "\"");
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true) == expected.dump(-1, ' ', true));
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii with strings of the source, decoded strings, and edited strings")
|
||||
{
|
||||
// plain ASCII, non-ASCII, escaped (decoded), and mixed strings, as values and as keys;
|
||||
// the second text has a DEL, which ensure_ascii writes as an escape
|
||||
for (const char* const del :
|
||||
{"", "\x7F"
|
||||
})
|
||||
{
|
||||
const std::string text = std::string("{\"plain\":\"abc\",\"caf\xC3\xA9\":\"\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80\",")
|
||||
+ "\"esc\\n\\\"\":\"a\\tb\\u00e4\\ud83d\\ude00\",\"del\":\"a" + del + "b\",\"\":\"\"," // NOLINT(modernize-raw-string-literal)
|
||||
+ "\"list\":[\"x\",\"\xC3\xBC\",\"\\\\\",\"long ASCII string with more than thirty-two bytes in it\","
|
||||
+ "\"long string with more than thirty-two bytes and one non-ASCII byte \xC3\xB6\"],"
|
||||
+ "\"n\":[1,2.5,-0,true,null]}";
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root()["list"].dump(-1, ' ', true) == j["list"].dump(-1, ' ', true));
|
||||
CHECK(d.root()["del"].dump(-1, ' ', true) == j["del"].dump(-1, ' ', true));
|
||||
|
||||
// strings that an edit wrote (the source strings that they replace were plain)
|
||||
ordered_json_editable_document e = ordered_json_editable_document::parse(text);
|
||||
ordered_json k = j;
|
||||
e.set(e.root()["plain"], std::string("\xC3\xA4gypten"));
|
||||
k["plain"] = "\xC3\xA4gypten";
|
||||
e.set(e.root()["list"][0], std::string("ascii after edit"));
|
||||
k["list"][0] = "ascii after edit";
|
||||
e.set(e.root()["list"][1], std::string("\xC3\xA4"));
|
||||
k["list"][1] = "\xC3\xA4";
|
||||
e.set(e.root()["del"], std::string("tab\there"));
|
||||
k["del"] = "tab\there";
|
||||
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
||||
CHECK(e.root().dump(2, ' ', true) == k.dump(2, ' ', true));
|
||||
CHECK(e.root().dump() == k.dump());
|
||||
|
||||
// strings that were edited from or to ASCII text
|
||||
e.set(e.root()["list"][2], std::string("\xC3\xA4"));
|
||||
k["list"][2] = "\xC3\xA4";
|
||||
e.set(e.root()["list"][4], std::string("plain again"));
|
||||
k["list"][4] = "plain again";
|
||||
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii with DEL or a non-ASCII character at every position")
|
||||
{
|
||||
// the check whether a string of the source has no byte >= 0x7F reads
|
||||
// eight bytes at a time and then the rest one by one
|
||||
for (std::size_t length = 1; length <= 20; ++length)
|
||||
{
|
||||
for (std::size_t pos = 0; pos < length; ++pos)
|
||||
{
|
||||
for (const char* const c :
|
||||
{
|
||||
"\x7F", "\x7E", "\xC3\xA4", "\xC3\xBF" // NOLINT(modernize-raw-string-literal)
|
||||
})
|
||||
{
|
||||
const std::string s = std::string(pos, 'a') + c + std::string(length - pos - 1, 'b');
|
||||
std::string text = "[\"";
|
||||
text.append(s).append("\",{\"").append(s).append("\":1}]");
|
||||
const json_document d = json_document::parse(text);
|
||||
const json j = json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(1, ' ', true) == j.dump(1, ' ', true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("streams and discarded views")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"a": [1, 2]})");
|
||||
std::ostringstream compact;
|
||||
compact << d.root();
|
||||
CHECK(compact.str() == R"({"a":[1,2]})");
|
||||
std::ostringstream pretty;
|
||||
pretty << std::setw(2) << std::setfill('.') << d.root() << d.root()["a"];
|
||||
CHECK(pretty.str() == "{\n..\"a\": [\n....1,\n....2\n..]\n}[1,2]");
|
||||
CHECK(json_view().dump() == json(json::value_t::discarded).dump());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view comparison")
|
||||
{
|
||||
SECTION("equality of the values parse() produces")
|
||||
{
|
||||
generator g;
|
||||
std::vector<std::string> texts;
|
||||
for (int i = 0; i < 600; ++i)
|
||||
{
|
||||
std::string text;
|
||||
g.value(text, 0);
|
||||
texts.push_back(text);
|
||||
// the same value written differently: sorted keys, canonical numbers
|
||||
texts.push_back(json::parse(text).dump(1));
|
||||
}
|
||||
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
|
||||
{
|
||||
for (std::size_t k = i; k < i + 3; ++k)
|
||||
{
|
||||
CAPTURE(texts[i])
|
||||
CAPTURE(texts[k])
|
||||
const json_document a = json_document::parse(texts[i]);
|
||||
const json_document b = json_document::parse(texts[k]);
|
||||
const json ja = json::parse(texts[i]);
|
||||
const json jb = json::parse(texts[k]);
|
||||
CHECK((a.root() == b.root()) == (ja == jb));
|
||||
CHECK((a.root() != b.root()) == (ja != jb));
|
||||
CHECK((a.root() == jb) == (ja == jb));
|
||||
CHECK((jb == a.root()) == (ja == jb));
|
||||
CHECK((a.root() != jb) == (ja != jb));
|
||||
CHECK((jb != a.root()) == (ja != jb));
|
||||
|
||||
// ordered_json compares members in order
|
||||
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
|
||||
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
|
||||
const ordered_json oja = ordered_json::parse(texts[i]);
|
||||
const ordered_json ojb = ordered_json::parse(texts[k]);
|
||||
CHECK((oa.root() == ob.root()) == (oja == ojb));
|
||||
CHECK((oa.root() == ojb) == (oja == ojb));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("numbers, duplicate keys, member order")
|
||||
{
|
||||
const auto same = [](const char* x, const char* y)
|
||||
{
|
||||
const json_document dx = json_document::parse(x);
|
||||
const json_document dy = json_document::parse(y);
|
||||
return dx.root() == dy.root();
|
||||
};
|
||||
CHECK(same("1", "1.0"));
|
||||
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
|
||||
CHECK(!same("1", "1.5"));
|
||||
CHECK(same("18446744073709551615", "18446744073709551615"));
|
||||
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
|
||||
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
|
||||
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
|
||||
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
|
||||
CHECK(!same("[1, 2]", "[2, 1]"));
|
||||
CHECK(!same("\"a\"", "\"b\""));
|
||||
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
|
||||
CHECK(!same("null", "false"));
|
||||
CHECK(!same("[]", "{}"));
|
||||
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
||||
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
|
||||
CHECK(dup.root() == last.root());
|
||||
// (== compares as parse() resolves duplicates, a lookup finds the first)
|
||||
CHECK(dup.root()["a"].materialize() == 1);
|
||||
CHECK(dup.root()["a"] != last.root()["a"]);
|
||||
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
|
||||
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
|
||||
CHECK(ab.root() != ba.root());
|
||||
|
||||
// discarded values compare as basic_json's do
|
||||
const json discarded(json::value_t::discarded);
|
||||
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
|
||||
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
|
||||
const json_document null_document = json_document::parse("null");
|
||||
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
|
||||
CHECK(!(null_document.root() == discarded));
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||
const json_document a = json_document::parse(deep);
|
||||
const json_document b = json_document::parse(deep);
|
||||
CHECK(a.root() == b.root());
|
||||
CHECK(a.root() == json::parse(deep));
|
||||
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
|
||||
const json_document c = json_document::parse(other);
|
||||
CHECK(a.root() != c.root());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view large objects")
|
||||
{
|
||||
// objects with 128 members or more are looked up with a hash index
|
||||
for (const std::size_t members :
|
||||
{
|
||||
127u, 128u, 129u, 10000u
|
||||
})
|
||||
{
|
||||
CAPTURE(members)
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\\n" : "") + "\":" + std::to_string(i);
|
||||
}
|
||||
text += R"(,"":"empty key","k1":"a duplicate of an earlier key"})";
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
const json j = json::parse(text);
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
const std::string key = std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\n" : "");
|
||||
CHECK(v[key].get<std::size_t>() == i);
|
||||
CHECK(v.contains(key));
|
||||
CHECK(v.find(key).key() == key);
|
||||
CHECK(v.at(key).get<std::size_t>() == i);
|
||||
CHECK(!v.contains(key + "x"));
|
||||
}
|
||||
CHECK(v[""].get_string() == "empty key");
|
||||
CHECK(v["k1"].get<int>() == 1); // the first of duplicate keys, as for small objects
|
||||
CHECK(!v.contains("missing"));
|
||||
CHECK_THROWS_WITH_AS(v.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
|
||||
CHECK(v == j);
|
||||
CHECK(v.materialize() == j);
|
||||
}
|
||||
|
||||
SECTION("duplicate keys: the first member wins, with and without a table")
|
||||
{
|
||||
// an object of `total` members: the keys "k0".."k<n-1>" in order, then
|
||||
// three keys repeated twice more (one copy in the middle, one at the
|
||||
// end), and two keys repeated once; the value of a member is its
|
||||
// position, so that the first member of a key can be told apart
|
||||
struct member
|
||||
{
|
||||
std::string key;
|
||||
std::size_t position;
|
||||
};
|
||||
const auto make_members = [](std::size_t total)
|
||||
{
|
||||
std::vector<std::string> keys;
|
||||
for (std::size_t i = 0; i + 8 < total; ++i)
|
||||
{
|
||||
keys.push_back("k" + std::to_string(i));
|
||||
}
|
||||
const std::size_t n = keys.size();
|
||||
const std::array<std::size_t, 3> triple = {{3, 17, n - 1}};
|
||||
const std::array<std::size_t, 2> twice = {{5, n / 2}};
|
||||
std::vector<std::string> ordered = keys;
|
||||
for (const std::size_t i : triple)
|
||||
{
|
||||
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 2), keys[i]);
|
||||
}
|
||||
for (const std::size_t i : twice)
|
||||
{
|
||||
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 3), keys[i]);
|
||||
}
|
||||
for (const std::size_t i : triple)
|
||||
{
|
||||
ordered.push_back(keys[i]);
|
||||
}
|
||||
std::vector<member> result;
|
||||
result.reserve(ordered.size());
|
||||
for (std::size_t i = 0; i < ordered.size(); ++i)
|
||||
{
|
||||
result.push_back({ordered[i], i});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// 100 and 127 members: no table; 128 members and more: a table
|
||||
for (const std::size_t total :
|
||||
{
|
||||
100u, 127u, 128u, 200u, 5000u
|
||||
})
|
||||
{
|
||||
CAPTURE(total)
|
||||
const std::vector<member> members = make_members(total);
|
||||
REQUIRE(members.size() >= total);
|
||||
REQUIRE(members.size() >= 100);
|
||||
std::string text = "{";
|
||||
std::map<std::string, std::size_t> first;
|
||||
std::map<std::string, std::size_t> last;
|
||||
std::map<std::string, std::size_t> occurrences;
|
||||
for (const member& m : members)
|
||||
{
|
||||
text += (text.size() > 1 ? ",\"" : "\"") + m.key + "\":" + std::to_string(m.position);
|
||||
first.insert({m.key, m.position});
|
||||
last[m.key] = m.position;
|
||||
++occurrences[m.key];
|
||||
}
|
||||
text += '}';
|
||||
REQUIRE(first.size() < members.size());
|
||||
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
const json j = json::parse(text);
|
||||
CHECK(v.size() == members.size()); // every occurrence is visited
|
||||
const json m = v.materialize();
|
||||
for (const auto& entry : first)
|
||||
{
|
||||
CAPTURE(entry.first)
|
||||
const std::size_t expected = entry.second;
|
||||
CHECK(v[entry.first].get<std::size_t>() == expected);
|
||||
CHECK(v.at(entry.first).get<std::size_t>() == expected);
|
||||
CHECK(v.find(entry.first).value().get<std::size_t>() == expected);
|
||||
CHECK(v.value(entry.first, std::size_t{0}) == expected);
|
||||
CHECK(v.contains(entry.first));
|
||||
CHECK(v.count(entry.first) == occurrences[entry.first]);
|
||||
const std::string pointer = "/" + entry.first;
|
||||
CHECK(v[json::json_pointer(pointer)].get<std::size_t>() == expected);
|
||||
CHECK(v.at(json::json_pointer(pointer)).get<std::size_t>() == expected);
|
||||
CHECK(v.value(json::json_pointer(pointer), std::size_t{0}) == expected);
|
||||
CHECK(v.contains(json::json_pointer(pointer)));
|
||||
// materialize() and parse() keep the last value instead
|
||||
CHECK(j[entry.first].get<std::size_t>() == last.at(entry.first));
|
||||
CHECK(m[entry.first].get<std::size_t>() == last.at(entry.first));
|
||||
}
|
||||
CHECK(!v.contains("k"));
|
||||
CHECK(v["missing"].is_discarded());
|
||||
CHECK(v.materialize() == j);
|
||||
CHECK(v == j);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("colliding keys")
|
||||
{
|
||||
// the hash is not seeded, so keys that all land in one place must not
|
||||
// make the table build quadratic: such an object gets no table and is
|
||||
// searched linearly
|
||||
constexpr std::size_t members = 300;
|
||||
constexpr std::size_t table_size = 1024; // the table size for 300 members: the next power of two >= 600
|
||||
std::vector<std::string> colliding;
|
||||
std::vector<std::string> spread;
|
||||
for (std::uint64_t counter = 0; colliding.size() < members || spread.size() < members; ++counter)
|
||||
{
|
||||
std::string key(8, 'a');
|
||||
std::uint64_t x = counter;
|
||||
for (std::size_t i = 0; i < 8; ++i, x /= 26)
|
||||
{
|
||||
key[i] = static_cast<char>('a' + (x % 26));
|
||||
}
|
||||
const bool lands_in_slot_zero = (nlohmann::detail::view::key_hash(key.data(), key.size()) & (table_size - 1)) == 0;
|
||||
if (lands_in_slot_zero && colliding.size() < members)
|
||||
{
|
||||
colliding.push_back(key);
|
||||
}
|
||||
else if (!lands_in_slot_zero && spread.size() < members)
|
||||
{
|
||||
spread.push_back(key);
|
||||
}
|
||||
}
|
||||
|
||||
const auto make_text = [](const std::vector<std::string>& keys)
|
||||
{
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i % 10);
|
||||
}
|
||||
return text + "}";
|
||||
};
|
||||
const std::string colliding_text = make_text(colliding);
|
||||
const std::string spread_text = make_text(spread);
|
||||
|
||||
json_document with_collisions = json_document::parse(colliding_text);
|
||||
json_document without_collisions = json_document::parse(spread_text);
|
||||
for (const auto* pair :
|
||||
{
|
||||
&colliding, &spread
|
||||
})
|
||||
{
|
||||
const json_view v = (pair == &colliding ? with_collisions : without_collisions).root();
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CHECK(v[(*pair)[i]].get<std::size_t>() == i % 10);
|
||||
CHECK(v.at((*pair)[i]).get<std::size_t>() == i % 10);
|
||||
CHECK(v.find((*pair)[i]).key() == (*pair)[i]);
|
||||
CHECK(!v.contains((*pair)[i] + "x"));
|
||||
}
|
||||
CHECK(!v.contains("missing"));
|
||||
}
|
||||
CHECK(with_collisions.root() == json::parse(colliding_text));
|
||||
|
||||
// only the object with the spread keys got a table (both texts have the
|
||||
// same length, so the tables are the only difference)
|
||||
with_collisions.shrink_to_fit();
|
||||
without_collisions.shrink_to_fit();
|
||||
CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (table_size * sizeof(std::uint32_t)));
|
||||
}
|
||||
|
||||
SECTION("keys that differ in their last bytes are spread over the table")
|
||||
{
|
||||
// The hash must spread the usual families of keys (numbered names, hex
|
||||
// strings, dates, short names): a key farther than index_max_displacement
|
||||
// from its home slot would leave the object without a table. The keys
|
||||
// are placed with linear probing, as the table does.
|
||||
const auto max_displacement = [](const std::vector<std::string>& keys)
|
||||
{
|
||||
std::size_t slots = 16;
|
||||
while (slots < 2 * keys.size())
|
||||
{
|
||||
slots *= 2;
|
||||
}
|
||||
std::vector<bool> used(slots, false);
|
||||
std::size_t farthest = 0;
|
||||
for (const std::string& key : keys)
|
||||
{
|
||||
std::size_t i = nlohmann::detail::conditional_static_cast<std::size_t>(nlohmann::detail::view::key_hash(key.data(), key.size())) & (slots - 1);
|
||||
std::size_t distance = 0;
|
||||
while (used[i])
|
||||
{
|
||||
i = (i + 1) & (slots - 1);
|
||||
++distance;
|
||||
}
|
||||
used[i] = true;
|
||||
farthest = (std::max)(farthest, distance);
|
||||
}
|
||||
return farthest;
|
||||
};
|
||||
// the keys of a printf-like pattern with one %d, %0Nd or %0Nx, formatted with a stream
|
||||
// (a format string that is not a literal is a Flawfinder error)
|
||||
const auto numbered = [](const char* format, int count, int step)
|
||||
{
|
||||
const std::string pattern(format);
|
||||
const std::size_t percent = pattern.find('%');
|
||||
std::size_t conversion = percent + 1;
|
||||
std::size_t width = 0;
|
||||
while (pattern[conversion] >= '0' && pattern[conversion] <= '9')
|
||||
{
|
||||
width = (width * 10) + static_cast<std::size_t>(pattern[conversion] - '0');
|
||||
++conversion;
|
||||
}
|
||||
const bool hex = pattern[conversion] == 'x';
|
||||
std::vector<std::string> keys;
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
std::ostringstream key;
|
||||
key << pattern.substr(0, percent) << std::setfill('0') << std::setw(static_cast<int>(width))
|
||||
<< (hex ? std::hex : std::dec) << (i * step) << pattern.substr(conversion + 1);
|
||||
keys.push_back(key.str());
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
for (const char* format :
|
||||
{"key%d", "%d", "item_%04d", "a%05d", "%03dxyz", "id%d", "user_%d", "%d_suffix", "0x%04x", "A%07d", "12345678%d", "1234567%d", "123456789%d",
|
||||
"configuration_parameter_value_%04d_extra", "2026-01-%02dT00:00:00Z"
|
||||
})
|
||||
{
|
||||
CAPTURE(format)
|
||||
CHECK(max_displacement(numbered(format, std::string(format).find("%02d") != std::string::npos ? 60 : 1000, 1)) < nlohmann::detail::view::index_max_displacement); // NOLINT(abseil-string-find-str-contains)
|
||||
CHECK(max_displacement(numbered(format, 600, 7)) < nlohmann::detail::view::index_max_displacement);
|
||||
}
|
||||
std::mt19937_64 rng(7); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
for (const std::size_t length :
|
||||
{
|
||||
std::size_t{1}, std::size_t{2}, std::size_t{3}, std::size_t{5}, std::size_t{7}, std::size_t{8}, std::size_t{9}, std::size_t{12}, std::size_t{16}, std::size_t{17}, std::size_t{33}
|
||||
})
|
||||
{
|
||||
CAPTURE(length)
|
||||
std::set<std::string> distinct;
|
||||
// (there are 26 and 676 keys of one and two letters)
|
||||
std::size_t wanted = 700;
|
||||
if (length == 1)
|
||||
{
|
||||
wanted = 26;
|
||||
}
|
||||
else if (length == 2)
|
||||
{
|
||||
wanted = 500;
|
||||
}
|
||||
while (distinct.size() < wanted)
|
||||
{
|
||||
std::string key(length, 'a');
|
||||
for (char& c : key)
|
||||
{
|
||||
c = static_cast<char>('a' + (rng() % 26));
|
||||
}
|
||||
distinct.insert(key);
|
||||
}
|
||||
CHECK(max_displacement(std::vector<std::string>(distinct.begin(), distinct.end())) < nlohmann::detail::view::index_max_displacement);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("keys of every length that differ in one byte")
|
||||
{
|
||||
// In a large object, a key is found by comparing its bytes with those of the member the
|
||||
// table points to: up to 16 bytes as two overlapping words, longer ones with memcmp.
|
||||
// Keys of every length from 0 to 40, and keys that differ from them in one byte.
|
||||
std::mt19937_64 rng(2026); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
std::vector<std::string> keys;
|
||||
std::set<std::string> distinct;
|
||||
for (std::size_t length = 0; length <= 40; ++length)
|
||||
{
|
||||
for (int copy = 0; copy < 5; ++copy)
|
||||
{
|
||||
std::string key(length, 'a');
|
||||
for (char& c : key)
|
||||
{
|
||||
c = static_cast<char>('a' + (rng() % 26));
|
||||
}
|
||||
if (distinct.insert(key).second)
|
||||
{
|
||||
keys.push_back(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i);
|
||||
}
|
||||
text += "}";
|
||||
REQUIRE(keys.size() >= 128); // (a table is built for 128 members)
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
CAPTURE(keys[i])
|
||||
CHECK(v[keys[i]].get<std::size_t>() == i);
|
||||
CHECK(v.at(keys[i]).get<std::size_t>() == i);
|
||||
CHECK(v.contains(keys[i]));
|
||||
for (std::size_t at = 0; at < keys[i].size(); ++at)
|
||||
{
|
||||
std::string other = keys[i];
|
||||
other[at] = other[at] == 'z' ? 'y' : 'z';
|
||||
CAPTURE(other)
|
||||
CHECK(v.contains(other) == (distinct.count(other) != 0));
|
||||
}
|
||||
CHECK(v.contains(keys[i] + "a") == (distinct.count(keys[i] + "a") != 0));
|
||||
if (!keys[i].empty())
|
||||
{
|
||||
CHECK(v.contains(keys[i].substr(1)) == (distinct.count(keys[i].substr(1)) != 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("shrink_to_fit releases the tables' spare capacity")
|
||||
{
|
||||
const auto make_text = [](int objects, int members)
|
||||
{
|
||||
std::string text = "[";
|
||||
for (int object = 0; object < objects; ++object)
|
||||
{
|
||||
text += object != 0 ? ",{" : "{";
|
||||
for (int i = 0, n = members + object; i < n; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
text += '}';
|
||||
}
|
||||
return text + "]";
|
||||
};
|
||||
const std::string small_text = make_text(5, 150);
|
||||
const std::string big_text = make_text(40, 400);
|
||||
|
||||
// reading a big text, and then a small one, leaves the spare capacity
|
||||
// of the big one: shrink_to_fit() brings the document to the size of
|
||||
// one parsed from the small text alone
|
||||
json_document d = json_document::parse(big_text);
|
||||
const std::size_t big = d.memory_usage();
|
||||
d.read(small_text);
|
||||
CHECK(d.memory_usage() >= big);
|
||||
d.shrink_to_fit();
|
||||
json_document fresh = json_document::parse(small_text);
|
||||
fresh.shrink_to_fit();
|
||||
CHECK(d.memory_usage() == fresh.memory_usage());
|
||||
CHECK(d.memory_usage() < big / 2);
|
||||
CHECK(d.root() == json::parse(small_text));
|
||||
for (int object = 0; object < 5; ++object)
|
||||
{
|
||||
const json_view v = d.root()[static_cast<std::size_t>(object)];
|
||||
for (int i = 0, n = 150 + object; i < n; ++i)
|
||||
{
|
||||
CHECK(v[std::to_string(i)].get<int>() == i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("nested, reused, and in arrays")
|
||||
{
|
||||
std::string inner = "{";
|
||||
for (int i = 0; i < 300; ++i)
|
||||
{
|
||||
inner += (i != 0 ? ",\"m" : "\"m") + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
inner += '}';
|
||||
const std::string text = "[" + inner + ",{\"x\":" + inner + "}," + inner + "]";
|
||||
json_document d = json_document::parse(text);
|
||||
CHECK(d.root()[0]["m299"].get<int>() == 299);
|
||||
CHECK(d.root()[1]["x"]["m150"].get<int>() == 150);
|
||||
CHECK(d.root()[2]["m0"].get<int>() == 0);
|
||||
const std::size_t with_index = d.memory_usage();
|
||||
d.read(std::string("{\"small\": 1}"));
|
||||
CHECK(d.root()["small"].get<int>() == 1);
|
||||
d.read(text);
|
||||
CHECK(d.root()[2]["m7"].get<int>() == 7);
|
||||
CHECK(d.memory_usage() >= with_index / 2);
|
||||
}
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json_view.hpp>
|
||||
using nlohmann::json;
|
||||
using nlohmann::ordered_json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::ordered_json_editable_document;
|
||||
using nlohmann::json_view;
|
||||
using nlohmann::ordered_json_document;
|
||||
using nlohmann::ordered_json_view;
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// These tests were split off unit-json_view_dump.cpp, whose object file got too large for the MinGW
|
||||
// linker (see json_view_test_helpers.hpp). Like that file, this one is also built
|
||||
// with C++17, as it mentions JSON_HAS_CPP_17.
|
||||
|
||||
TEST_CASE("json_view dump: escaping, nesting, and output buffers")
|
||||
{
|
||||
SECTION("members in document order, all of them")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"b": 1, "a": 2, "b": 3})");
|
||||
CHECK(d.root().dump() == R"({"b":1,"a":2,"b":3})");
|
||||
CHECK(d.root().dump(1) == "{\n \"b\": 1,\n \"a\": 2,\n \"b\": 3\n}");
|
||||
// dump() writes all members, though the lookup finds the first
|
||||
CHECK(d.root()["b"].dump() == "1");
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||
const json_document deep_document = json_document::parse(deep);
|
||||
CHECK(deep_document.root().dump() == deep);
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii: a character that needs an escape at each position of a string")
|
||||
{
|
||||
// string lengths around the width of the vector steps, in a string value, a key, and a value near the end of the text
|
||||
for (const char* const special :
|
||||
{
|
||||
"\xC3\xA9", "\xE4\xB8\xAD", "\xF0\x9F\x98\x80", "\x7F", "\\n", "\\u0001", "\\\"", "\\\\"
|
||||
})
|
||||
{
|
||||
for (std::size_t length = 0; length <= 40; ++length)
|
||||
{
|
||||
for (std::size_t position = 0; position <= length; ++position)
|
||||
{
|
||||
std::string content(length, 'a');
|
||||
content.insert(position, special);
|
||||
const std::array<std::string, 3> texts =
|
||||
{
|
||||
{
|
||||
"\"" + content + "\"",
|
||||
"[\"" + content + "\"]",
|
||||
"{\"" + content + "\":1}",
|
||||
}
|
||||
};
|
||||
for (const std::string& text : texts)
|
||||
{
|
||||
CAPTURE(text)
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii: every character of the Basic Multilingual Plane and some beyond")
|
||||
{
|
||||
// (the two- and three-byte sequences, including the ranges that need a check: E0, ED, EF BF BF)
|
||||
std::string text = "[";
|
||||
std::size_t in_chunk = 0;
|
||||
const auto append = [&](std::uint32_t cp)
|
||||
{
|
||||
if (cp < 0x80)
|
||||
{
|
||||
if (cp < 0x20 || cp == '"' || cp == '\\')
|
||||
{
|
||||
return;
|
||||
}
|
||||
text += static_cast<char>(cp);
|
||||
}
|
||||
else if (cp < 0x800)
|
||||
{
|
||||
text += static_cast<char>(0xC0 | (cp >> 6));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else if (cp < 0x10000)
|
||||
{
|
||||
text += static_cast<char>(0xE0 | (cp >> 12));
|
||||
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else
|
||||
{
|
||||
text += static_cast<char>(0xF0 | (cp >> 18));
|
||||
text += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
||||
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
};
|
||||
for (std::uint32_t cp = 0x20; cp < 0x10400; ++cp)
|
||||
{
|
||||
if (cp >= 0xD800 && cp < 0xE000)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (in_chunk == 0)
|
||||
{
|
||||
text += text.size() > 1 ? ",\"" : "\"";
|
||||
}
|
||||
append(cp);
|
||||
if (++in_chunk == 37)
|
||||
{
|
||||
text += "\"";
|
||||
in_chunk = 0;
|
||||
}
|
||||
}
|
||||
text += in_chunk != 0 ? "\"]" : "]";
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root().dump(2) == j.dump(2));
|
||||
}
|
||||
|
||||
SECTION("deep nesting, indented")
|
||||
{
|
||||
// indentations of up to and beyond the width of the writer's fixed-size copy
|
||||
for (const int depth :
|
||||
{
|
||||
1, 5, 20, 70, 300
|
||||
})
|
||||
{
|
||||
std::string text;
|
||||
for (int i = 0; i < depth; ++i)
|
||||
{
|
||||
text += i % 2 == 0 ? "[1," : R"({"k":2,"\u00e9":)";
|
||||
}
|
||||
text += "[]";
|
||||
for (int i = depth - 1; i >= 0; --i)
|
||||
{
|
||||
text += i % 2 == 0 ? "]" : "}";
|
||||
}
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
for (const int indent :
|
||||
{
|
||||
0, 1, 2, 3, 8
|
||||
})
|
||||
{
|
||||
CHECK(d.root().dump(indent) == j.dump(indent));
|
||||
CHECK(d.root().dump(indent, '\t') == j.dump(indent, '\t'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("the output buffer of a small value is small")
|
||||
{
|
||||
// an escaped key after the value: its node lies in the arena, so the
|
||||
// source extent of the value cannot be read from the next node
|
||||
const std::string big(100000, 'a');
|
||||
const std::string text = R"({"small":1,"list":[1,2,3],"k\n":")" + big + R"("})";
|
||||
const json_document d = json_document::parse(text);
|
||||
const auto small = d.root()["small"].dump();
|
||||
CHECK(small == "1");
|
||||
CHECK(small.capacity() < 4096);
|
||||
const auto list = d.root()["list"].dump();
|
||||
CHECK(list == "[1,2,3]");
|
||||
CHECK(list.capacity() < 4096);
|
||||
CHECK(d.root()["list"].dump(2).capacity() < 4096);
|
||||
|
||||
// the whole document and the large value are unaffected
|
||||
CHECK(d.root().dump() == ordered_json::parse(text).dump());
|
||||
CHECK(d.root()["k\n"].dump() == "\"" + big + "\"");
|
||||
}
|
||||
|
||||
SECTION("output that outgrows the estimate")
|
||||
{
|
||||
// ensure_ascii writes six bytes for each two-byte character
|
||||
std::string chars;
|
||||
for (int i = 0; i < 5000; ++i)
|
||||
{
|
||||
chars += "\xC3\xA9";
|
||||
}
|
||||
const json_document d = json_document::parse(R"({"a":")" + chars + R"(","k\n":1})");
|
||||
const json expected = json::parse("\"" + chars + "\"");
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true) == expected.dump(-1, ' ', true));
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii with strings of the source, decoded strings, and edited strings")
|
||||
{
|
||||
// plain ASCII, non-ASCII, escaped (decoded), and mixed strings, as values and as keys;
|
||||
// the second text has a DEL, which ensure_ascii writes as an escape
|
||||
for (const char* const del :
|
||||
{"", "\x7F"
|
||||
})
|
||||
{
|
||||
const std::string text = std::string("{\"plain\":\"abc\",\"caf\xC3\xA9\":\"\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80\",")
|
||||
+ "\"esc\\n\\\"\":\"a\\tb\\u00e4\\ud83d\\ude00\",\"del\":\"a" + del + "b\",\"\":\"\"," // NOLINT(modernize-raw-string-literal)
|
||||
+ "\"list\":[\"x\",\"\xC3\xBC\",\"\\\\\",\"long ASCII string with more than thirty-two bytes in it\","
|
||||
+ "\"long string with more than thirty-two bytes and one non-ASCII byte \xC3\xB6\"],"
|
||||
+ "\"n\":[1,2.5,-0,true,null]}";
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root()["list"].dump(-1, ' ', true) == j["list"].dump(-1, ' ', true));
|
||||
CHECK(d.root()["del"].dump(-1, ' ', true) == j["del"].dump(-1, ' ', true));
|
||||
|
||||
// strings that an edit wrote (the source strings that they replace were plain)
|
||||
ordered_json_editable_document e = ordered_json_editable_document::parse(text);
|
||||
ordered_json k = j;
|
||||
e.set(e.root()["plain"], std::string("\xC3\xA4gypten"));
|
||||
k["plain"] = "\xC3\xA4gypten";
|
||||
e.set(e.root()["list"][0], std::string("ascii after edit"));
|
||||
k["list"][0] = "ascii after edit";
|
||||
e.set(e.root()["list"][1], std::string("\xC3\xA4"));
|
||||
k["list"][1] = "\xC3\xA4";
|
||||
e.set(e.root()["del"], std::string("tab\there"));
|
||||
k["del"] = "tab\there";
|
||||
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
||||
CHECK(e.root().dump(2, ' ', true) == k.dump(2, ' ', true));
|
||||
CHECK(e.root().dump() == k.dump());
|
||||
|
||||
// strings that were edited from or to ASCII text
|
||||
e.set(e.root()["list"][2], std::string("\xC3\xA4"));
|
||||
k["list"][2] = "\xC3\xA4";
|
||||
e.set(e.root()["list"][4], std::string("plain again"));
|
||||
k["list"][4] = "plain again";
|
||||
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii with DEL or a non-ASCII character at every position")
|
||||
{
|
||||
// the check whether a string of the source has no byte >= 0x7F reads
|
||||
// eight bytes at a time and then the rest one by one
|
||||
for (std::size_t length = 1; length <= 20; ++length)
|
||||
{
|
||||
for (std::size_t pos = 0; pos < length; ++pos)
|
||||
{
|
||||
for (const char* const c :
|
||||
{
|
||||
"\x7F", "\x7E", "\xC3\xA4", "\xC3\xBF" // NOLINT(modernize-raw-string-literal)
|
||||
})
|
||||
{
|
||||
const std::string s = std::string(pos, 'a') + c + std::string(length - pos - 1, 'b');
|
||||
std::string text = "[\"";
|
||||
text.append(s).append("\",{\"").append(s).append("\":1}]");
|
||||
const json_document d = json_document::parse(text);
|
||||
const json j = json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(1, ' ', true) == j.dump(1, ' ', true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("streams and discarded views")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"a": [1, 2]})");
|
||||
std::ostringstream compact;
|
||||
compact << d.root();
|
||||
CHECK(compact.str() == R"({"a":[1,2]})");
|
||||
std::ostringstream pretty;
|
||||
pretty << std::setw(2) << std::setfill('.') << d.root() << d.root()["a"];
|
||||
CHECK(pretty.str() == "{\n..\"a\": [\n....1,\n....2\n..]\n}[1,2]");
|
||||
CHECK(json_view().dump() == json(json::value_t::discarded).dump());
|
||||
}
|
||||
}
|
||||
@@ -1,544 +0,0 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json_view.hpp>
|
||||
using nlohmann::json;
|
||||
using nlohmann::ordered_json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::ordered_json_editable_document;
|
||||
using nlohmann::json_view;
|
||||
using nlohmann::ordered_json_document;
|
||||
using nlohmann::ordered_json_view;
|
||||
|
||||
#include "json_view_test_helpers.hpp"
|
||||
using json_view_test::generator;
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// These tests were split off unit-json_view_dump.cpp, whose object file got too large for the MinGW
|
||||
// linker (see json_view_test_helpers.hpp). Like that file, this one is also built
|
||||
// with C++17, as it mentions JSON_HAS_CPP_17.
|
||||
|
||||
TEST_CASE("json_view comparison")
|
||||
{
|
||||
SECTION("equality of the values parse() produces")
|
||||
{
|
||||
generator g;
|
||||
std::vector<std::string> texts;
|
||||
for (int i = 0; i < 600; ++i)
|
||||
{
|
||||
std::string text;
|
||||
g.value(text, 0);
|
||||
texts.push_back(text);
|
||||
// the same value written differently: sorted keys, canonical numbers
|
||||
texts.push_back(json::parse(text).dump(1));
|
||||
}
|
||||
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
|
||||
{
|
||||
for (std::size_t k = i; k < i + 3; ++k)
|
||||
{
|
||||
CAPTURE(texts[i])
|
||||
CAPTURE(texts[k])
|
||||
const json_document a = json_document::parse(texts[i]);
|
||||
const json_document b = json_document::parse(texts[k]);
|
||||
const json ja = json::parse(texts[i]);
|
||||
const json jb = json::parse(texts[k]);
|
||||
CHECK((a.root() == b.root()) == (ja == jb));
|
||||
CHECK((a.root() != b.root()) == (ja != jb));
|
||||
CHECK((a.root() == jb) == (ja == jb));
|
||||
CHECK((jb == a.root()) == (ja == jb));
|
||||
CHECK((a.root() != jb) == (ja != jb));
|
||||
CHECK((jb != a.root()) == (ja != jb));
|
||||
|
||||
// ordered_json compares members in order
|
||||
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
|
||||
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
|
||||
const ordered_json oja = ordered_json::parse(texts[i]);
|
||||
const ordered_json ojb = ordered_json::parse(texts[k]);
|
||||
CHECK((oa.root() == ob.root()) == (oja == ojb));
|
||||
CHECK((oa.root() == ojb) == (oja == ojb));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("numbers, duplicate keys, member order")
|
||||
{
|
||||
const auto same = [](const char* x, const char* y)
|
||||
{
|
||||
const json_document dx = json_document::parse(x);
|
||||
const json_document dy = json_document::parse(y);
|
||||
return dx.root() == dy.root();
|
||||
};
|
||||
CHECK(same("1", "1.0"));
|
||||
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
|
||||
CHECK(!same("1", "1.5"));
|
||||
CHECK(same("18446744073709551615", "18446744073709551615"));
|
||||
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
|
||||
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
|
||||
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
|
||||
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
|
||||
CHECK(!same("[1, 2]", "[2, 1]"));
|
||||
CHECK(!same("\"a\"", "\"b\""));
|
||||
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
|
||||
CHECK(!same("null", "false"));
|
||||
CHECK(!same("[]", "{}"));
|
||||
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
||||
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
|
||||
CHECK(dup.root() == last.root());
|
||||
// (== compares as parse() resolves duplicates, a lookup finds the first)
|
||||
CHECK(dup.root()["a"].materialize() == 1);
|
||||
CHECK(dup.root()["a"] != last.root()["a"]);
|
||||
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
|
||||
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
|
||||
CHECK(ab.root() != ba.root());
|
||||
|
||||
// discarded values compare as basic_json's do
|
||||
const json discarded(json::value_t::discarded);
|
||||
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
|
||||
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
|
||||
const json_document null_document = json_document::parse("null");
|
||||
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
|
||||
CHECK(!(null_document.root() == discarded));
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||
const json_document a = json_document::parse(deep);
|
||||
const json_document b = json_document::parse(deep);
|
||||
CHECK(a.root() == b.root());
|
||||
CHECK(a.root() == json::parse(deep));
|
||||
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
|
||||
const json_document c = json_document::parse(other);
|
||||
CHECK(a.root() != c.root());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view large objects")
|
||||
{
|
||||
// objects with 128 members or more are looked up with a hash index
|
||||
for (const std::size_t members :
|
||||
{
|
||||
127u, 128u, 129u, 10000u
|
||||
})
|
||||
{
|
||||
CAPTURE(members)
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\\n" : "") + "\":" + std::to_string(i);
|
||||
}
|
||||
text += R"(,"":"empty key","k1":"a duplicate of an earlier key"})";
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
const json j = json::parse(text);
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
const std::string key = std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\n" : "");
|
||||
CHECK(v[key].get<std::size_t>() == i);
|
||||
CHECK(v.contains(key));
|
||||
CHECK(v.find(key).key() == key);
|
||||
CHECK(v.at(key).get<std::size_t>() == i);
|
||||
CHECK(!v.contains(key + "x"));
|
||||
}
|
||||
CHECK(v[""].get_string() == "empty key");
|
||||
CHECK(v["k1"].get<int>() == 1); // the first of duplicate keys, as for small objects
|
||||
CHECK(!v.contains("missing"));
|
||||
CHECK_THROWS_WITH_AS(v.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
|
||||
CHECK(v == j);
|
||||
CHECK(v.materialize() == j);
|
||||
}
|
||||
|
||||
SECTION("duplicate keys: the first member wins, with and without a table")
|
||||
{
|
||||
// an object of `total` members: the keys "k0".."k<n-1>" in order, then
|
||||
// three keys repeated twice more (one copy in the middle, one at the
|
||||
// end), and two keys repeated once; the value of a member is its
|
||||
// position, so that the first member of a key can be told apart
|
||||
struct member
|
||||
{
|
||||
std::string key;
|
||||
std::size_t position;
|
||||
};
|
||||
const auto make_members = [](std::size_t total)
|
||||
{
|
||||
std::vector<std::string> keys;
|
||||
for (std::size_t i = 0; i + 8 < total; ++i)
|
||||
{
|
||||
keys.push_back("k" + std::to_string(i));
|
||||
}
|
||||
const std::size_t n = keys.size();
|
||||
const std::array<std::size_t, 3> triple = {{3, 17, n - 1}};
|
||||
const std::array<std::size_t, 2> twice = {{5, n / 2}};
|
||||
std::vector<std::string> ordered = keys;
|
||||
for (const std::size_t i : triple)
|
||||
{
|
||||
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 2), keys[i]);
|
||||
}
|
||||
for (const std::size_t i : twice)
|
||||
{
|
||||
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 3), keys[i]);
|
||||
}
|
||||
for (const std::size_t i : triple)
|
||||
{
|
||||
ordered.push_back(keys[i]);
|
||||
}
|
||||
std::vector<member> result;
|
||||
result.reserve(ordered.size());
|
||||
for (std::size_t i = 0; i < ordered.size(); ++i)
|
||||
{
|
||||
result.push_back({ordered[i], i});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// 100 and 127 members: no table; 128 members and more: a table
|
||||
for (const std::size_t total :
|
||||
{
|
||||
100u, 127u, 128u, 200u, 5000u
|
||||
})
|
||||
{
|
||||
CAPTURE(total)
|
||||
const std::vector<member> members = make_members(total);
|
||||
REQUIRE(members.size() >= total);
|
||||
REQUIRE(members.size() >= 100);
|
||||
std::string text = "{";
|
||||
std::map<std::string, std::size_t> first;
|
||||
std::map<std::string, std::size_t> last;
|
||||
std::map<std::string, std::size_t> occurrences;
|
||||
for (const member& m : members)
|
||||
{
|
||||
text += (text.size() > 1 ? ",\"" : "\"") + m.key + "\":" + std::to_string(m.position);
|
||||
first.insert({m.key, m.position});
|
||||
last[m.key] = m.position;
|
||||
++occurrences[m.key];
|
||||
}
|
||||
text += '}';
|
||||
REQUIRE(first.size() < members.size());
|
||||
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
const json j = json::parse(text);
|
||||
CHECK(v.size() == members.size()); // every occurrence is visited
|
||||
const json m = v.materialize();
|
||||
for (const auto& entry : first)
|
||||
{
|
||||
CAPTURE(entry.first)
|
||||
const std::size_t expected = entry.second;
|
||||
CHECK(v[entry.first].get<std::size_t>() == expected);
|
||||
CHECK(v.at(entry.first).get<std::size_t>() == expected);
|
||||
CHECK(v.find(entry.first).value().get<std::size_t>() == expected);
|
||||
CHECK(v.value(entry.first, std::size_t{0}) == expected);
|
||||
CHECK(v.contains(entry.first));
|
||||
CHECK(v.count(entry.first) == occurrences[entry.first]);
|
||||
const std::string pointer = "/" + entry.first;
|
||||
CHECK(v[json::json_pointer(pointer)].get<std::size_t>() == expected);
|
||||
CHECK(v.at(json::json_pointer(pointer)).get<std::size_t>() == expected);
|
||||
CHECK(v.value(json::json_pointer(pointer), std::size_t{0}) == expected);
|
||||
CHECK(v.contains(json::json_pointer(pointer)));
|
||||
// materialize() and parse() keep the last value instead
|
||||
CHECK(j[entry.first].get<std::size_t>() == last.at(entry.first));
|
||||
CHECK(m[entry.first].get<std::size_t>() == last.at(entry.first));
|
||||
}
|
||||
CHECK(!v.contains("k"));
|
||||
CHECK(v["missing"].is_discarded());
|
||||
CHECK(v.materialize() == j);
|
||||
CHECK(v == j);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("colliding keys")
|
||||
{
|
||||
// the hash is not seeded, so keys that all land in one place must not
|
||||
// make the table build quadratic: such an object gets no table and is
|
||||
// searched linearly
|
||||
constexpr std::size_t members = 300;
|
||||
constexpr std::size_t table_size = 1024; // the table size for 300 members: the next power of two >= 600
|
||||
std::vector<std::string> colliding;
|
||||
std::vector<std::string> spread;
|
||||
for (std::uint64_t counter = 0; colliding.size() < members || spread.size() < members; ++counter)
|
||||
{
|
||||
std::string key(8, 'a');
|
||||
std::uint64_t x = counter;
|
||||
for (std::size_t i = 0; i < 8; ++i, x /= 26)
|
||||
{
|
||||
key[i] = static_cast<char>('a' + (x % 26));
|
||||
}
|
||||
const bool lands_in_slot_zero = (nlohmann::detail::view::key_hash(key.data(), key.size()) & (table_size - 1)) == 0;
|
||||
if (lands_in_slot_zero && colliding.size() < members)
|
||||
{
|
||||
colliding.push_back(key);
|
||||
}
|
||||
else if (!lands_in_slot_zero && spread.size() < members)
|
||||
{
|
||||
spread.push_back(key);
|
||||
}
|
||||
}
|
||||
|
||||
const auto make_text = [](const std::vector<std::string>& keys)
|
||||
{
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i % 10);
|
||||
}
|
||||
return text + "}";
|
||||
};
|
||||
const std::string colliding_text = make_text(colliding);
|
||||
const std::string spread_text = make_text(spread);
|
||||
|
||||
json_document with_collisions = json_document::parse(colliding_text);
|
||||
json_document without_collisions = json_document::parse(spread_text);
|
||||
for (const auto* pair :
|
||||
{
|
||||
&colliding, &spread
|
||||
})
|
||||
{
|
||||
const json_view v = (pair == &colliding ? with_collisions : without_collisions).root();
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CHECK(v[(*pair)[i]].get<std::size_t>() == i % 10);
|
||||
CHECK(v.at((*pair)[i]).get<std::size_t>() == i % 10);
|
||||
CHECK(v.find((*pair)[i]).key() == (*pair)[i]);
|
||||
CHECK(!v.contains((*pair)[i] + "x"));
|
||||
}
|
||||
CHECK(!v.contains("missing"));
|
||||
}
|
||||
CHECK(with_collisions.root() == json::parse(colliding_text));
|
||||
|
||||
// only the object with the spread keys got a table (both texts have the
|
||||
// same length, so the tables are the only difference)
|
||||
with_collisions.shrink_to_fit();
|
||||
without_collisions.shrink_to_fit();
|
||||
CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (table_size * sizeof(std::uint32_t)));
|
||||
}
|
||||
|
||||
SECTION("keys that differ in their last bytes are spread over the table")
|
||||
{
|
||||
// The hash must spread the usual families of keys (numbered names, hex
|
||||
// strings, dates, short names): a key farther than index_max_displacement
|
||||
// from its home slot would leave the object without a table. The keys
|
||||
// are placed with linear probing, as the table does.
|
||||
const auto max_displacement = [](const std::vector<std::string>& keys)
|
||||
{
|
||||
std::size_t slots = 16;
|
||||
while (slots < 2 * keys.size())
|
||||
{
|
||||
slots *= 2;
|
||||
}
|
||||
std::vector<bool> used(slots, false);
|
||||
std::size_t farthest = 0;
|
||||
for (const std::string& key : keys)
|
||||
{
|
||||
std::size_t i = nlohmann::detail::conditional_static_cast<std::size_t>(nlohmann::detail::view::key_hash(key.data(), key.size())) & (slots - 1);
|
||||
std::size_t distance = 0;
|
||||
while (used[i])
|
||||
{
|
||||
i = (i + 1) & (slots - 1);
|
||||
++distance;
|
||||
}
|
||||
used[i] = true;
|
||||
farthest = (std::max)(farthest, distance);
|
||||
}
|
||||
return farthest;
|
||||
};
|
||||
// the keys of a printf-like pattern with one %d, %0Nd or %0Nx, formatted with a stream
|
||||
// (a format string that is not a literal is a Flawfinder error)
|
||||
const auto numbered = [](const char* format, int count, int step)
|
||||
{
|
||||
const std::string pattern(format);
|
||||
const std::size_t percent = pattern.find('%');
|
||||
std::size_t conversion = percent + 1;
|
||||
std::size_t width = 0;
|
||||
while (pattern[conversion] >= '0' && pattern[conversion] <= '9')
|
||||
{
|
||||
width = (width * 10) + static_cast<std::size_t>(pattern[conversion] - '0');
|
||||
++conversion;
|
||||
}
|
||||
const bool hex = pattern[conversion] == 'x';
|
||||
std::vector<std::string> keys;
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
std::ostringstream key;
|
||||
key << pattern.substr(0, percent) << std::setfill('0') << std::setw(static_cast<int>(width))
|
||||
<< (hex ? std::hex : std::dec) << (i * step) << pattern.substr(conversion + 1);
|
||||
keys.push_back(key.str());
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
for (const char* format :
|
||||
{"key%d", "%d", "item_%04d", "a%05d", "%03dxyz", "id%d", "user_%d", "%d_suffix", "0x%04x", "A%07d", "12345678%d", "1234567%d", "123456789%d",
|
||||
"configuration_parameter_value_%04d_extra", "2026-01-%02dT00:00:00Z"
|
||||
})
|
||||
{
|
||||
CAPTURE(format)
|
||||
CHECK(max_displacement(numbered(format, std::string(format).find("%02d") != std::string::npos ? 60 : 1000, 1)) < nlohmann::detail::view::index_max_displacement); // NOLINT(abseil-string-find-str-contains)
|
||||
CHECK(max_displacement(numbered(format, 600, 7)) < nlohmann::detail::view::index_max_displacement);
|
||||
}
|
||||
std::mt19937_64 rng(7); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
for (const std::size_t length :
|
||||
{
|
||||
std::size_t{1}, std::size_t{2}, std::size_t{3}, std::size_t{5}, std::size_t{7}, std::size_t{8}, std::size_t{9}, std::size_t{12}, std::size_t{16}, std::size_t{17}, std::size_t{33}
|
||||
})
|
||||
{
|
||||
CAPTURE(length)
|
||||
std::set<std::string> distinct;
|
||||
// (there are 26 and 676 keys of one and two letters)
|
||||
std::size_t wanted = 700;
|
||||
if (length == 1)
|
||||
{
|
||||
wanted = 26;
|
||||
}
|
||||
else if (length == 2)
|
||||
{
|
||||
wanted = 500;
|
||||
}
|
||||
while (distinct.size() < wanted)
|
||||
{
|
||||
std::string key(length, 'a');
|
||||
for (char& c : key)
|
||||
{
|
||||
c = static_cast<char>('a' + (rng() % 26));
|
||||
}
|
||||
distinct.insert(key);
|
||||
}
|
||||
CHECK(max_displacement(std::vector<std::string>(distinct.begin(), distinct.end())) < nlohmann::detail::view::index_max_displacement);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("keys of every length that differ in one byte")
|
||||
{
|
||||
// In a large object, a key is found by comparing its bytes with those of the member the
|
||||
// table points to: up to 16 bytes as two overlapping words, longer ones with memcmp.
|
||||
// Keys of every length from 0 to 40, and keys that differ from them in one byte.
|
||||
std::mt19937_64 rng(2026); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
std::vector<std::string> keys;
|
||||
std::set<std::string> distinct;
|
||||
for (std::size_t length = 0; length <= 40; ++length)
|
||||
{
|
||||
for (int copy = 0; copy < 5; ++copy)
|
||||
{
|
||||
std::string key(length, 'a');
|
||||
for (char& c : key)
|
||||
{
|
||||
c = static_cast<char>('a' + (rng() % 26));
|
||||
}
|
||||
if (distinct.insert(key).second)
|
||||
{
|
||||
keys.push_back(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i);
|
||||
}
|
||||
text += "}";
|
||||
REQUIRE(keys.size() >= 128); // (a table is built for 128 members)
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
CAPTURE(keys[i])
|
||||
CHECK(v[keys[i]].get<std::size_t>() == i);
|
||||
CHECK(v.at(keys[i]).get<std::size_t>() == i);
|
||||
CHECK(v.contains(keys[i]));
|
||||
for (std::size_t at = 0; at < keys[i].size(); ++at)
|
||||
{
|
||||
std::string other = keys[i];
|
||||
other[at] = other[at] == 'z' ? 'y' : 'z';
|
||||
CAPTURE(other)
|
||||
CHECK(v.contains(other) == (distinct.count(other) != 0));
|
||||
}
|
||||
CHECK(v.contains(keys[i] + "a") == (distinct.count(keys[i] + "a") != 0));
|
||||
if (!keys[i].empty())
|
||||
{
|
||||
CHECK(v.contains(keys[i].substr(1)) == (distinct.count(keys[i].substr(1)) != 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("shrink_to_fit releases the tables' spare capacity")
|
||||
{
|
||||
const auto make_text = [](int objects, int members)
|
||||
{
|
||||
std::string text = "[";
|
||||
for (int object = 0; object < objects; ++object)
|
||||
{
|
||||
text += object != 0 ? ",{" : "{";
|
||||
for (int i = 0, n = members + object; i < n; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
text += '}';
|
||||
}
|
||||
return text + "]";
|
||||
};
|
||||
const std::string small_text = make_text(5, 150);
|
||||
const std::string big_text = make_text(40, 400);
|
||||
|
||||
// reading a big text, and then a small one, leaves the spare capacity
|
||||
// of the big one: shrink_to_fit() brings the document to the size of
|
||||
// one parsed from the small text alone
|
||||
json_document d = json_document::parse(big_text);
|
||||
const std::size_t big = d.memory_usage();
|
||||
d.read(small_text);
|
||||
CHECK(d.memory_usage() >= big);
|
||||
d.shrink_to_fit();
|
||||
json_document fresh = json_document::parse(small_text);
|
||||
fresh.shrink_to_fit();
|
||||
CHECK(d.memory_usage() == fresh.memory_usage());
|
||||
CHECK(d.memory_usage() < big / 2);
|
||||
CHECK(d.root() == json::parse(small_text));
|
||||
for (int object = 0; object < 5; ++object)
|
||||
{
|
||||
const json_view v = d.root()[static_cast<std::size_t>(object)];
|
||||
for (int i = 0, n = 150 + object; i < n; ++i)
|
||||
{
|
||||
CHECK(v[std::to_string(i)].get<int>() == i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("nested, reused, and in arrays")
|
||||
{
|
||||
std::string inner = "{";
|
||||
for (int i = 0; i < 300; ++i)
|
||||
{
|
||||
inner += (i != 0 ? ",\"m" : "\"m") + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
inner += '}';
|
||||
const std::string text = "[" + inner + ",{\"x\":" + inner + "}," + inner + "]";
|
||||
json_document d = json_document::parse(text);
|
||||
CHECK(d.root()[0]["m299"].get<int>() == 299);
|
||||
CHECK(d.root()[1]["x"]["m150"].get<int>() == 150);
|
||||
CHECK(d.root()[2]["m0"].get<int>() == 0);
|
||||
const std::size_t with_index = d.memory_usage();
|
||||
d.read(std::string("{\"small\": 1}"));
|
||||
CHECK(d.root()["small"].get<int>() == 1);
|
||||
d.read(text);
|
||||
CHECK(d.root()[2]["m7"].get<int>() == 7);
|
||||
CHECK(d.memory_usage() >= with_index / 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
# 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)
|
||||
```
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
#!/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())
|
||||
@@ -0,0 +1,27 @@
|
||||
# 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.
|
||||
Executable
+230
@@ -0,0 +1,230 @@
|
||||
#!/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())
|
||||
@@ -0,0 +1 @@
|
||||
PyYAML==6.0.3
|
||||
Reference in new issue
Block a user