diff --git a/.github/codecov/verification.gpg.asc b/.github/codecov/verification.gpg.asc
deleted file mode 100644
index b7dfce0ee56..00000000000
--- a/.github/codecov/verification.gpg.asc
+++ /dev/null
@@ -1,52 +0,0 @@
------BEGIN PGP PUBLIC KEY BLOCK-----
-
-mQINBGCsMn0BEACiCKZOhkbhUjb+obvhH49p3ShjJzU5b/GqAXSDhRhdXUq7ZoGq
-KEKCd7sQHrCf16Pi5UVacGIyE9hS93HwY15kMlLwM+lNeAeCglEscOjpCly1qUIr
-sN1wjkd2cwDXS6zHBJTqJ7wSOiXbZfTAeKhd6DuLEpmA+Rz4Yc+4qZP+fVxVG3Pv
-2v06m+E5CP/JQVQPO8HYi+S36hJImTh+zaDspu+VujSai5KzJ6YKmgwslVNIp5X5
-GnEr2uAh5w6UTnt9UQUjFFliAvQ3lPLWzm7DWs6AP9hslYxSWzwbzVF5qbOIjUJL
-KfoUpvCYDs2ObgRn8WUQO0ndkRCBIxhlF3HGGYWKQaCEsiom7lyi8VbAszmUCDjw
-HdbQHFmm5yHLpTXJbg+iaxQzKnhWVXzye5/x92IJmJswW81Ky346VxYdC1XFL/+Y
-zBaj9oMmV7WfRpdch09Gf4TgosMzWf3NjJbtKE5xkaghJckIgxwzcrRmF/RmCJue
-IMqZ8A5qUUlK7NBzj51xmAQ4BtkUa2bcCBRV/vP+rk9wcBWz2LiaW+7Mwlfr/C/Q
-Swvv/JW2LsQ4iWc1BY7m7ksn9dcdypEq/1JbIzVLCRDG7pbMj9yLgYmhe5TtjOM3
-ygk25584EhXSgUA3MZw+DIqhbHQBYgrKndTr2N/wuBQY62zZg1YGQByD4QARAQAB
-tEpDb2RlY292IFVwbG9hZGVyIChDb2RlY292IFVwbG9hZGVyIFZlcmlmaWNhdGlv
-biBLZXkpIDxzZWN1cml0eUBjb2RlY292LmlvPokCTgQTAQoAOBYhBCcDTn/bhQ4L
-vCxi/4Brsortd5hpBQJgrDJ9AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJ
-EIBrsortd5hpxLMP/3Fbgx5EG7zUUOqPZ+Ya9z8JlZFIkh3FxYMfMFE8jH9Es26F
-V2ZTJLO259MxM+5N0XzObi3h4XqIzBn42pDRfwtojY5wl2STJ9Bzu+ykPog7OB1u
-yfWXDRKcqPTUIxI1/WdU+c0/WNE6wjyzK+lRc1YUlp4pdNU7l+j2vKN+jGi2b6nV
-PTPRsMcwy3B90fKf5h2wNMNqO+KX/rjgpG9Uhej+xyFWkGM1tZDQQYFj+ugQUj61
-BMsQrUmxOnaVVnix21cHnACDCaxqgQZH3iZyEOKPNMsRFRP+0fLEnUMP+DVnQE6J
-Brk1Z+XhtjGI9PISQVx5KKDKscreS/D5ae2Cw/FUlQMf57kir6mkbZVhz2khtccz
-atD0r59WomNywIDyk1QfAKV0+O0WeJg8A69/Jk6yegsrUb5qEfkih/I38vvI0OVL
-BYve/mQIHuQo5ziBptNytCrN5TXHXzguX9GOW1V1+3DR+w/vXcnz67sjlYDysf1f
-JUZv9edZ2RGKW7agbrgOw2hB+zuWZ10tjoEcsaSGOLtKRGFDfmu/dBxzl8yopUpa
-Tn79QKOieleRm5+uCcKCPTeKV0GbhDntCZJ+Yiw6ZPmrpcjDowAoMQ9kiMVa10+Q
-WwwoaRWuqhf+dL6Q2OLFOxlyCDKVSyW0YF4Vrf3fKGyxKJmszAL+NS1mVcdxuQIN
-BGCsMn0BEADLrIesbpfdAfWRvUFDN+PoRfa0ROwa/JOMhEgVsowQuk9No8yRva/X
-VyiA6oCq6na7IvZXMxT7di4FWDjDtw5xHjbtFg336IJTGBcnzm7WIsjvyyw8kKfB
-8cvG7D2OkzAUF8SVXLarJ1zdBP/Dr1Nz6F/gJsx5+BM8wGHEz4DsdMRV7ZMTVh6b
-PaGuPZysPjSEw62R8MFJ1fSyDGCKJYwMQ/sKFzseNaY/kZVR5lq0dmhiYjNVQeG9
-HJ6ZCGSGT5PKNOwx/UEkT6jhvzWgfr2eFVGJTcdwSLEgIrJIDzP7myHGxuOiuCmJ
-ENgL1f7mzGkJ/hYXq1RWqsn1Fh2I9KZMHggqu4a+s3RiscmNcbIlIhJLXoE1bxZ/
-TfYZ9Aod6Bd5TsSMTZNwV2am9zelhDiFF60FWww/5nEbhm/X4suC9W86qWBxs3Kh
-vk1dxhElRjtgwUEHA5OFOO48ERHfR7COH719D/YmqLU3EybBgJbGoC/yjlGJxv0R
-kOMAiG2FneNKEZZihReh8A5Jt6jYrSoHFRwL6oJIZfLezB7Rdajx1uH7uYcUyIaE
-SiDWlkDw/IFM315NYFA8c1TCSIfnabUYaAxSLNFRmXnt+GQpm44qAK1x8EGhY633
-e5B4FWorIXx0tTmsVM4rkQ6IgAodeywKG+c2Ikd+5dQLFmb7dW/6CwARAQABiQI2
-BBgBCgAgFiEEJwNOf9uFDgu8LGL/gGuyiu13mGkFAmCsMn0CGwwACgkQgGuyiu13
-mGkYWxAAkzF64SVpYvY9nY/QSYikL8UHlyyqirs6eFZ3Mj9lMRpHM2Spn9a3c701
-0Ge4wDbRP2oftCyPP+p9pdUA77ifMTlRcoMYX8oXAuyE5RT2emBDiWvSR6hQQ8bZ
-WFNXal+bUPpaRiruCCUPD2b8Od1ftzLqbYOosxr/m5Du0uahgOuGw6zlGBJCVOo7
-UB2Y++oZ8P7oDGF722opepWQ+bl2a6TRMLNWWlj4UANknyjlhyZZ7PKhWLjoC6MU
-dAKcwQUdp+XYLc/3b00bvgju0e99QgHZMX2fN3d3ktdN5Q2fqiAi5R6BmCCO4ISF
-o5j10gGU/sdqGHvNhv5C21ibun7HEzMtxBhnhGmytfBJzrsj7GOReePsfTLoCoUq
-dFMOAVUDciVfRtL2m8cv42ZJOXtPfDjsFOf8AKJk40/tc8mMMqZP7RVBr9RWOoq5
-y9D37NfI6UB8rPZ6qs0a1Vfm8lIh2/k1AFECduXgftMDTsmmXOgXXS37HukGW7AL
-QKWiWJQF/XopkXwkyAYpyuyRMZ77oF7nuqLFnl5VVEiRo0Fwu45erebc6ccSwYZU
-8pmeSx7s0aJtxCZPSZEKZ3mn0BXOR32Cgs48CjzFWf6PKucTwOy/YO0/4Gt/upNJ
-3DyeINcYcKyD08DEIF9f5tLyoiD4xz+N23ltTBoMPyv4f3X/wCQ=
-=ch7z
------END PGP PUBLIC KEY BLOCK-----
diff --git a/.github/coverage-thresholds.json b/.github/coverage-thresholds.json
new file mode 100644
index 00000000000..d34c1627305
--- /dev/null
+++ b/.github/coverage-thresholds.json
@@ -0,0 +1,9 @@
+{
+ "schema_version": 1,
+ "scope": "src/coreComponents",
+ "minimum_basis_points": {
+ "lines": 7001,
+ "functions": 7501,
+ "branches": 5001
+ }
+}
diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml
index 4b746e51a9a..d921fbfa038 100644
--- a/.github/workflows/build_and_test.yml
+++ b/.github/workflows/build_and_test.yml
@@ -20,10 +20,18 @@ on:
required: false
type: string
default: ''
- CODE_COVERAGE:
+ LLVM_SOURCE_COVERAGE:
required: false
type: boolean
default: false
+ COVERAGE_BASE_SHA:
+ required: false
+ type: string
+ default: ''
+ COVERAGE_HEAD_SHA:
+ required: false
+ type: string
+ default: ''
CTEST_PARALLEL_LEVEL:
required: false
type: string
@@ -88,8 +96,6 @@ on:
required: false
type: string
secrets:
- CODECOV_TOKEN:
- required: false
GOOGLE_CLOUD_GCP:
required: false
jobs:
@@ -135,7 +141,9 @@ jobs:
with:
submodules: true
lfs: ${{ inputs.BUILD_TYPE == 'integrated_tests' }}
- fetch-depth: 1
+ # A coverage PR is built from GitHub's merge commit. Its two parents
+ # are needed to attribute coverage to the exact patch that was tested.
+ fetch-depth: ${{ inputs.LLVM_SOURCE_COVERAGE && 2 || 1 }}
- id: 'auth'
if: ${{ (inputs.UPLOAD_TO_GCP && inputs.GCP_BUCKET) || (inputs.USE_SCCACHE && !startsWith(inputs.RUNS_ON, 'streak2')) }}
@@ -144,18 +152,6 @@ jobs:
credentials_json: '${{ secrets.GOOGLE_CLOUD_GCP }}'
create_credentials_file: true
- - name: Prepare Codecov verification key
- if: inputs.CODE_COVERAGE
- run: |
- set -euxo pipefail
- GNUPGHOME="${RUNNER_TEMP}/codecov-gnupg"
- export GNUPGHOME
- mkdir -p "${GNUPGHOME}"
- chmod 700 "${GNUPGHOME}"
- gpg --batch --import "${GITHUB_WORKSPACE}/.github/codecov/verification.gpg.asc" || true
- gpg --batch --list-keys 806BB28AED779869 >/dev/null
- echo "GNUPGHOME=${GNUPGHOME}" >> "${GITHUB_ENV}"
-
- name: 'Set up Cloud SDK'
if: inputs.UPLOAD_TO_GCP && inputs.GCP_BUCKET
uses: 'google-github-actions/setup-gcloud@v2.1.4'
@@ -163,12 +159,25 @@ jobs:
version: '>= 363.0.0'
- name: Build, test, deploy.
+ env:
+ CI_DOCKER_IMAGE_TAG: ${{ inputs.DOCKER_IMAGE_TAG }}
+ CI_DOCKER_REPOSITORY: ${{ inputs.DOCKER_REPOSITORY }}
+ CI_LLVM_SOURCE_COVERAGE: ${{ inputs.LLVM_SOURCE_COVERAGE }}
+ CI_RUNNER_LABEL: ${{ inputs.RUNS_ON }}
+ CI_COVERAGE_BASE_SHA: ${{ inputs.COVERAGE_BASE_SHA }}
+ CI_COVERAGE_HEAD_SHA: ${{ inputs.COVERAGE_HEAD_SHA }}
+ CI_REPOSITORY: ${{ github.repository }}
+ # This standard, read-only Actions token is used only by the bounded
+ # host-side baseline fetcher and is never passed into Docker.
+ GITHUB_TOKEN: ${{ github.token }}
id: build_test_deploy
run: |
# Those two bash arrays will be populated depending on the required options,
# and expended as CLI arguments for the docker and scripts calls.
docker_args=()
script_args=()
+ DOCKER_IMAGE="${CI_DOCKER_REPOSITORY}:${CI_DOCKER_IMAGE_TAG}"
+ DOCKER_RUN_IMAGE="${DOCKER_IMAGE}"
# Keep the repository mount point consistent between Docker and script args.
GITHUB_WORKSPACE_MOUNT_POINT=/tmp/geos
@@ -184,7 +193,7 @@ jobs:
docker_args+=(${{ inputs.DOCKER_RUN_ARGS }})
- COMMIT=${{ github.event.pull_request.head.sha }}
+ COMMIT=${{ github.event.pull_request.head.sha || github.sha }}
SHORT_COMMIT=${COMMIT:0:7}
script_args+=(--install-dir-basename GEOS-${SHORT_COMMIT})
@@ -245,6 +254,91 @@ jobs:
docker_args+=(--volume=${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE_MOUNT_POINT})
script_args+=(--repository ${GITHUB_WORKSPACE_MOUNT_POINT})
+ if [[ "${CI_LLVM_SOURCE_COVERAGE}" == 'true' ]]; then
+ coverage_base_sha="${CI_COVERAGE_BASE_SHA}"
+ coverage_head_sha="${CI_COVERAGE_HEAD_SHA}"
+ if [[ ! "${coverage_head_sha}" =~ ^[0-9a-f]{40}$ ]]; then
+ echo "Coverage head SHA is missing or invalid: ${coverage_head_sha}" >&2
+ exit 1
+ fi
+ if [[ "$(git rev-parse HEAD)" != "${coverage_head_sha}" ]]; then
+ echo "Coverage checkout does not match the tested head SHA." >&2
+ exit 1
+ fi
+
+ if [[ -n "${coverage_base_sha}" ]]; then
+ if [[ ! "${coverage_base_sha}" =~ ^[0-9a-f]{40}$ ]]; then
+ echo "Coverage base SHA is invalid: ${coverage_base_sha}" >&2
+ exit 1
+ fi
+ if ! git cat-file -e "${coverage_base_sha}^{commit}" 2>/dev/null; then
+ git fetch --no-tags --depth=1 origin "${coverage_base_sha}"
+ fi
+ if ! git cat-file -e "${coverage_base_sha}^{commit}" 2>/dev/null; then
+ echo "Coverage base commit is unavailable after fetching it." >&2
+ exit 1
+ fi
+ if [[ "$(git rev-parse "${coverage_head_sha}^1")" != "${coverage_base_sha}" ]]; then
+ echo "Coverage base SHA is not the first parent of the tested merge commit." >&2
+ exit 1
+ fi
+ fi
+
+ COVERAGE_ARTIFACT_DIR=$(mktemp -d "${RUNNER_TEMP}/llvm-source-coverage.XXXXXX")
+ if [[ ! -d "${COVERAGE_ARTIFACT_DIR}" || -L "${COVERAGE_ARTIFACT_DIR}" ]]; then
+ echo "Failed to create a safe LLVM coverage artifact directory." >&2
+ exit 1
+ fi
+ printf 'LLVM_COVERAGE_ARTIFACT_DIR=%s\n' "${COVERAGE_ARTIFACT_DIR}" >> "${GITHUB_ENV}"
+ COVERAGE_ARTIFACT_MOUNT_POINT=/tmp/llvm-source-coverage-artifact
+ docker_args+=(--volume=${COVERAGE_ARTIFACT_DIR}:${COVERAGE_ARTIFACT_MOUNT_POINT})
+ docker pull "${DOCKER_IMAGE}"
+ docker_image_id=$(docker image inspect --format '{{.Id}}' "${DOCKER_IMAGE}")
+ docker_image_digests=$(docker image inspect --format '{{json .RepoDigests}}' "${DOCKER_IMAGE}")
+ DOCKER_RUN_IMAGE="${docker_image_id}"
+ docker_args+=(-e "GEOS_CI_CONTAINER_IMAGE=${DOCKER_IMAGE}")
+ docker_args+=(-e "GEOS_CI_CONTAINER_IMAGE_ID=${docker_image_id}")
+ docker_args+=(-e "GEOS_CI_CONTAINER_IMAGE_DIGESTS=${docker_image_digests}")
+ docker_args+=(-e "GEOS_CI_RUNNER_LABEL=${CI_RUNNER_LABEL}")
+ script_args+=(--llvm-source-coverage
+ --coverage-output-dir "${COVERAGE_ARTIFACT_MOUNT_POINT}"
+ --coverage-head-sha "${coverage_head_sha}")
+
+ if [[ -n "${coverage_base_sha}" ]]; then
+ script_args+=(--coverage-base-sha "${coverage_base_sha}")
+ COVERAGE_BASELINE_DIR=$(mktemp -d "${RUNNER_TEMP}/llvm-coverage-baseline.XXXXXX")
+ if [[ ! -d "${COVERAGE_BASELINE_DIR}" || -L "${COVERAGE_BASELINE_DIR}" ]]; then
+ echo "Failed to create a safe coverage baseline directory." >&2
+ exit 1
+ fi
+ printf 'LLVM_COVERAGE_BASELINE_DIR=%s\n' "${COVERAGE_BASELINE_DIR}" >> "${GITHUB_ENV}"
+
+ set +e
+ python3 scripts/fetch_coverage_baseline.py \
+ --repository "${CI_REPOSITORY}" \
+ --workflow ci_tests.yml \
+ --base-sha "${coverage_base_sha}" \
+ --artifact-name-prefix llvm-source-coverage-baseline- \
+ --token-env GITHUB_TOKEN \
+ --output-dir "${COVERAGE_BASELINE_DIR}"
+ baseline_fetch_status=$?
+ set -e
+ case "${baseline_fetch_status}" in
+ 0)
+ baseline_mount=/tmp/llvm-coverage-baseline
+ docker_args+=(--volume="${COVERAGE_BASELINE_DIR}:${baseline_mount}:ro")
+ script_args+=(--coverage-baseline-summary "${baseline_mount}/coverage-summary.json")
+ ;;
+ 3)
+ echo "No trusted exact-base coverage artifact is available; PR patch coverage will still be reported."
+ ;;
+ *)
+ echo "::warning::Coverage baseline lookup failed with status ${baseline_fetch_status}; PR patch coverage will continue without an aggregate delta."
+ ;;
+ esac
+ fi
+ fi
+
if [ ! -z "${{ inputs.GEOS_ENABLE_BOUNDS_CHECK }}" ]; then
script_args+=(--geos-enable-bounds-check "${{ inputs.GEOS_ENABLE_BOUNDS_CHECK }}")
fi
@@ -291,7 +385,7 @@ jobs:
script_args+=(${{ inputs.BUILD_AND_TEST_CLI_ARGS }})
- DOCKER_REPOSITORY=${{ inputs.DOCKER_REPOSITORY }}
+ DOCKER_REPOSITORY=${CI_DOCKER_REPOSITORY}
SPLIT_DOCKER_REPOSITORY=(${DOCKER_REPOSITORY//// })
CONTAINER_NAME=geosx_build_${SPLIT_DOCKER_REPOSITORY[1]}_${GITHUB_SHA:0:7}_${{ inputs.CMAKE_BUILD_TYPE }}
echo "CONTAINER_NAME: ${CONTAINER_NAME}"
@@ -305,10 +399,6 @@ jobs:
script_args+=(--ctest-parallel-level "${{ inputs.CTEST_PARALLEL_LEVEL }}")
fi
- if ${{ inputs.CODE_COVERAGE }} == 'true'; then
- script_args+=(--code-coverage)
- fi
-
if [[ -n "${{ inputs.LOCAL_BASELINE_DIR }}" ]]; then
# Extract the 'baseline' value
@@ -353,7 +443,7 @@ jobs:
--user nobody \
${docker_args[@]} \
-h=`hostname` \
- ${{ inputs.DOCKER_REPOSITORY }}:${{ inputs.DOCKER_IMAGE_TAG }} \
+ ${DOCKER_RUN_IMAGE} \
${GITHUB_WORKSPACE_MOUNT_POINT}/scripts/ci_build_and_test_in_container.sh \
${script_args[@]}"
@@ -365,7 +455,7 @@ jobs:
docker run \
${docker_args[@]} \
-h=`hostname` \
- ${{ inputs.DOCKER_REPOSITORY }}:${{ inputs.DOCKER_IMAGE_TAG }} \
+ "${DOCKER_RUN_IMAGE}" \
${GITHUB_WORKSPACE_MOUNT_POINT}/scripts/ci_build_and_test_in_container.sh \
${script_args[@]}
EXIT_STATUS=$?
@@ -473,34 +563,160 @@ jobs:
echo "GCP upload disabled."
fi
- - name: Prepare pip for Codecov
- if: inputs.CODE_COVERAGE
+ if [[ "${CI_LLVM_SOURCE_COVERAGE}" == 'true' &&
+ -n "${COVERAGE_BASELINE_DIR:-}" &&
+ -f "${COVERAGE_BASELINE_DIR}/coverage-baseline-status.json" &&
+ ! -L "${COVERAGE_BASELINE_DIR}/coverage-baseline-status.json" ]]; then
+ cp -- "${COVERAGE_BASELINE_DIR}/coverage-baseline-status.json" \
+ "${COVERAGE_ARTIFACT_DIR}/coverage-baseline-status.json"
+ fi
+
+ if [[ "${CI_LLVM_SOURCE_COVERAGE}" == 'true' &&
+ -n "${COVERAGE_ARTIFACT_DIR:-}" &&
+ -d "${COVERAGE_ARTIFACT_DIR}" &&
+ ! -L "${COVERAGE_ARTIFACT_DIR}" ]]; then
+ if ! python3 scripts/render_coverage_report_html.py \
+ --artifact-dir "${COVERAGE_ARTIFACT_DIR}" \
+ --output "${COVERAGE_ARTIFACT_DIR}/index.html"; then
+ echo "LLVM source coverage HTML report could not be finalized." >&2
+ if [[ ${EXIT_STATUS} -eq 0 ]]; then
+ EXIT_STATUS=1
+ fi
+ fi
+ fi
+
+ exit ${EXIT_STATUS}
+
+ - name: Validate LLVM coverage HTML report
+ id: validate_llvm_coverage_artifact
+ if: ${{ always() && inputs.LLVM_SOURCE_COVERAGE }}
run: |
- set -euxo pipefail
- python3 --version
- python3 -m ensurepip --upgrade || true
- python3 -m pip --version
- mkdir -p "${RUNNER_TEMP}/codecov-bin"
- cat > "${RUNNER_TEMP}/codecov-bin/pip" <<'EOF'
- #!/usr/bin/env bash
- exec python3 -m pip "$@"
- EOF
- chmod +x "${RUNNER_TEMP}/codecov-bin/pip"
- echo "${RUNNER_TEMP}/codecov-bin" >> "${GITHUB_PATH}"
-
- - name: Upload coverage to Codecov
- if: inputs.CODE_COVERAGE
- uses: codecov/codecov-action@v7.0.0
- with:
- files: ${GITHUB_WORKSPACE}/geos_coverage.info.cleaned
- fail_ci_if_error: true
- use_pypi: true
- use_legacy_upload_endpoint: true
+ if [[ -z "${LLVM_COVERAGE_ARTIFACT_DIR:-}" ||
+ ! -d "${LLVM_COVERAGE_ARTIFACT_DIR}" ||
+ -L "${LLVM_COVERAGE_ARTIFACT_DIR}" ]]; then
+ echo "LLVM coverage artifact directory is missing or unsafe." >&2
+ exit 1
+ fi
+ if [[ ! -f "${LLVM_COVERAGE_ARTIFACT_DIR}/index.html" ||
+ -L "${LLVM_COVERAGE_ARTIFACT_DIR}/index.html" ]]; then
+ echo "LLVM coverage HTML report is missing or unsafe." >&2
+ exit 1
+ fi
+ unsafe_link=$(find "${LLVM_COVERAGE_ARTIFACT_DIR}" -type l -print -quit)
+ if [[ -n "${unsafe_link}" ]]; then
+ echo "LLVM coverage artifact contains a symbolic link: ${unsafe_link}" >&2
+ exit 1
+ fi
+ unexpected_path=$(find "${LLVM_COVERAGE_ARTIFACT_DIR}" -mindepth 1 ! -type f -print -quit)
+ if [[ -n "${unexpected_path}" ]]; then
+ echo "LLVM coverage artifact contains a non-file entry: ${unexpected_path}" >&2
+ exit 1
+ fi
+ while IFS= read -r artifact_file; do
+ case "$(basename -- "${artifact_file}")" in
+ CMakeCache.txt|branch-summary.txt|coverage-baseline-status.json|coverage-objects.txt|coverage-status.md|coverage-summary.json|coverage-summary.md|ctest.log|index.html|llvm-summary.json|mapping-integrity.log|native-export.log|pr-coverage.json|pr-coverage.md|summary-export.log|toolchain.txt) ;;
+ *)
+ echo "Unexpected LLVM coverage artifact: ${artifact_file}" >&2
+ exit 1
+ ;;
+ esac
+ done < <(find "${LLVM_COVERAGE_ARTIFACT_DIR}" -mindepth 1 -maxdepth 1 -type f -print)
+ artifact_bytes=$(find "${LLVM_COVERAGE_ARTIFACT_DIR}" -type f -printf '%s\n' \
+ | awk '{ total += $1 } END { print total + 0 }')
+ if (( artifact_bytes > 50 * 1024 * 1024 )); then
+ echo "LLVM coverage artifact is too large: ${artifact_bytes} bytes" >&2
+ exit 1
+ fi
+
+ - name: Publish LLVM coverage summary
+ if: ${{ always() && inputs.LLVM_SOURCE_COVERAGE }}
env:
- CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- SSL_CERT_FILE: /etc/pki/tls/certs/ca-bundle.crt
- REQUESTS_CA_BUNDLE: /etc/pki/tls/certs/ca-bundle.crt
- CURL_CA_BUNDLE: /etc/pki/tls/certs/ca-bundle.crt
+ CI_COVERAGE_ARTIFACT_VALIDATION: ${{ steps.validate_llvm_coverage_artifact.outcome }}
+ CI_COVERAGE_BASE_SHA: ${{ inputs.COVERAGE_BASE_SHA }}
+ run: |
+ printf '## LLVM source coverage\n\n' >> "${GITHUB_STEP_SUMMARY}"
+ if [[ "${CI_COVERAGE_ARTIFACT_VALIDATION}" != success ]]; then
+ {
+ printf '### ❌ Coverage results unavailable\n\n'
+ printf '| Validation | Result | Details |\n'
+ printf '|---|:---:|---|\n'
+ printf '| HTML report artifact | ❌ Failed | Artifact validation failed; inspect the job log |\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ exit 0
+ fi
+ status_report="${LLVM_COVERAGE_ARTIFACT_DIR}/coverage-status.md"
+ metrics_report="${LLVM_COVERAGE_ARTIFACT_DIR}/coverage-summary.md"
+ if [[ -f "${status_report}" && ! -L "${status_report}" ]]; then
+ cat "${status_report}" >> "${GITHUB_STEP_SUMMARY}"
+ else
+ {
+ printf '### ❌ Coverage results unavailable\n\n'
+ printf '| Validation | Result | Details |\n'
+ printf '|---|:---:|---|\n'
+ printf '| Build, test, and report | ❌ Failed | No status report was produced; inspect the job log |\n\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+ if [[ -f "${metrics_report}" && ! -L "${metrics_report}" ]]; then
+ cat "${metrics_report}" >> "${GITHUB_STEP_SUMMARY}"
+ else
+ {
+ printf '### Coverage metrics unavailable\n\n'
+ printf 'The job did not produce a validated coverage report. Inspect the build and report logs.\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+ if [[ -n "${CI_COVERAGE_BASE_SHA}" ]]; then
+ pr_report="${LLVM_COVERAGE_ARTIFACT_DIR}/pr-coverage.md"
+ if [[ -f "${pr_report}" && ! -L "${pr_report}" ]]; then
+ printf '\n' >> "${GITHUB_STEP_SUMMARY}"
+ cat "${pr_report}" >> "${GITHUB_STEP_SUMMARY}"
+ else
+ {
+ printf '\n### PR coverage unavailable\n\n'
+ printf 'The patch-specific report was not produced; inspect the job log.\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+ fi
+
+ - name: Retain LLVM coverage HTML report
+ if: ${{ always() && inputs.LLVM_SOURCE_COVERAGE && steps.validate_llvm_coverage_artifact.outcome == 'success' }}
+ uses: actions/upload-artifact@v4.6.2
+ with:
+ name: llvm-source-coverage-${{ github.run_id }}-${{ github.run_attempt }}
+ path: ${{ env.LLVM_COVERAGE_ARTIFACT_DIR }}/index.html
+ if-no-files-found: warn
+ retention-days: 7
+
+ - name: Retain trusted develop coverage baseline
+ if: ${{ success() && inputs.LLVM_SOURCE_COVERAGE && github.event_name == 'push' && github.ref == 'refs/heads/develop' && steps.validate_llvm_coverage_artifact.outcome == 'success' }}
+ uses: actions/upload-artifact@v4.6.2
+ with:
+ name: llvm-source-coverage-baseline-${{ github.sha }}
+ path: |
+ ${{ env.LLVM_COVERAGE_ARTIFACT_DIR }}/coverage-summary.json
+ ${{ env.LLVM_COVERAGE_ARTIFACT_DIR }}/llvm-summary.json
+ ${{ env.LLVM_COVERAGE_ARTIFACT_DIR }}/toolchain.txt
+ if-no-files-found: error
+ overwrite: true
+ retention-days: 90
+
+ - name: Remove temporary LLVM coverage files
+ if: ${{ always() && inputs.LLVM_SOURCE_COVERAGE }}
+ run: |
+ if [[ -n "${LLVM_COVERAGE_ARTIFACT_DIR:-}" &&
+ -d "${LLVM_COVERAGE_ARTIFACT_DIR}" &&
+ ! -L "${LLVM_COVERAGE_ARTIFACT_DIR}" ]]; then
+ docker run --rm \
+ --volume="${LLVM_COVERAGE_ARTIFACT_DIR}:/tmp/coverage-artifact" \
+ alpine:3.20 \
+ sh -lc 'find /tmp/coverage-artifact -mindepth 1 -delete'
+ rmdir "${LLVM_COVERAGE_ARTIFACT_DIR}"
+ fi
+ if [[ -n "${LLVM_COVERAGE_BASELINE_DIR:-}" &&
+ -d "${LLVM_COVERAGE_BASELINE_DIR}" &&
+ ! -L "${LLVM_COVERAGE_BASELINE_DIR}" ]]; then
+ find "${LLVM_COVERAGE_BASELINE_DIR}" -mindepth 1 -maxdepth 1 -type f -delete
+ rmdir "${LLVM_COVERAGE_BASELINE_DIR}"
+ fi
- name: Show sccache summary
if: ${{ always() && inputs.USE_SCCACHE }}
diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml
index 27d9f1f9cdc..e387304cd43 100644
--- a/.github/workflows/ci_tests.yml
+++ b/.github/workflows/ci_tests.yml
@@ -13,6 +13,10 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+permissions:
+ contents: read
+ pull-requests: read
+
# Please define `build.args.GEOS_TPL_TAG` in `.devcontainer/devcontainer.json`
jobs:
@@ -59,7 +63,13 @@ jobs:
- name: Extract docker image tag
id: extract_docker_image_tag
run: |
- echo "DOCKER_IMAGE_TAG=$(jq '.build.args.GEOS_TPL_TAG' -r .devcontainer/devcontainer.json)" >> "$GITHUB_OUTPUT"
+ docker_image_tag=$(jq -er '.build.args.GEOS_TPL_TAG | select(type == "string")' \
+ .devcontainer/devcontainer.json)
+ if [[ ! "${docker_image_tag}" =~ ^[0-9]+-[0-9]+$ ]]; then
+ echo "Invalid GEOS_TPL_TAG: ${docker_image_tag}" >&2
+ exit 1
+ fi
+ printf 'DOCKER_IMAGE_TAG=%s\n' "${docker_image_tag}" >> "${GITHUB_OUTPUT}"
# PR must be assigned to be merged.
# This job will fail if this is not the case.
@@ -301,26 +311,36 @@ jobs:
- name: Check that the baseline logs are modified if rebaselines are detected
run: "scripts/check_baseline_log.sh"
- # code_coverage:
- # needs:
- # - is_not_draft_pull_request
- # uses: ./.github/workflows/build_and_test.yml
- # secrets:
- # CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- # GOOGLE_CLOUD_GCP: ${{ secrets.GOOGLE_CLOUD_GCP }}
- # with:
- # BUILD_AND_TEST_CLI_ARGS: "--no-run-unit-tests"
- # CMAKE_BUILD_TYPE: Debug
- # CODE_COVERAGE: true
- # DOCKER_IMAGE_TAG: ${{ needs.is_not_draft_pull_request.outputs.DOCKER_IMAGE_TAG }}
- # DOCKER_REPOSITORY: geosx/ubuntu24.04-gcc13
- # ENABLE_HYPRE: ON
- # ENABLE_TRILINOS: OFF
- # RUNS_ON: streak2
- # REQUIRED_LABEL: "ci: run code coverage"
- # HOST_CONFIG: /spack-generated.cmake
- # NPROC: 4
- # DOCKER_RUN_ARGS: "--cpus=8 --memory=256g -v /etc/pki/tls/certs/ca-bundle.crt:/etc/pki/tls/certs/ca-bundle.crt:ro -v /etc/pki/tls/certs/ca-bundle.crt:/certs/ca-bundle.crt:ro"
+ llvm_source_coverage:
+ needs:
+ - is_not_draft_pull_request
+ permissions:
+ actions: read
+ contents: read
+ uses: ./.github/workflows/build_and_test.yml
+ with:
+ BUILD_AND_TEST_CLI_ARGS: "--build-exe-only"
+ BUILD_SHARED_LIBS: "ON"
+ CMAKE_BUILD_TYPE: RelWithDebInfo
+ COVERAGE_BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || '' }}
+ COVERAGE_HEAD_SHA: ${{ github.sha }}
+ # Each instrumented process flushes a sizeable LLVM profile. Keeping 12
+ # CTest slots avoids turning the short smoke-test phase into an I/O race.
+ CTEST_PARALLEL_LEVEL: "12"
+ DOCKER_IMAGE_TAG: ${{ needs.is_not_draft_pull_request.outputs.DOCKER_IMAGE_TAG }}
+ DOCKER_REPOSITORY: geosx/ubuntu24.04-clang20
+ DOCKER_RUN_ARGS: "--cpus=32 --memory=256g -v /etc/pki/tls/certs/ca-bundle.crt:/etc/pki/tls/certs/ca-bundle.crt:ro -v /etc/pki/tls/certs/ca-bundle.crt:/certs/ca-bundle.crt:ro"
+ ENABLE_HYPRE: "ON"
+ ENABLE_TRILINOS: "OFF"
+ GEOS_ENABLE_BOUNDS_CHECK: "OFF"
+ HOST_CONFIG: /spack-generated.cmake
+ LLVM_SOURCE_COVERAGE: true
+ REQUIRED_LABEL: "ci: run code coverage"
+ # Instrumented compilation dominates runtime, so reserve the dedicated
+ # 32-core lane while leaving the profile-heavy test concurrency bounded.
+ NPROC: "32"
+ RUNS_ON: streak2-32core
+ USE_SCCACHE: false
# mac_builds:
@@ -446,6 +466,7 @@ jobs:
- check_code_rules
- cpu_builds
- cuda_builds
+ - llvm_source_coverage
- run_integrated_tests
if: ${{ always() }}
steps:
@@ -455,12 +476,14 @@ jobs:
echo "check_code_style_and_documentation: ${{needs.check_code_style_and_documentation.result}}"
echo "cpu_builds: ${{needs.cpu_builds.result}}"
echo "cuda_builds: ${{needs.cuda_builds.result}}"
+ echo "llvm_source_coverage: ${{needs.llvm_source_coverage.result}}"
echo "run_integrated_tests: ${{needs.run_integrated_tests.result}} "
${{
needs.if_not_unassigned_pull_request.result == 'success' &&
needs.are_submodules_in_sync.result == 'success' &&
needs.check_code_style_and_documentation.result == 'success' &&
needs.cpu_builds.result == 'success' &&
- needs.cuda_builds.result == 'success' &&
+ needs.cuda_builds.result == 'success' &&
+ needs.llvm_source_coverage.result == 'success' &&
needs.run_integrated_tests.result == 'success'
}}
diff --git a/README.md b/README.md
index 6d4cfee25ff..d16a4167be9 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,4 @@
[](https://zenodo.org/badge/latestdoi/131810578)
-[](https://codecov.io/github/GEOS-DEV/GEOS)
[](https://github.com/GEOS-DEV/GEOS/actions?query=branch%3Adevelop)
[](https://geosx-geosx.readthedocs-hosted.com/en/latest/)
diff --git a/inputFiles/coverageSmokes/ALM_inclinedFault_small_mesh.xml b/inputFiles/coverageSmokes/ALM_inclinedFault_small_mesh.xml
new file mode 100644
index 00000000000..0ddc5c89ce6
--- /dev/null
+++ b/inputFiles/coverageSmokes/ALM_inclinedFault_small_mesh.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/ContactMechanics_SimpleCubes_small_mesh.xml b/inputFiles/coverageSmokes/ContactMechanics_SimpleCubes_small_mesh.xml
new file mode 100644
index 00000000000..8afbe912908
--- /dev/null
+++ b/inputFiles/coverageSmokes/ContactMechanics_SimpleCubes_small_mesh.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/FieldCaseTutorial3_Isothermal_small_mesh.xml b/inputFiles/coverageSmokes/FieldCaseTutorial3_Isothermal_small_mesh.xml
new file mode 100644
index 00000000000..5fe85c30c3b
--- /dev/null
+++ b/inputFiles/coverageSmokes/FieldCaseTutorial3_Isothermal_small_mesh.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/FieldCaseTutorial3_Thermal_small_mesh.xml b/inputFiles/coverageSmokes/FieldCaseTutorial3_Thermal_small_mesh.xml
new file mode 100644
index 00000000000..70520b5d874
--- /dev/null
+++ b/inputFiles/coverageSmokes/FieldCaseTutorial3_Thermal_small_mesh.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/FlowProppantBedTransport2d_fast_outputs.xml b/inputFiles/coverageSmokes/FlowProppantBedTransport2d_fast_outputs.xml
new file mode 100644
index 00000000000..2ad6918b144
--- /dev/null
+++ b/inputFiles/coverageSmokes/FlowProppantBedTransport2d_fast_outputs.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/LagrangeContactBubbleStab_small_mesh.xml b/inputFiles/coverageSmokes/LagrangeContactBubbleStab_small_mesh.xml
new file mode 100644
index 00000000000..ab01ad63878
--- /dev/null
+++ b/inputFiles/coverageSmokes/LagrangeContactBubbleStab_small_mesh.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/PhaseFieldFracture_Nucleation_fast_events.xml b/inputFiles/coverageSmokes/PhaseFieldFracture_Nucleation_fast_events.xml
new file mode 100644
index 00000000000..cd3a3c807bc
--- /dev/null
+++ b/inputFiles/coverageSmokes/PhaseFieldFracture_Nucleation_fast_events.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/PoroElastic_staircase_fim_small_mesh.xml b/inputFiles/coverageSmokes/PoroElastic_staircase_fim_small_mesh.xml
new file mode 100644
index 00000000000..68ad874ddfa
--- /dev/null
+++ b/inputFiles/coverageSmokes/PoroElastic_staircase_fim_small_mesh.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/SeismicityRate_small_mesh.xml b/inputFiles/coverageSmokes/SeismicityRate_small_mesh.xml
new file mode 100644
index 00000000000..a4f9bded257
--- /dev/null
+++ b/inputFiles/coverageSmokes/SeismicityRate_small_mesh.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/SpringSliderImplicit_fast_events.xml b/inputFiles/coverageSmokes/SpringSliderImplicit_fast_events.xml
new file mode 100644
index 00000000000..8bac2c514e0
--- /dev/null
+++ b/inputFiles/coverageSmokes/SpringSliderImplicit_fast_events.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/TFrac_small_mesh.xml b/inputFiles/coverageSmokes/TFrac_small_mesh.xml
new file mode 100644
index 00000000000..07cd2792f37
--- /dev/null
+++ b/inputFiles/coverageSmokes/TFrac_small_mesh.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/ThermoPoroDruckerPrager_fast_events.xml b/inputFiles/coverageSmokes/ThermoPoroDruckerPrager_fast_events.xml
new file mode 100644
index 00000000000..dd9f308d29f
--- /dev/null
+++ b/inputFiles/coverageSmokes/ThermoPoroDruckerPrager_fast_events.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/ThermoPoroElastic_staircase_small_mesh.xml b/inputFiles/coverageSmokes/ThermoPoroElastic_staircase_small_mesh.xml
new file mode 100644
index 00000000000..7d5360a484f
--- /dev/null
+++ b/inputFiles/coverageSmokes/ThermoPoroElastic_staircase_small_mesh.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/acouselas3D_Q2_small_mesh.xml b/inputFiles/coverageSmokes/acouselas3D_Q2_small_mesh.xml
new file mode 100644
index 00000000000..ddbc662b11e
--- /dev/null
+++ b/inputFiles/coverageSmokes/acouselas3D_Q2_small_mesh.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/co2_flux_3d_small_mesh.xml b/inputFiles/coverageSmokes/co2_flux_3d_small_mesh.xml
new file mode 100644
index 00000000000..c2c11cb066d
--- /dev/null
+++ b/inputFiles/coverageSmokes/co2_flux_3d_small_mesh.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/co2_flux_dirichlet_small_mesh.xml b/inputFiles/coverageSmokes/co2_flux_dirichlet_small_mesh.xml
new file mode 100644
index 00000000000..928f8df3a33
--- /dev/null
+++ b/inputFiles/coverageSmokes/co2_flux_dirichlet_small_mesh.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/co2_thermal_obl_small_mesh.xml b/inputFiles/coverageSmokes/co2_thermal_obl_small_mesh.xml
new file mode 100644
index 00000000000..999234923e1
--- /dev/null
+++ b/inputFiles/coverageSmokes/co2_thermal_obl_small_mesh.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/deadOilEgg_small_mesh.xml b/inputFiles/coverageSmokes/deadOilEgg_small_mesh.xml
new file mode 100644
index 00000000000..a896c6a8093
--- /dev/null
+++ b/inputFiles/coverageSmokes/deadOilEgg_small_mesh.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/deadOil_edfm_small_mesh.xml b/inputFiles/coverageSmokes/deadOil_edfm_small_mesh.xml
new file mode 100644
index 00000000000..27d9f54e31c
--- /dev/null
+++ b/inputFiles/coverageSmokes/deadOil_edfm_small_mesh.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/fault_contact_coverage.vtu b/inputFiles/coverageSmokes/fault_contact_coverage.vtu
new file mode 100644
index 00000000000..d2b23917110
--- /dev/null
+++ b/inputFiles/coverageSmokes/fault_contact_coverage.vtu
@@ -0,0 +1,1495 @@
+
+
+
+
+
+
+ 1 12 14 15 16 17
+ 63 64 119 147 475 476
+ 477 478 479 480 481 482
+ 483 484 485 486 487 488
+ 489 490 491 492 493 494
+ 495 496 497 498 499 500
+ 501 502 503 531 532 560
+ 588 589 590 591 592 593
+ 594 595 596 597 598 599
+ 600 601 602 603 604 605
+ 606 607 608 609 610 611
+ 612 613 614 615 616 617
+ 618 619 620 621 622 623
+ 624 625 626 627 628 629
+ 630 631 632 633 634 635
+ 636 637 638 639 640 641
+ 642 643 644 645 646 647
+ 648 649 650 651 652 653
+ 654 655 656 657 658 659
+ 660 661 662 663 664 665
+ 666 667 668 669 670 671
+ 672 673 674 675 676 677
+ 678 679 680 681 682 683
+ 684 685 686 687 688 689
+ 690 691 692 693 694 695
+ 696 697 698 699 700 701
+ 702 703 704 705 706 707
+ 708 709 737 738 766 794
+ 823 852 938 1024 1130 1245
+ 1653 1681 1709 1737 1765 1793
+ 1821 1849 1877 1905 1933 1961
+ 1989 2017 2045 2073 2101 2129
+ 2157 2185 2213 2241 2269 2297
+ 2325 2353 2381 2409 2437 2465
+ 2493 2521 2549 2577 2605 2633
+ 2661 2689 2717 2745 2773 2801
+ 2829 2857 2885 2913 2941 2969
+ 2997 3025 3053 3081 3109 3137
+ 3165 3193 3221 3249 3277 3305
+ 3333 3361 3389 3417 3445 3473
+ 3501 3529 3557 3585 3613 3641
+ 3669 3697 3725 3753 3781 3809
+ 3837 3865 3893 3921 3949 3977
+ 4005 4033 4061 4089 4117 4145
+ 4173 4201 4229 4257 4285 4313
+ 4341 4369 4397 4425 4453 4481
+ 4509 4537 4565 4593 4621 4649
+ 4677 4705 4733 4761 4789 4817
+ 4845 4873 4901 4929 4957 4985
+ 5013 5041 5069 5097 5125 5153
+ 5181 5209 5237 5265 5293 5321
+ 5349 5377 5405 5433 5461 5489
+ 5517 5545 5573 5601 5629 5657
+ 5685 5713 5741 5769 5797 5825
+ 5826 5854 5882 5910 5938 5966
+ 5994 6022 6050 6078 6106 6134
+ 6162 6190 6218 6246 6274 6302
+ 6330 6358 6386 6414 6442 6470
+ 6498 6526 6554 6582 6637 6665
+ 6693 6721 6749 6777 6805 6833
+ 6861 6889 6917 6945 6973 7001
+ 7029 7057 7085 7113 7141 7169
+ 7197 7225 7253 7281 7309 7337
+ 7365 7393 7394 7422 7450 7478
+ 7506 7534 7562 7590 7618 7646
+ 7674 7702 7730 7758 7786 7814
+ 7842 7870 7898 7926 7954 7982
+ 8010 8038 8066 8094 8122 8150
+ 8178 8206 8234 8262 8290 8318
+ 8346 8374 8402 8430 8458 8486
+ 8514 8542 8570 8598 8626 8654
+ 8682 8710 8738 8766 8794 8822
+ 8850 8878 8906 8934 8962 8990
+ 9018 9046 9074 9102 9130 9158
+ 9186 9214 9242 9270 9298 9326
+ 9354 9382 9410 9438 9466 9494
+ 9522 9550 9578 9606 9634 9662
+ 9690 9718 9746 9774 9802 9830
+ 9858 9886 9914 9942 9970 9998
+ 10053 10054 10055 10056 10057 10058
+ 10059 10060 10061 10062 10063 10064
+ 10065 10066 10067 10068 10069 10070
+ 10071 10072 10073 10074 10075 10076
+ 10077 10078 10079 10080 10081 10109
+ 10922 10923 10924 10925 10926 10927
+ 10928 10929 10930 10931 10932 10933
+ 10934 10935 10936 10937 10938 10939
+ 10940 10941 10942 10943 10944 10945
+ 10946 10947 10948 10949 10977 11790
+ 11791 11792 11793 11794 11795 11796
+ 11797 11798 11799 11800 11801 11802
+ 11803 11804 11805 11806 11807 11808
+ 11809 11810 11811 11812 11813 11814
+ 11815 11816 11817 11818 11819 11820
+ 11821 11822 11823 11824 11825 11826
+ 11827 11855 12958 12959 12960 12961
+ 12962 12963 12964 12965 12966 12967
+ 12968 12969 12970 12971 12972 12973
+ 12974 12975 12976 12977 12978 12979
+ 12980 12981 12982 12983 12984 12985
+ 13013 13881 13882 13883 13884 13885
+ 13886 13887 13888 13889 13890 13891
+ 13892 13893 13894 13895 13896 13897
+ 13898 13899 13900 13901 13902 13903
+ 13904 13905 13906 13907 13908 13909
+ 14694 14750 15590 16402 17532 18372
+ 19968 19969 19970 19971 19972 19973
+ 19974 19975 19976 19977 19978 19979
+ 19980 19981 19982 19983 19984 19985
+ 19986 19987 19988 19989 19990 19991
+ 19992 19993 19994 19995 20752 20753
+ 20754 20755 20756 20757 20758 20759
+ 20760 20761 20762 20763 20764 20765
+ 20766 20767 20768 20769 20770 20771
+ 20772 20773 20774 20775 20776 20777
+ 20778 20779 21806 21807 21808 21809
+ 21810 21811 21812 21813 21814 21815
+ 21816 21817 21818 21819 21820 21821
+ 21822 21823 21824 21825 21826 21827
+ 21828 21829 21830 21831 21832 21833
+ 21834 21835 21836 21837 21838 21839
+ 21840 21841 21842 21843 22600 22601
+ 22602 22603 22604 22605 22606 22607
+ 22608 22609 22610 22611 22612 22613
+ 22614 22615 22616 22617 22618 22619
+ 22620 22621 22622 22623 22624 22625
+ 22626 22627 23384 23385 23386 23387
+ 23388 23389 23390 23391 23392 23393
+ 23394 23395 23396 23397 23398 23399
+ 23400 23401 23402 23403 23404 23405
+ 23406 23407 23408 23409 23410 23411
+ 23412 23413 23414 23415 23416 23417
+ 23418 23419 23420 23421 23422 23423
+ 23424 23425 23426 23427 23428 23429
+ 23430 23431 23432 23433 23434 23435
+ 23436 23437 23438 23439 24223 24251
+ 24279 24307 24335 24363 24391 24419
+ 24447 24475 24503 24531 24559 24587
+ 24615 24643 24671 24699 24727 24755
+ 24783 24811 24839 24867 24895 24923
+ 24951 24979 24980 24981 24982 24983
+ 24984 24985 24986 24987 24988 24989
+ 24990 24991 24992 24993 24994 24995
+ 24996 24997 24998 24999 25000 25001
+ 25002 25003 25004 25005 25006 25007
+ 25008 25009 25010 25011 25012 25013
+ 25014 25015 25016 25017 26044 26045
+ 26046 26047 26048 26049 26050 26051
+ 26052 26053 26054 26055 26056 26057
+ 26058 26059 26060 26061 26062 26063
+ 26064 26065 26066 26067 26068 26069
+ 26070 26071 26828 26829 26830 26831
+ 26832 26833 26834 26835 26836 26837
+ 26838 26839 26840 26841 26842 26843
+ 26844 26845 26846 26847 26848 26849
+ 26850 26851 26852 26853 26854 26855
+
+
+ 1 1 1 1 1 1
+ 0 0 0 0 0 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 0 0 0
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 0 0 0 0
+ 1 1 1 1 1 1
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 0
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 0 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 0 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 0 0 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+ 0 0 0 0 0 0
+
+
+
+
+ 95 95 95 95 95 95
+ 95 95 95 95 95 95
+ 95 95 95 95 95 95
+ 95 95 95 95 95 95
+ 95 95 95 95 95 96
+ 96 96 96 96 96 96
+ 96 96 96 96 96 96
+ 96 96 96 96 96 96
+ 96 96 96 96 96 96
+ 96 96 96 96 96 96
+ 96 96 96 96 96 96
+ 96 96 96 96 96 96
+ 96 96 96 96 96 96
+ 96 96 96 96 96 96
+ 96 96 96 96 96 96
+ 96 96 96 96 96 96
+ 96 94 94 94 94 94
+ 94 94 94 94 94 94
+ 94 94 94 94 94 94
+ 94 94 94 94 94 94
+ 94 94 94 94 94 94
+ 94 94 94 94 94 94
+ 94 94 94 94 94 94
+ 94 94 94 94 94 94
+ 94 94 94 94 94 94
+ 94 94 94 94 94 94
+ 94 94 94 94 94 94
+ 94 94 94 94 94 94
+ 94 94 94 94 94 94
+ 94 94 94 94 94 94
+ 94 94 94 94 97 97
+ 97 97 97 97 97 97
+ 97 97 97 97 97 97
+ 97 97 97 97 97 97
+ 97 97 97 97 97 97
+ 97 97 97 97 97 97
+ 97 97 97 97 97 97
+ 97 97 97 97 97 97
+ 97 97 97 97 97 97
+ 97 97 97 97 97 97
+ 97 97 97 97 97 97
+ 97 97 97 97 97 97
+ 95 95 95 95 95 95
+ 95 95 95 95 95 95
+ 95 95 95 95 95 95
+ 95 95 95 95 95 95
+ 95 95 95 95 95 95
+ 95 95 95 95 95 95
+ 95 95 95 95 95 95
+ 95 95 95 95 95 95
+ 95 95 95 95 95 95
+ 95 95 95 95
+
+
+ 1653 1654 1655 1656 1657 1658
+ 1659 1660 1661 1662 1663 1664
+ 1665 1666 1667 1668 1669 1670
+ 1671 1672 1673 1674 1675 1676
+ 1677 1678 1679 1680 1681 3335
+ 3336 3337 3338 3339 3340 3341
+ 3342 3343 3344 3345 3346 3347
+ 3348 3349 3350 3351 3352 3353
+ 3354 3355 3356 3357 3358 3359
+ 3360 3361 3362 3363 5587 5588
+ 5589 5590 5591 5592 5593 5594
+ 5595 5596 5597 5598 5599 5600
+ 5601 5602 5603 5604 5605 5606
+ 5607 5608 5609 5610 5611 5612
+ 5613 5614 5615 5616 5617 5618
+ 5619 5620 5621 5622 5623 5624
+ 5625 7279 7280 7281 7282 7283
+ 7284 7285 7286 7287 7288 7289
+ 7290 7291 7292 7293 7294 7295
+ 7296 7297 7298 7299 7300 7301
+ 7302 7303 7304 7305 7306 7307
+ 8961 8962 8963 8964 8965 8966
+ 8967 8968 8969 8970 8971 8972
+ 8973 8974 8975 8976 8977 8978
+ 8979 8980 8981 8982 8983 8984
+ 8985 8986 8987 8988 8989 9831
+ 9832 9833 9834 9835 9836 9837
+ 9838 9839 9840 9841 9842 9843
+ 9844 9845 9846 9847 9848 9849
+ 9850 9851 9852 9853 9854 9855
+ 9856 9857 9858 9859 10729 10787
+ 10845 10903 10961 11019 11077 11135
+ 11193 11251 11309 11367 11425 11483
+ 11541 11599 11657 11715 11773 11831
+ 11889 11947 12005 12063 12121 12179
+ 12237 12295 12353 13485 13486 13487
+ 13488 13489 13490 13491 13492 13493
+ 13494 13495 13496 13497 13498 13499
+ 13500 13501 13502 13503 13504 13505
+ 13506 13507 13508 13509 13510 13511
+ 13512 13513 13514 13515 13516 13517
+ 13518 13519 13520 13521 13522 13523
+ 15457 15458 15459 15460 15461 15462
+ 15463 15464 15465 15466 15467 15468
+ 15469 15470 15471 15472 15473 15474
+ 15475 15476 15477 15478 15479 15480
+ 15481 15482 15483 15484 15485 17139
+ 17140 17141 17142 17143 17144 17145
+ 17146 17147 17148 17149 17150 17151
+ 17152 17153 17154 17155 17156 17157
+ 17158 17159 17160 17161 17162 17163
+ 17164 17165 17166 17167
+
+
+ 1653 1654 1655 1656 1657 1658
+ 1659 1660 1661 1662 1663 1664
+ 1665 1666 1667 1668 1669 1670
+ 1671 1672 1673 1674 1675 1676
+ 1677 1678 1679 1680 1681 3335
+ 3336 3337 3338 3339 3340 3341
+ 3342 3343 3344 3345 3346 3347
+ 3348 3349 3350 3351 3352 3353
+ 3354 3355 3356 3357 3358 3359
+ 3360 3361 3362 3363 5587 5588
+ 5589 5590 5591 5592 5593 5594
+ 5595 5596 5597 5598 5599 5600
+ 5601 5602 5603 5604 5605 5606
+ 5607 5608 5609 5610 5611 5612
+ 5613 5614 5615 5616 5617 5618
+ 5619 5620 5621 5622 5623 5624
+ 5625 7279 7280 7281 7282 7283
+ 7284 7285 7286 7287 7288 7289
+ 7290 7291 7292 7293 7294 7295
+ 7296 7297 7298 7299 7300 7301
+ 7302 7303 7304 7305 7306 7307
+ 8961 8962 8963 8964 8965 8966
+ 8967 8968 8969 8970 8971 8972
+ 8973 8974 8975 8976 8977 8978
+ 8979 8980 8981 8982 8983 8984
+ 8985 8986 8987 8988 8989 9831
+ 9832 9833 9834 9835 9836 9837
+ 9838 9839 9840 9841 9842 9843
+ 9844 9845 9846 9847 9848 9849
+ 9850 9851 9852 9853 9854 9855
+ 9856 9857 9858 9859 10729 10787
+ 10845 10903 10961 11019 11077 11135
+ 11193 11251 11309 11367 11425 11483
+ 11541 11599 11657 11715 11773 11831
+ 11889 11947 12005 12063 12121 12179
+ 12237 12295 12353 13485 13486 13487
+ 13488 13489 13490 13491 13492 13493
+ 13494 13495 13496 13497 13498 13499
+ 13500 13501 13502 13503 13504 13505
+ 13506 13507 13508 13509 13510 13511
+ 13512 13513 13514 13515 13516 13517
+ 13518 13519 13520 13521 13522 13523
+ 15457 15458 15459 15460 15461 15462
+ 15463 15464 15465 15466 15467 15468
+ 15469 15470 15471 15472 15473 15474
+ 15475 15476 15477 15478 15479 15480
+ 15481 15482 15483 15484 15485 17139
+ 17140 17141 17142 17143 17144 17145
+ 17146 17147 17148 17149 17150 17151
+ 17152 17153 17154 17155 17156 17157
+ 17158 17159 17160 17161 17162 17163
+ 17164 17165 17166 17167
+
+
+
+
+ -288.7 -7.566675e-14 -3500 288.7 7.566675e-14 -2500
+ 115.48 3.02667e-14 -2800 57.74 1.513335e-14 -2900
+ -57.74 -1.513335e-14 -3100 -115.48 -3.02667e-14 -3200
+ -276.1301 -7.720612e-14 -3500 -300.21375 -7.425672e-14 -3500
+ 298.09875 7.451574e-14 -2500 273.30176 7.755249e-14 -2500
+ 125.83009 2.899918e-14 -2800 282.7269 7.410123e-14 -2510.3447
+ 276.75378 7.2535714e-14 -2520.6897 270.7807 7.0970194e-14 -2531.0344
+ 264.8076 6.9404674e-14 -2541.3794 258.83447 6.783915e-14 -2551.724
+ 252.86137 6.627363e-14 -2562.0688 246.88828 6.470812e-14 -2572.4138
+ 240.91518 6.31426e-14 -2582.7585 234.94206 6.157708e-14 -2593.1035
+ 228.96896 6.001156e-14 -2603.4482 222.99586 5.844604e-14 -2613.7932
+ 217.02277 5.688052e-14 -2624.138 211.04965 5.5315002e-14 -2634.4827
+ 205.07655 5.3749485e-14 -2644.8276 199.10345 5.2183965e-14 -2655.1724
+ 193.13034 5.0618445e-14 -2665.5173 187.15724 4.9052928e-14 -2675.862
+ 181.18414 4.7487408e-14 -2686.2068 175.21103 4.5921888e-14 -2696.5518
+ 169.23793 4.435637e-14 -2706.8965 163.26483 4.279085e-14 -2717.2415
+ 157.29172 4.1225334e-14 -2727.5862 151.31862 3.9659814e-14 -2737.9312
+ 145.34552 3.8094294e-14 -2748.276 139.37242 3.6528777e-14 -2758.6206
+ 133.3993 3.4963257e-14 -2768.9656 127.42621 3.3397737e-14 -2779.3103
+ 121.4531 3.183222e-14 -2789.6553 68.40721 1.3826993e-14 -2900
+ -70.80767 -1.3533022e-14 -3100 -128.1592 -2.8713946e-14 -3200
+ 113.48897 2.974486e-14 -2803.4482 111.49793 2.922302e-14 -2806.8965
+ 109.5069 2.870118e-14 -2810.3447 107.51586 2.8179341e-14 -2813.7932
+ 105.524826 2.7657501e-14 -2817.2415 103.53379 2.7135661e-14 -2820.6897
+ 101.542755 2.6613823e-14 -2824.138 99.55173 2.6091983e-14 -2827.5862
+ 97.56069 2.5570143e-14 -2831.0344 95.56966 2.5048302e-14 -2834.4827
+ 93.57862 2.4526464e-14 -2837.9312 91.587585 2.4004624e-14 -2841.3794
+ 89.59655 2.3482784e-14 -2844.8276 87.605515 2.2960944e-14 -2848.276
+ 85.61448 2.2439106e-14 -2851.724 83.62345 2.1917266e-14 -2855.1724
+ 81.632416 2.1395425e-14 -2858.6206 79.64138 2.0873585e-14 -2862.0688
+ 77.650345 2.0351747e-14 -2865.5173 75.65931 1.9829907e-14 -2868.9656
+ 73.668274 1.9308067e-14 -2872.4138 71.67724 1.8786227e-14 -2875.862
+ 69.68621 1.8264388e-14 -2879.3103 67.695175 1.7742548e-14 -2882.7585
+ 65.70414 1.7220708e-14 -2886.2068 63.713104 1.6698868e-14 -2889.6553
+ 61.72207 1.617703e-14 -2893.1035 59.731033 1.565519e-14 -2896.5518
+ 54.778973 1.4357281e-14 -2905.1282 51.817947 1.3581211e-14 -2910.2563
+ 48.856922 1.28051426e-14 -2915.3845 45.895897 1.2029073e-14 -2920.513
+ 42.93487 1.1253004e-14 -2925.641 39.973846 1.04769344e-14 -2930.7693
+ 37.01282 9.700865e-15 -2935.8975 34.051796 8.924796e-15 -2941.0256
+ 31.090769 8.148727e-15 -2946.1538 28.129744 7.372658e-15 -2951.282
+ 25.168718 6.5965884e-15 -2956.4102 22.207693 5.820519e-15 -2961.5386
+ 19.246666 5.04445e-15 -2966.6667 16.28564 4.2683807e-15 -2971.795
+ 13.3246155 3.4923115e-15 -2976.923 10.363589 2.7162424e-15 -2982.0513
+ 7.402564 1.940173e-15 -2987.1794 4.4415383 1.1641038e-15 -2992.3076
+ 1.4805129 3.880346e-16 -2997.4358 -1.4805129 -3.880346e-16 -3002.5642
+ -4.4415383 -1.1641038e-15 -3007.6924 -7.402564 -1.940173e-15 -3012.8206
+ -10.363589 -2.7162424e-15 -3017.9487 -13.3246155 -3.4923115e-15 -3023.077
+ -16.28564 -4.2683807e-15 -3028.205 -19.246666 -5.04445e-15 -3033.3333
+ -22.207693 -5.820519e-15 -3038.4614 -25.168718 -6.5965884e-15 -3043.5898
+ -28.129744 -7.372658e-15 -3048.718 -31.090769 -8.148727e-15 -3053.8462
+ -34.051796 -8.924796e-15 -3058.9744 -37.01282 -9.700865e-15 -3064.1025
+ -39.973846 -1.04769344e-14 -3069.2307 -42.93487 -1.1253004e-14 -3074.359
+ -45.895897 -1.2029073e-14 -3079.487 -48.856922 -1.28051426e-14 -3084.6155
+ -51.817947 -1.3581211e-14 -3089.7437 -54.778973 -1.4357281e-14 -3094.8718
+ -59.731033 -1.565519e-14 -3103.4482 -61.72207 -1.617703e-14 -3106.8965
+ -63.713104 -1.6698868e-14 -3110.3447 -65.70414 -1.7220708e-14 -3113.7932
+ -67.695175 -1.7742548e-14 -3117.2415 -69.68621 -1.8264388e-14 -3120.6897
+ -71.67724 -1.8786227e-14 -3124.138 -73.668274 -1.9308067e-14 -3127.5862
+ -75.65931 -1.9829907e-14 -3131.0344 -77.650345 -2.0351747e-14 -3134.4827
+ -79.64138 -2.0873585e-14 -3137.9312 -81.632416 -2.1395425e-14 -3141.3794
+ -83.62345 -2.1917266e-14 -3144.8276 -85.61448 -2.2439106e-14 -3148.276
+ -87.605515 -2.2960944e-14 -3151.724 -89.59655 -2.3482784e-14 -3155.1724
+ -91.587585 -2.4004624e-14 -3158.6206 -93.57862 -2.4526464e-14 -3162.0688
+ -95.56966 -2.5048302e-14 -3165.5173 -97.56069 -2.5570143e-14 -3168.9656
+ -99.55173 -2.6091983e-14 -3172.4138 -101.542755 -2.6613823e-14 -3175.862
+ -103.53379 -2.7135661e-14 -3179.3103 -105.524826 -2.7657501e-14 -3182.7585
+ -107.51586 -2.8179341e-14 -3186.2068 -109.5069 -2.870118e-14 -3189.6553
+ -111.49793 -2.922302e-14 -3193.1035 -113.48897 -2.974486e-14 -3196.5518
+ -121.4531 -3.183222e-14 -3210.3447 -127.42621 -3.3397737e-14 -3220.6897
+ -133.3993 -3.4963257e-14 -3231.0344 -139.37242 -3.6528777e-14 -3241.3794
+ -145.34552 -3.8094294e-14 -3251.724 -151.31862 -3.9659814e-14 -3262.0688
+ -157.29172 -4.1225334e-14 -3272.4138 -163.26483 -4.279085e-14 -3282.7585
+ -169.23793 -4.435637e-14 -3293.1035 -175.21103 -4.5921888e-14 -3303.4482
+ -181.18414 -4.7487408e-14 -3313.7932 -187.15724 -4.9052928e-14 -3324.138
+ -193.13034 -5.0618445e-14 -3334.4827 -199.10345 -5.2183965e-14 -3344.8276
+ -205.07655 -5.3749485e-14 -3355.1724 -211.04965 -5.5315002e-14 -3365.5173
+ -217.02277 -5.688052e-14 -3375.862 -222.99586 -5.844604e-14 -3386.2068
+ -228.96896 -6.001156e-14 -3396.5518 -234.94206 -6.157708e-14 -3406.8965
+ -240.91518 -6.31426e-14 -3417.2415 -246.88828 -6.470812e-14 -3427.5862
+ -252.86137 -6.627363e-14 -3437.9312 -258.83447 -6.783915e-14 -3448.276
+ -264.8076 -6.9404674e-14 -3458.6206 -270.7807 -7.0970194e-14 -3468.9656
+ -276.75378 -7.2535714e-14 -3479.3103 -282.7269 -7.410123e-14 -3489.6553
+ -46.438553 -1.6517378e-14 -3100 -103.8612 -3.1689593e-14 -3200
+ 43.89537 1.6828829e-14 -2900 101.246895 3.200975e-14 -2800
+ 288.7 1000 -2500 115.48 1000 -2800
+ 57.74 1000 -2900 -57.74 1000 -3100
+ -115.48 1000 -3200 -288.7 1000 -3500
+ 131.77039 3.0568714e-14 -2789.6553 137.7107 3.2138253e-14 -2779.3103
+ 143.65099 3.3707788e-14 -2768.9656 149.5913 3.5277323e-14 -2758.6206
+ 155.53159 3.684686e-14 -2748.276 161.47188 3.8416397e-14 -2737.9312
+ 167.41219 3.9985935e-14 -2727.5862 173.35248 4.155547e-14 -2717.2415
+ 179.29279 4.3125005e-14 -2706.8965 185.23308 4.4694544e-14 -2696.5518
+ 191.17337 4.626408e-14 -2686.2068 197.11368 4.7833617e-14 -2675.862
+ 203.05397 4.9403153e-14 -2665.5173 208.99426 5.0972688e-14 -2655.1724
+ 214.93457 5.2542226e-14 -2644.8276 220.87486 5.411176e-14 -2634.4827
+ 226.81517 5.56813e-14 -2624.138 232.75546 5.7250835e-14 -2613.7932
+ 238.69576 5.8820373e-14 -2603.4482 244.63606 6.0389905e-14 -2593.1035
+ 250.57635 6.1959444e-14 -2582.7585 256.51666 6.352898e-14 -2572.4138
+ 262.45694 6.5098514e-14 -2562.0688 268.39725 6.666805e-14 -2551.724
+ 274.33755 6.823759e-14 -2541.3794 280.27786 6.980713e-14 -2531.0344
+ 286.21814 7.137666e-14 -2520.6897 292.15845 7.29462e-14 -2510.3447
+ 70.387314 1.4350171e-14 -2896.5518 72.36741 1.4873351e-14 -2893.1035
+ 74.34751 1.539653e-14 -2889.6553 76.32761 1.5919708e-14 -2886.2068
+ 78.30771 1.6442886e-14 -2882.7585 80.28781 1.6966067e-14 -2879.3103
+ 82.26791 1.7489245e-14 -2875.862 84.24801 1.8012423e-14 -2872.4138
+ 86.2281 1.8535602e-14 -2868.9656 88.208206 1.9058782e-14 -2865.5173
+ 90.18831 1.958196e-14 -2862.0688 92.1684 2.0105138e-14 -2858.6206
+ 94.148506 2.0628319e-14 -2855.1724 96.1286 2.1151497e-14 -2851.724
+ 98.1087 2.1674675e-14 -2848.276 100.088806 2.2197854e-14 -2844.8276
+ 102.0689 2.2721034e-14 -2841.3794 104.049 2.3244212e-14 -2837.9312
+ 106.0291 2.376739e-14 -2834.4827 108.0092 2.4290569e-14 -2831.0344
+ 109.989296 2.4813749e-14 -2827.5862 111.9694 2.5336927e-14 -2824.138
+ 113.9495 2.5860106e-14 -2820.6897 115.929596 2.6383284e-14 -2817.2415
+ 117.9097 2.6906464e-14 -2813.7932 119.88979 2.7429642e-14 -2810.3447
+ 121.869896 2.795282e-14 -2806.8965 123.85 2.8476e-14 -2803.4482
+ -43.49379 -1.5739317e-14 -3094.8718 -40.549026 -1.4961256e-14 -3089.7437
+ -37.604263 -1.4183196e-14 -3084.6155 -34.6595 -1.3405135e-14 -3079.487
+ -31.714737 -1.2627074e-14 -3074.359 -28.769974 -1.18490135e-14 -3069.2307
+ -25.82521 -1.1070953e-14 -3064.1025 -22.880447 -1.0292892e-14 -3058.9744
+ -19.935684 -9.514831e-15 -3053.8462 -16.990921 -8.7367705e-15 -3048.718
+ -14.046157 -7.958709e-15 -3043.5898 -11.101394 -7.180648e-15 -3038.4614
+ -8.1566305 -6.402588e-15 -3033.3333 -5.211868 -5.624527e-15 -3028.205
+ -2.2671044 -4.846466e-15 -3023.077 0.6776588 -4.068405e-15 -3017.9487
+ 3.622422 -3.2903444e-15 -3012.8206 6.5671854 -2.5122836e-15 -3007.6924
+ 9.511949 -1.7342229e-15 -3002.5642 12.456712 -9.56162e-16 -2997.4358
+ 15.401475 -1.7810126e-16 -2992.3076 18.346237 5.999595e-16 -2987.1794
+ 21.291 1.3780203e-15 -2982.0513 24.235764 2.156081e-15 -2976.923
+ 27.180527 2.934142e-15 -2971.795 30.12529 3.7122028e-15 -2966.6667
+ 33.070053 4.4902635e-15 -2961.5386 36.014816 5.2683243e-15 -2956.4102
+ 38.95958 6.046385e-15 -2951.282 41.904343 6.8244458e-15 -2946.1538
+ 44.849106 7.602507e-15 -2941.0256 47.79387 8.380568e-15 -2935.8975
+ 50.738632 9.1586284e-15 -2930.7693 53.683395 9.936689e-15 -2925.641
+ 56.62816 1.071475e-14 -2920.513 59.57292 1.1492811e-14 -2915.3845
+ 62.517685 1.22708714e-14 -2910.2563 65.46245 1.3048932e-14 -2905.1282
+ -101.8811 -3.116641e-14 -3196.5518 -99.90102 -3.0643233e-14 -3193.1035
+ -97.92092 -3.0120055e-14 -3189.6553 -95.940834 -2.9596873e-14 -3186.2068
+ -93.96074 -2.9073694e-14 -3182.7585 -91.98065 -2.8550513e-14 -3179.3103
+ -90.00056 -2.8027334e-14 -3175.862 -88.02047 -2.7504154e-14 -3172.4138
+ -86.040375 -2.6980974e-14 -3168.9656 -84.06029 -2.6457794e-14 -3165.5173
+ -82.08019 -2.5934614e-14 -3162.0688 -80.100105 -2.5411436e-14 -3158.6206
+ -78.12001 -2.4888256e-14 -3155.1724 -76.13992 -2.4365076e-14 -3151.724
+ -74.15983 -2.3841895e-14 -3148.276 -72.17974 -2.3318715e-14 -3144.8276
+ -70.199646 -2.2795535e-14 -3141.3794 -68.21956 -2.2272355e-14 -3137.9312
+ -66.23946 -2.1749177e-14 -3134.4827 -64.25938 -2.1225997e-14 -3131.0344
+ -62.27928 -2.0702817e-14 -3127.5862 -60.29919 -2.0179637e-14 -3124.138
+ -58.3191 -1.9656457e-14 -3120.6897 -56.33901 -1.9133277e-14 -3117.2415
+ -54.358917 -1.8610097e-14 -3113.7932 -52.378826 -1.8086918e-14 -3110.3447
+ -50.398735 -1.7563738e-14 -3106.8965 -48.418644 -1.7040558e-14 -3103.4482
+ -270.1898 -7.5636586e-14 -3489.6553 -264.24948 -7.406705e-14 -3479.3103
+ -258.30917 -7.2497516e-14 -3468.9656 -252.36885 -7.092798e-14 -3458.6206
+ -246.42854 -6.9358446e-14 -3448.276 -240.48825 -6.778891e-14 -3437.9312
+ -234.54794 -6.6219376e-14 -3427.5862 -228.60764 -6.464984e-14 -3417.2415
+ -222.66733 -6.30803e-14 -3406.8965 -216.72702 -6.151077e-14 -3396.5518
+ -210.78671 -5.994123e-14 -3386.2068 -204.8464 -5.83717e-14 -3375.862
+ -198.9061 -5.6802158e-14 -3365.5173 -192.96579 -5.5232623e-14 -3355.1724
+ -187.02548 -5.3663088e-14 -3344.8276 -181.08517 -5.2093553e-14 -3334.4827
+ -175.14487 -5.0524018e-14 -3324.138 -169.20457 -4.8954483e-14 -3313.7932
+ -163.26427 -4.7384948e-14 -3303.4482 -157.32396 -4.5815412e-14 -3293.1035
+ -151.38365 -4.4245877e-14 -3282.7585 -145.44334 -4.2676342e-14 -3272.4138
+ -139.50304 -4.1106804e-14 -3262.0688 -133.56273 -3.953727e-14 -3251.724
+ -127.62242 -3.7967733e-14 -3241.3794 -121.68211 -3.63982e-14 -3231.0344
+ -115.74181 -3.4828663e-14 -3220.6897 -109.801506 -3.3259128e-14 -3210.3447
+ -294.28085 -7.268628e-14 -3489.6553 -288.34793 -7.111584e-14 -3479.3103
+ -282.415 -6.9545396e-14 -3468.9656 -276.4821 -6.7974956e-14 -3458.6206
+ -270.54916 -6.640452e-14 -3448.276 -264.61627 -6.483408e-14 -3437.9312
+ -258.68335 -6.326364e-14 -3427.5862 -252.75043 -6.16932e-14 -3417.2415
+ -246.8175 -6.012276e-14 -3406.8965 -240.8846 -5.855232e-14 -3396.5518
+ -234.95168 -5.698188e-14 -3386.2068 -229.01877 -5.5411435e-14 -3375.862
+ -223.08585 -5.3840995e-14 -3365.5173 -217.15294 -5.2270556e-14 -3355.1724
+ -211.22002 -5.0700113e-14 -3344.8276 -205.2871 -4.9129673e-14 -3334.4827
+ -199.35419 -4.7559233e-14 -3324.138 -193.42126 -4.5988793e-14 -3313.7932
+ -187.48836 -4.441835e-14 -3303.4482 -181.55544 -4.284791e-14 -3293.1035
+ -175.62251 -4.127747e-14 -3282.7585 -169.6896 -3.970703e-14 -3272.4138
+ -163.75668 -3.813659e-14 -3262.0688 -157.82378 -3.6566148e-14 -3251.724
+ -151.89085 -3.499571e-14 -3241.3794 -145.95795 -3.342527e-14 -3231.0344
+ -140.02502 -3.185483e-14 -3220.6897 -134.0921 -3.0284386e-14 -3210.3447
+ -126.18156 -2.8190466e-14 -3196.5518 -124.20392 -2.7666986e-14 -3193.1035
+ -122.22627 -2.7143506e-14 -3189.6553 -120.248634 -2.6620026e-14 -3186.2068
+ -118.270996 -2.6096546e-14 -3182.7585 -116.29336 -2.5573065e-14 -3179.3103
+ -114.31572 -2.5049585e-14 -3175.862 -112.33808 -2.4526105e-14 -3172.4138
+ -110.36044 -2.4002625e-14 -3168.9656 -108.382805 -2.3479145e-14 -3165.5173
+ -106.40517 -2.2955665e-14 -3162.0688 -104.42753 -2.2432185e-14 -3158.6206
+ -102.44989 -2.1908704e-14 -3155.1724 -100.47225 -2.1385224e-14 -3151.724
+ -98.49461 -2.0861744e-14 -3148.276 -96.516975 -2.0338264e-14 -3144.8276
+ -94.53934 -1.9814784e-14 -3141.3794 -92.5617 -1.9291304e-14 -3137.9312
+ -90.58406 -1.8767824e-14 -3134.4827 -88.60642 -1.8244343e-14 -3131.0344
+ -86.62878 -1.7720863e-14 -3127.5862 -84.65114 -1.7197383e-14 -3124.138
+ -82.6735 -1.6673903e-14 -3120.6897 -80.69586 -1.6150423e-14 -3117.2415
+ -78.71822 -1.5626943e-14 -3113.7932 -76.740585 -1.5103461e-14 -3110.3447
+ -74.76295 -1.4579982e-14 -3106.8965 -72.78531 -1.4056502e-14 -3103.4482
+ -67.86657 -1.2754513e-14 -3094.8718 -64.92546 -1.1976004e-14 -3089.7437
+ -61.98436 -1.1197495e-14 -3084.6155 -59.043255 -1.04189855e-14 -3079.487
+ -56.102154 -9.640477e-15 -3074.359 -53.16105 -8.861968e-15 -3069.2307
+ -50.219944 -8.083459e-15 -3064.1025 -47.278843 -7.30495e-15 -3058.9744
+ -44.337738 -6.526441e-15 -3053.8462 -41.396633 -5.747932e-15 -3048.718
+ -38.455532 -4.9694233e-15 -3043.5898 -35.514427 -4.190914e-15 -3038.4614
+ -32.573322 -3.4124052e-15 -3033.3333 -29.63222 -2.6338962e-15 -3028.205
+ -26.691116 -1.8553873e-15 -3023.077 -23.750013 -1.0768783e-15 -3017.9487
+ -20.808908 -2.9836931e-16 -3012.8206 -17.867805 4.8013967e-16 -3007.6924
+ -14.926702 1.2586486e-15 -3002.5642 -11.985599 2.0371576e-15 -2997.4358
+ -9.044495 2.8156665e-15 -2992.3076 -6.103391 3.5941755e-15 -2987.1794
+ -3.1622872 4.3726843e-15 -2982.0513 -0.22118369 5.1511936e-15 -2976.923
+ 2.71992 5.9297024e-15 -2971.795 5.6610236 6.7082113e-15 -2966.6667
+ 8.602127 7.48672e-15 -2961.5386 11.543231 8.265229e-15 -2956.4102
+ 14.484335 9.043739e-15 -2951.282 17.425438 9.822247e-15 -2946.1538
+ 20.36654 1.0600756e-14 -2941.0256 23.307646 1.1379265e-14 -2935.8975
+ 26.248749 1.2157774e-14 -2930.7693 29.189852 1.2936283e-14 -2925.641
+ 32.130955 1.3714792e-14 -2920.513 35.07206 1.4493301e-14 -2915.3845
+ 38.013165 1.527181e-14 -2910.2563 40.954266 1.6050319e-14 -2905.1282
+ 45.87301 1.7352308e-14 -2896.5518 47.850647 1.7875788e-14 -2893.1035
+ 49.828285 1.8399268e-14 -2889.6553 51.805927 1.8922748e-14 -2886.2068
+ 53.783566 1.9446228e-14 -2882.7585 55.761204 1.996971e-14 -2879.3103
+ 57.738842 2.049319e-14 -2875.862 59.71648 2.101667e-14 -2872.4138
+ 61.69412 2.154015e-14 -2868.9656 63.671757 2.206363e-14 -2865.5173
+ 65.6494 2.258711e-14 -2862.0688 67.62704 2.311059e-14 -2858.6206
+ 69.604675 2.363407e-14 -2855.1724 71.58231 2.415755e-14 -2851.724
+ 73.55995 2.468103e-14 -2848.276 75.53759 2.520451e-14 -2844.8276
+ 77.51523 2.572799e-14 -2841.3794 79.49287 2.625147e-14 -2837.9312
+ 81.470505 2.677495e-14 -2834.4827 83.44814 2.7298432e-14 -2831.0344
+ 85.42578 2.7821912e-14 -2827.5862 87.40342 2.8345392e-14 -2824.138
+ 89.38106 2.886887e-14 -2820.6897 91.358696 2.939235e-14 -2817.2415
+ 93.336334 2.991583e-14 -2813.7932 95.31397 3.043931e-14 -2810.3447
+ 97.29162 3.096279e-14 -2806.8965 99.26926 3.148627e-14 -2803.4482
+ 107.17982 3.358019e-14 -2789.6553 113.11275 3.515063e-14 -2779.3103
+ 119.04567 3.672107e-14 -2768.9656 124.9786 3.829151e-14 -2758.6206
+ 130.91153 3.986195e-14 -2748.276 136.84445 4.143239e-14 -2737.9312
+ 142.77737 4.3002826e-14 -2727.5862 148.71031 4.4573266e-14 -2717.2415
+ 154.64323 4.6143705e-14 -2706.8965 160.57616 4.7714145e-14 -2696.5518
+ 166.50908 4.9284585e-14 -2686.2068 172.44202 5.0855025e-14 -2675.862
+ 178.37494 5.242546e-14 -2665.5173 184.30786 5.39959e-14 -2655.1724
+ 190.24078 5.556634e-14 -2644.8276 196.17372 5.713678e-14 -2634.4827
+ 202.10664 5.8707217e-14 -2624.138 208.03957 6.0277656e-14 -2613.7932
+ 213.9725 6.1848096e-14 -2603.4482 219.90543 6.3418536e-14 -2593.1035
+ 225.83835 6.4988976e-14 -2582.7585 231.77127 6.6559415e-14 -2572.4138
+ 237.70421 6.8129855e-14 -2562.0688 243.63713 6.9700295e-14 -2551.724
+ 249.57005 7.1270735e-14 -2541.3794 255.50298 7.2841174e-14 -2531.0344
+ 261.4359 7.4411614e-14 -2520.6897 267.36884 7.5982054e-14 -2510.3447
+ 298.09875 1000 -2500 121.4531 1000 -2789.6553
+ 127.42621 1000 -2779.3103 133.3993 1000 -2768.9656
+ 139.37242 1000 -2758.6206 145.34552 1000 -2748.276
+ 151.31862 1000 -2737.9312 157.29172 1000 -2727.5862
+ 163.26483 1000 -2717.2415 169.23793 1000 -2706.8965
+ 175.21103 1000 -2696.5518 181.18414 1000 -2686.2068
+ 187.15724 1000 -2675.862 193.13034 1000 -2665.5173
+ 199.10345 1000 -2655.1724 205.07655 1000 -2644.8276
+ 211.04965 1000 -2634.4827 217.02277 1000 -2624.138
+ 222.99586 1000 -2613.7932 228.96896 1000 -2603.4482
+ 234.94206 1000 -2593.1035 240.91518 1000 -2582.7585
+ 246.88828 1000 -2572.4138 252.86137 1000 -2562.0688
+ 258.83447 1000 -2551.724 264.8076 1000 -2541.3794
+ 270.7807 1000 -2531.0344 276.75378 1000 -2520.6897
+ 282.7269 1000 -2510.3447 125.83009 1000 -2800
+ 59.731033 1000 -2896.5518 61.72207 1000 -2893.1035
+ 63.713104 1000 -2889.6553 65.70414 1000 -2886.2068
+ 67.695175 1000 -2882.7585 69.68621 1000 -2879.3103
+ 71.67724 1000 -2875.862 73.668274 1000 -2872.4138
+ 75.65931 1000 -2868.9656 77.650345 1000 -2865.5173
+ 79.64138 1000 -2862.0688 81.632416 1000 -2858.6206
+ 83.62345 1000 -2855.1724 85.61448 1000 -2851.724
+ 87.605515 1000 -2848.276 89.59655 1000 -2844.8276
+ 91.587585 1000 -2841.3794 93.57862 1000 -2837.9312
+ 95.56966 1000 -2834.4827 97.56069 1000 -2831.0344
+ 99.55173 1000 -2827.5862 101.542755 1000 -2824.138
+ 103.53379 1000 -2820.6897 105.524826 1000 -2817.2415
+ 107.51586 1000 -2813.7932 109.5069 1000 -2810.3447
+ 111.49793 1000 -2806.8965 113.48897 1000 -2803.4482
+ 68.40721 1000 -2900 -54.778973 1000 -3094.8718
+ -51.817947 1000 -3089.7437 -48.856922 1000 -3084.6155
+ -45.895897 1000 -3079.487 -42.93487 1000 -3074.359
+ -39.973846 1000 -3069.2307 -37.01282 1000 -3064.1025
+ -34.051796 1000 -3058.9744 -31.090769 1000 -3053.8462
+ -28.129744 1000 -3048.718 -25.168718 1000 -3043.5898
+ -22.207693 1000 -3038.4614 -19.246666 1000 -3033.3333
+ -16.28564 1000 -3028.205 -13.3246155 1000 -3023.077
+ -10.363589 1000 -3017.9487 -7.402564 1000 -3012.8206
+ -4.4415383 1000 -3007.6924 -1.4805129 1000 -3002.5642
+ 1.4805129 1000 -2997.4358 4.4415383 1000 -2992.3076
+ 7.402564 1000 -2987.1794 10.363589 1000 -2982.0513
+ 13.3246155 1000 -2976.923 16.28564 1000 -2971.795
+ 19.246666 1000 -2966.6667 22.207693 1000 -2961.5386
+ 25.168718 1000 -2956.4102 28.129744 1000 -2951.282
+ 31.090769 1000 -2946.1538 34.051796 1000 -2941.0256
+ 37.01282 1000 -2935.8975 39.973846 1000 -2930.7693
+ 42.93487 1000 -2925.641 45.895897 1000 -2920.513
+ 48.856922 1000 -2915.3845 51.817947 1000 -2910.2563
+ 54.778973 1000 -2905.1282 -46.438553 1000 -3100
+ -113.48897 1000 -3196.5518 -111.49793 1000 -3193.1035
+ -109.5069 1000 -3189.6553 -107.51586 1000 -3186.2068
+ -105.524826 1000 -3182.7585 -103.53379 1000 -3179.3103
+ -101.542755 1000 -3175.862 -99.55173 1000 -3172.4138
+ -97.56069 1000 -3168.9656 -95.56966 1000 -3165.5173
+ -93.57862 1000 -3162.0688 -91.587585 1000 -3158.6206
+ -89.59655 1000 -3155.1724 -87.605515 1000 -3151.724
+ -85.61448 1000 -3148.276 -83.62345 1000 -3144.8276
+ -81.632416 1000 -3141.3794 -79.64138 1000 -3137.9312
+ -77.650345 1000 -3134.4827 -75.65931 1000 -3131.0344
+ -73.668274 1000 -3127.5862 -71.67724 1000 -3124.138
+ -69.68621 1000 -3120.6897 -67.695175 1000 -3117.2415
+ -65.70414 1000 -3113.7932 -63.713104 1000 -3110.3447
+ -61.72207 1000 -3106.8965 -59.731033 1000 -3103.4482
+ -103.8612 1000 -3200 -276.1301 1000 -3500
+ -282.7269 1000 -3489.6553 -276.75378 1000 -3479.3103
+ -270.7807 1000 -3468.9656 -264.8076 1000 -3458.6206
+ -258.83447 1000 -3448.276 -252.86137 1000 -3437.9312
+ -246.88828 1000 -3427.5862 -240.91518 1000 -3417.2415
+ -234.94206 1000 -3406.8965 -228.96896 1000 -3396.5518
+ -222.99586 1000 -3386.2068 -217.02277 1000 -3375.862
+ -211.04965 1000 -3365.5173 -205.07655 1000 -3355.1724
+ -199.10345 1000 -3344.8276 -193.13034 1000 -3334.4827
+ -187.15724 1000 -3324.138 -181.18414 1000 -3313.7932
+ -175.21103 1000 -3303.4482 -169.23793 1000 -3293.1035
+ -163.26483 1000 -3282.7585 -157.29172 1000 -3272.4138
+ -151.31862 1000 -3262.0688 -145.34552 1000 -3251.724
+ -139.37242 1000 -3241.3794 -133.3993 1000 -3231.0344
+ -127.42621 1000 -3220.6897 -121.4531 1000 -3210.3447
+ -128.1592 1000 -3200 -300.21375 1000 -3500
+ -70.80767 1000 -3100 43.89537 1000 -2900
+ 101.246895 1000 -2800 273.30176 1000 -2500
+ 292.15845 1000 -2510.3447 286.21814 1000 -2520.6897
+ 280.27786 1000 -2531.0344 274.33755 1000 -2541.3794
+ 268.39725 1000 -2551.724 262.45694 1000 -2562.0688
+ 256.51666 1000 -2572.4138 250.57635 1000 -2582.7585
+ 244.63606 1000 -2593.1035 238.69576 1000 -2603.4482
+ 232.75546 1000 -2613.7932 226.81517 1000 -2624.138
+ 220.87486 1000 -2634.4827 214.93457 1000 -2644.8276
+ 208.99426 1000 -2655.1724 203.05397 1000 -2665.5173
+ 197.11368 1000 -2675.862 191.17337 1000 -2686.2068
+ 185.23308 1000 -2696.5518 179.29279 1000 -2706.8965
+ 173.35248 1000 -2717.2415 167.41219 1000 -2727.5862
+ 161.47188 1000 -2737.9312 155.53159 1000 -2748.276
+ 149.5913 1000 -2758.6206 143.65099 1000 -2768.9656
+ 137.7107 1000 -2779.3103 131.77039 1000 -2789.6553
+ 123.85 1000 -2803.4482 121.869896 1000 -2806.8965
+ 119.88979 1000 -2810.3447 117.9097 1000 -2813.7932
+ 115.929596 1000 -2817.2415 113.9495 1000 -2820.6897
+ 111.9694 1000 -2824.138 109.989296 1000 -2827.5862
+ 108.0092 1000 -2831.0344 106.0291 1000 -2834.4827
+ 104.049 1000 -2837.9312 102.0689 1000 -2841.3794
+ 100.088806 1000 -2844.8276 98.1087 1000 -2848.276
+ 96.1286 1000 -2851.724 94.148506 1000 -2855.1724
+ 92.1684 1000 -2858.6206 90.18831 1000 -2862.0688
+ 88.208206 1000 -2865.5173 86.2281 1000 -2868.9656
+ 84.24801 1000 -2872.4138 82.26791 1000 -2875.862
+ 80.28781 1000 -2879.3103 78.30771 1000 -2882.7585
+ 76.32761 1000 -2886.2068 74.34751 1000 -2889.6553
+ 72.36741 1000 -2893.1035 70.387314 1000 -2896.5518
+ 65.46245 1000 -2905.1282 62.517685 1000 -2910.2563
+ 59.57292 1000 -2915.3845 56.62816 1000 -2920.513
+ 53.683395 1000 -2925.641 50.738632 1000 -2930.7693
+ 47.79387 1000 -2935.8975 44.849106 1000 -2941.0256
+ 41.904343 1000 -2946.1538 38.95958 1000 -2951.282
+ 36.014816 1000 -2956.4102 33.070053 1000 -2961.5386
+ 30.12529 1000 -2966.6667 27.180527 1000 -2971.795
+ 24.235764 1000 -2976.923 21.291 1000 -2982.0513
+ 18.346237 1000 -2987.1794 15.401475 1000 -2992.3076
+ 12.456712 1000 -2997.4358 9.511949 1000 -3002.5642
+ 6.5671854 1000 -3007.6924 3.622422 1000 -3012.8206
+ 0.6776588 1000 -3017.9487 -2.2671044 1000 -3023.077
+ -5.211868 1000 -3028.205 -8.1566305 1000 -3033.3333
+ -11.101394 1000 -3038.4614 -14.046157 1000 -3043.5898
+ -16.990921 1000 -3048.718 -19.935684 1000 -3053.8462
+ -22.880447 1000 -3058.9744 -25.82521 1000 -3064.1025
+ -28.769974 1000 -3069.2307 -31.714737 1000 -3074.359
+ -34.6595 1000 -3079.487 -37.604263 1000 -3084.6155
+ -40.549026 1000 -3089.7437 -43.49379 1000 -3094.8718
+ -48.418644 1000 -3103.4482 -50.398735 1000 -3106.8965
+ -52.378826 1000 -3110.3447 -54.358917 1000 -3113.7932
+ -56.33901 1000 -3117.2415 -58.3191 1000 -3120.6897
+ -60.29919 1000 -3124.138 -62.27928 1000 -3127.5862
+ -64.25938 1000 -3131.0344 -66.23946 1000 -3134.4827
+ -68.21956 1000 -3137.9312 -70.199646 1000 -3141.3794
+ -72.17974 1000 -3144.8276 -74.15983 1000 -3148.276
+ -76.13992 1000 -3151.724 -78.12001 1000 -3155.1724
+ -80.100105 1000 -3158.6206 -82.08019 1000 -3162.0688
+ -84.06029 1000 -3165.5173 -86.040375 1000 -3168.9656
+ -88.02047 1000 -3172.4138 -90.00056 1000 -3175.862
+ -91.98065 1000 -3179.3103 -93.96074 1000 -3182.7585
+ -95.940834 1000 -3186.2068 -97.92092 1000 -3189.6553
+ -99.90102 1000 -3193.1035 -101.8811 1000 -3196.5518
+ -109.801506 1000 -3210.3447 -115.74181 1000 -3220.6897
+ -121.68211 1000 -3231.0344 -127.62242 1000 -3241.3794
+ -133.56273 1000 -3251.724 -139.50304 1000 -3262.0688
+ -145.44334 1000 -3272.4138 -151.38365 1000 -3282.7585
+ -157.32396 1000 -3293.1035 -163.26427 1000 -3303.4482
+ -169.20457 1000 -3313.7932 -175.14487 1000 -3324.138
+ -181.08517 1000 -3334.4827 -187.02548 1000 -3344.8276
+ -192.96579 1000 -3355.1724 -198.9061 1000 -3365.5173
+ -204.8464 1000 -3375.862 -210.78671 1000 -3386.2068
+ -216.72702 1000 -3396.5518 -222.66733 1000 -3406.8965
+ -228.60764 1000 -3417.2415 -234.54794 1000 -3427.5862
+ -240.48825 1000 -3437.9312 -246.42854 1000 -3448.276
+ -252.36885 1000 -3458.6206 -258.30917 1000 -3468.9656
+ -264.24948 1000 -3479.3103 -270.1898 1000 -3489.6553
+ -134.0921 1000 -3210.3447 -140.02502 1000 -3220.6897
+ -145.95795 1000 -3231.0344 -151.89085 1000 -3241.3794
+ -157.82378 1000 -3251.724 -163.75668 1000 -3262.0688
+ -169.6896 1000 -3272.4138 -175.62251 1000 -3282.7585
+ -181.55544 1000 -3293.1035 -187.48836 1000 -3303.4482
+ -193.42126 1000 -3313.7932 -199.35419 1000 -3324.138
+ -205.2871 1000 -3334.4827 -211.22002 1000 -3344.8276
+ -217.15294 1000 -3355.1724 -223.08585 1000 -3365.5173
+ -229.01877 1000 -3375.862 -234.95168 1000 -3386.2068
+ -240.8846 1000 -3396.5518 -246.8175 1000 -3406.8965
+ -252.75043 1000 -3417.2415 -258.68335 1000 -3427.5862
+ -264.61627 1000 -3437.9312 -270.54916 1000 -3448.276
+ -276.4821 1000 -3458.6206 -282.415 1000 -3468.9656
+ -288.34793 1000 -3479.3103 -294.28085 1000 -3489.6553
+ -72.78531 1000 -3103.4482 -74.76295 1000 -3106.8965
+ -76.740585 1000 -3110.3447 -78.71822 1000 -3113.7932
+ -80.69586 1000 -3117.2415 -82.6735 1000 -3120.6897
+ -84.65114 1000 -3124.138 -86.62878 1000 -3127.5862
+ -88.60642 1000 -3131.0344 -90.58406 1000 -3134.4827
+ -92.5617 1000 -3137.9312 -94.53934 1000 -3141.3794
+ -96.516975 1000 -3144.8276 -98.49461 1000 -3148.276
+ -100.47225 1000 -3151.724 -102.44989 1000 -3155.1724
+ -104.42753 1000 -3158.6206 -106.40517 1000 -3162.0688
+ -108.382805 1000 -3165.5173 -110.36044 1000 -3168.9656
+ -112.33808 1000 -3172.4138 -114.31572 1000 -3175.862
+ -116.29336 1000 -3179.3103 -118.270996 1000 -3182.7585
+ -120.248634 1000 -3186.2068 -122.22627 1000 -3189.6553
+ -124.20392 1000 -3193.1035 -126.18156 1000 -3196.5518
+ 40.954266 1000 -2905.1282 38.013165 1000 -2910.2563
+ 35.07206 1000 -2915.3845 32.130955 1000 -2920.513
+ 29.189852 1000 -2925.641 26.248749 1000 -2930.7693
+ 23.307646 1000 -2935.8975 20.36654 1000 -2941.0256
+ 17.425438 1000 -2946.1538 14.484335 1000 -2951.282
+ 11.543231 1000 -2956.4102 8.602127 1000 -2961.5386
+ 5.6610236 1000 -2966.6667 2.71992 1000 -2971.795
+ -0.22118369 1000 -2976.923 -3.1622872 1000 -2982.0513
+ -6.103391 1000 -2987.1794 -9.044495 1000 -2992.3076
+ -11.985599 1000 -2997.4358 -14.926702 1000 -3002.5642
+ -17.867805 1000 -3007.6924 -20.808908 1000 -3012.8206
+ -23.750013 1000 -3017.9487 -26.691116 1000 -3023.077
+ -29.63222 1000 -3028.205 -32.573322 1000 -3033.3333
+ -35.514427 1000 -3038.4614 -38.455532 1000 -3043.5898
+ -41.396633 1000 -3048.718 -44.337738 1000 -3053.8462
+ -47.278843 1000 -3058.9744 -50.219944 1000 -3064.1025
+ -53.16105 1000 -3069.2307 -56.102154 1000 -3074.359
+ -59.043255 1000 -3079.487 -61.98436 1000 -3084.6155
+ -64.92546 1000 -3089.7437 -67.86657 1000 -3094.8718
+ 99.26926 1000 -2803.4482 97.29162 1000 -2806.8965
+ 95.31397 1000 -2810.3447 93.336334 1000 -2813.7932
+ 91.358696 1000 -2817.2415 89.38106 1000 -2820.6897
+ 87.40342 1000 -2824.138 85.42578 1000 -2827.5862
+ 83.44814 1000 -2831.0344 81.470505 1000 -2834.4827
+ 79.49287 1000 -2837.9312 77.51523 1000 -2841.3794
+ 75.53759 1000 -2844.8276 73.55995 1000 -2848.276
+ 71.58231 1000 -2851.724 69.604675 1000 -2855.1724
+ 67.62704 1000 -2858.6206 65.6494 1000 -2862.0688
+ 63.671757 1000 -2865.5173 61.69412 1000 -2868.9656
+ 59.71648 1000 -2872.4138 57.738842 1000 -2875.862
+ 55.761204 1000 -2879.3103 53.783566 1000 -2882.7585
+ 51.805927 1000 -2886.2068 49.828285 1000 -2889.6553
+ 47.850647 1000 -2893.1035 45.87301 1000 -2896.5518
+ 267.36884 1000 -2510.3447 261.4359 1000 -2520.6897
+ 255.50298 1000 -2531.0344 249.57005 1000 -2541.3794
+ 243.63713 1000 -2551.724 237.70421 1000 -2562.0688
+ 231.77127 1000 -2572.4138 225.83835 1000 -2582.7585
+ 219.90543 1000 -2593.1035 213.9725 1000 -2603.4482
+ 208.03957 1000 -2613.7932 202.10664 1000 -2624.138
+ 196.17372 1000 -2634.4827 190.24078 1000 -2644.8276
+ 184.30786 1000 -2655.1724 178.37494 1000 -2665.5173
+ 172.44202 1000 -2675.862 166.50908 1000 -2686.2068
+ 160.57616 1000 -2696.5518 154.64323 1000 -2706.8965
+ 148.71031 1000 -2717.2415 142.77737 1000 -2727.5862
+ 136.84445 1000 -2737.9312 130.91153 1000 -2748.276
+ 124.9786 1000 -2758.6206 119.04567 1000 -2768.9656
+ 113.11275 1000 -2779.3103 107.17982 1000 -2789.6553
+
+
+ 2514.8944015
+
+
+ 3652.4140363
+
+
+
+
+
+
+ 1 11 502 168 8 201
+ 636 474 11 12 501 502
+ 201 200 637 636 12 13
+ 500 501 200 199 638 637
+ 13 14 499 500 199 198
+ 639 638 14 15 498 499
+ 198 197 640 639 15 16
+ 497 498 197 196 641 640
+ 16 17 496 497 196 195
+ 642 641 17 18 495 496
+ 195 194 643 642 18 19
+ 494 495 194 193 644 643
+ 19 20 493 494 193 192
+ 645 644 20 21 492 493
+ 192 191 646 645 21 22
+ 491 492 191 190 647 646
+ 22 23 490 491 190 189
+ 648 647 23 24 489 490
+ 189 188 649 648 24 25
+ 488 489 188 187 650 649
+ 25 26 487 488 187 186
+ 651 650 26 27 486 487
+ 186 185 652 651 27 28
+ 485 486 185 184 653 652
+ 28 29 484 485 184 183
+ 654 653 29 30 483 484
+ 183 182 655 654 30 31
+ 482 483 182 181 656 655
+ 31 32 481 482 181 180
+ 657 656 32 33 480 481
+ 180 179 658 657 33 34
+ 479 480 179 178 659 658
+ 34 35 478 479 178 177
+ 660 659 35 36 477 478
+ 177 176 661 660 36 37
+ 476 477 176 175 662 661
+ 37 38 475 476 175 174
+ 663 662 38 2 169 475
+ 174 10 503 663 2 42
+ 531 169 10 229 664 503
+ 42 43 530 531 229 228
+ 665 664 43 44 529 530
+ 228 227 666 665 44 45
+ 528 529 227 226 667 666
+ 45 46 527 528 226 225
+ 668 667 46 47 526 527
+ 225 224 669 668 47 48
+ 525 526 224 223 670 669
+ 48 49 524 525 223 222
+ 671 670 49 50 523 524
+ 222 221 672 671 50 51
+ 522 523 221 220 673 672
+ 51 52 521 522 220 219
+ 674 673 52 53 520 521
+ 219 218 675 674 53 54
+ 519 520 218 217 676 675
+ 54 55 518 519 217 216
+ 677 676 55 56 517 518
+ 216 215 678 677 56 57
+ 516 517 215 214 679 678
+ 57 58 515 516 214 213
+ 680 679 58 59 514 515
+ 213 212 681 680 59 60
+ 513 514 212 211 682 681
+ 60 61 512 513 211 210
+ 683 682 61 62 511 512
+ 210 209 684 683 62 63
+ 510 511 209 208 685 684
+ 63 64 509 510 208 207
+ 686 685 64 65 508 509
+ 207 206 687 686 65 66
+ 507 508 206 205 688 687
+ 66 67 506 507 205 204
+ 689 688 67 68 505 506
+ 204 203 690 689 68 69
+ 504 505 203 202 691 690
+ 69 3 170 504 202 39
+ 532 691 3 70 570 170
+ 39 267 692 532 70 71
+ 569 570 267 266 693 692
+ 71 72 568 569 266 265
+ 694 693 72 73 567 568
+ 265 264 695 694 73 74
+ 566 567 264 263 696 695
+ 74 75 565 566 263 262
+ 697 696 75 76 564 565
+ 262 261 698 697 76 77
+ 563 564 261 260 699 698
+ 77 78 562 563 260 259
+ 700 699 78 79 561 562
+ 259 258 701 700 79 80
+ 560 561 258 257 702 701
+ 80 81 559 560 257 256
+ 703 702 81 82 558 559
+ 256 255 704 703 82 83
+ 557 558 255 254 705 704
+ 83 84 556 557 254 253
+ 706 705 84 85 555 556
+ 253 252 707 706 85 86
+ 554 555 252 251 708 707
+ 86 87 553 554 251 250
+ 709 708 87 88 552 553
+ 250 249 710 709 88 89
+ 551 552 249 248 711 710
+ 89 90 550 551 248 247
+ 712 711 90 91 549 550
+ 247 246 713 712 91 92
+ 548 549 246 245 714 713
+ 92 93 547 548 245 244
+ 715 714 93 94 546 547
+ 244 243 716 715 94 95
+ 545 546 243 242 717 716
+ 95 96 544 545 242 241
+ 718 717 96 97 543 544
+ 241 240 719 718 97 98
+ 542 543 240 239 720 719
+ 98 99 541 542 239 238
+ 721 720 99 100 540 541
+ 238 237 722 721 100 101
+ 539 540 237 236 723 722
+ 101 102 538 539 236 235
+ 724 723 102 103 537 538
+ 235 234 725 724 103 104
+ 536 537 234 233 726 725
+ 104 105 535 536 233 232
+ 727 726 105 106 534 535
+ 232 231 728 727 106 107
+ 533 534 231 230 729 728
+ 107 4 171 533 230 164
+ 571 729 4 108 599 171
+ 164 295 730 571 108 109
+ 598 599 295 294 731 730
+ 109 110 597 598 294 293
+ 732 731 110 111 596 597
+ 293 292 733 732 111 112
+ 595 596 292 291 734 733
+ 112 113 594 595 291 290
+ 735 734 113 114 593 594
+ 290 289 736 735 114 115
+ 592 593 289 288 737 736
+ 115 116 591 592 288 287
+ 738 737 116 117 590 591
+ 287 286 739 738 117 118
+ 589 590 286 285 740 739
+ 118 119 588 589 285 284
+ 741 740 119 120 587 588
+ 284 283 742 741 120 121
+ 586 587 283 282 743 742
+ 121 122 585 586 282 281
+ 744 743 122 123 584 585
+ 281 280 745 744 123 124
+ 583 584 280 279 746 745
+ 124 125 582 583 279 278
+ 747 746 125 126 581 582
+ 278 277 748 747 126 127
+ 580 581 277 276 749 748
+ 127 128 579 580 276 275
+ 750 749 128 129 578 579
+ 275 274 751 750 129 130
+ 577 578 274 273 752 751
+ 130 131 576 577 273 272
+ 753 752 131 132 575 576
+ 272 271 754 753 132 133
+ 574 575 271 270 755 754
+ 133 134 573 574 270 269
+ 756 755 134 135 572 573
+ 269 268 757 756 135 5
+ 172 572 268 165 600 757
+ 5 136 629 172 165 323
+ 758 600 136 137 628 629
+ 323 322 759 758 137 138
+ 627 628 322 321 760 759
+ 138 139 626 627 321 320
+ 761 760 139 140 625 626
+ 320 319 762 761 140 141
+ 624 625 319 318 763 762
+ 141 142 623 624 318 317
+ 764 763 142 143 622 623
+ 317 316 765 764 143 144
+ 621 622 316 315 766 765
+ 144 145 620 621 315 314
+ 767 766 145 146 619 620
+ 314 313 768 767 146 147
+ 618 619 313 312 769 768
+ 147 148 617 618 312 311
+ 770 769 148 149 616 617
+ 311 310 771 770 149 150
+ 615 616 310 309 772 771
+ 150 151 614 615 309 308
+ 773 772 151 152 613 614
+ 308 307 774 773 152 153
+ 612 613 307 306 775 774
+ 153 154 611 612 306 305
+ 776 775 154 155 610 611
+ 305 304 777 776 155 156
+ 609 610 304 303 778 777
+ 156 157 608 609 303 302
+ 779 778 157 158 607 608
+ 302 301 780 779 158 159
+ 606 607 301 300 781 780
+ 159 160 605 606 300 299
+ 782 781 160 161 604 605
+ 299 298 783 782 161 162
+ 603 604 298 297 784 783
+ 162 163 602 603 297 296
+ 785 784 163 0 173 602
+ 296 6 601 785 41 351
+ 786 630 5 136 629 172
+ 351 350 787 786 136 137
+ 628 629 350 349 788 787
+ 137 138 627 628 349 348
+ 789 788 138 139 626 627
+ 348 347 790 789 139 140
+ 625 626 347 346 791 790
+ 140 141 624 625 346 345
+ 792 791 141 142 623 624
+ 345 344 793 792 142 143
+ 622 623 344 343 794 793
+ 143 144 621 622 343 342
+ 795 794 144 145 620 621
+ 342 341 796 795 145 146
+ 619 620 341 340 797 796
+ 146 147 618 619 340 339
+ 798 797 147 148 617 618
+ 339 338 799 798 148 149
+ 616 617 338 337 800 799
+ 149 150 615 616 337 336
+ 801 800 150 151 614 615
+ 336 335 802 801 151 152
+ 613 614 335 334 803 802
+ 152 153 612 613 334 333
+ 804 803 153 154 611 612
+ 333 332 805 804 154 155
+ 610 611 332 331 806 805
+ 155 156 609 610 331 330
+ 807 806 156 157 608 609
+ 330 329 808 807 157 158
+ 607 608 329 328 809 808
+ 158 159 606 607 328 327
+ 810 809 159 160 605 606
+ 327 326 811 810 160 161
+ 604 605 326 325 812 811
+ 161 162 603 604 325 324
+ 813 812 162 163 602 603
+ 324 7 631 813 163 0
+ 173 602 632 171 4 40
+ 814 599 108 379 814 599
+ 108 379 815 598 109 378
+ 815 598 109 378 816 597
+ 110 377 816 597 110 377
+ 817 596 111 376 817 596
+ 111 376 818 595 112 375
+ 818 595 112 375 819 594
+ 113 374 819 594 113 374
+ 820 593 114 373 820 593
+ 114 373 821 592 115 372
+ 821 592 115 372 822 591
+ 116 371 822 591 116 371
+ 823 590 117 370 823 590
+ 117 370 824 589 118 369
+ 824 589 118 369 825 588
+ 119 368 825 588 119 368
+ 826 587 120 367 826 587
+ 120 367 827 586 121 366
+ 827 586 121 366 828 585
+ 122 365 828 585 122 365
+ 829 584 123 364 829 584
+ 123 364 830 583 124 363
+ 830 583 124 363 831 582
+ 125 362 831 582 125 362
+ 832 581 126 361 832 581
+ 126 361 833 580 127 360
+ 833 580 127 360 834 579
+ 128 359 834 579 128 359
+ 835 578 129 358 835 578
+ 129 358 836 577 130 357
+ 836 577 130 357 837 576
+ 131 356 837 576 131 356
+ 838 575 132 355 838 575
+ 132 355 839 574 133 354
+ 839 574 133 354 840 573
+ 134 353 840 573 134 353
+ 841 572 135 352 841 572
+ 135 352 630 172 5 41
+ 166 417 842 633 3 70
+ 570 170 417 416 843 842
+ 70 71 569 570 416 415
+ 844 843 71 72 568 569
+ 415 414 845 844 72 73
+ 567 568 414 413 846 845
+ 73 74 566 567 413 412
+ 847 846 74 75 565 566
+ 412 411 848 847 75 76
+ 564 565 411 410 849 848
+ 76 77 563 564 410 409
+ 850 849 77 78 562 563
+ 409 408 851 850 78 79
+ 561 562 408 407 852 851
+ 79 80 560 561 407 406
+ 853 852 80 81 559 560
+ 406 405 854 853 81 82
+ 558 559 405 404 855 854
+ 82 83 557 558 404 403
+ 856 855 83 84 556 557
+ 403 402 857 856 84 85
+ 555 556 402 401 858 857
+ 85 86 554 555 401 400
+ 859 858 86 87 553 554
+ 400 399 860 859 87 88
+ 552 553 399 398 861 860
+ 88 89 551 552 398 397
+ 862 861 89 90 550 551
+ 397 396 863 862 90 91
+ 549 550 396 395 864 863
+ 91 92 548 549 395 394
+ 865 864 92 93 547 548
+ 394 393 866 865 93 94
+ 546 547 393 392 867 866
+ 94 95 545 546 392 391
+ 868 867 95 96 544 545
+ 391 390 869 868 96 97
+ 543 544 390 389 870 869
+ 97 98 542 543 389 388
+ 871 870 98 99 541 542
+ 388 387 872 871 99 100
+ 540 541 387 386 873 872
+ 100 101 539 540 386 385
+ 874 873 101 102 538 539
+ 385 384 875 874 102 103
+ 537 538 384 383 876 875
+ 103 104 536 537 383 382
+ 877 876 104 105 535 536
+ 382 381 878 877 105 106
+ 534 535 381 380 879 878
+ 106 107 533 534 380 40
+ 632 879 107 4 171 533
+ 167 445 880 634 2 42
+ 531 169 445 444 881 880
+ 42 43 530 531 444 443
+ 882 881 43 44 529 530
+ 443 442 883 882 44 45
+ 528 529 442 441 884 883
+ 45 46 527 528 441 440
+ 885 884 46 47 526 527
+ 440 439 886 885 47 48
+ 525 526 439 438 887 886
+ 48 49 524 525 438 437
+ 888 887 49 50 523 524
+ 437 436 889 888 50 51
+ 522 523 436 435 890 889
+ 51 52 521 522 435 434
+ 891 890 52 53 520 521
+ 434 433 892 891 53 54
+ 519 520 433 432 893 892
+ 54 55 518 519 432 431
+ 894 893 55 56 517 518
+ 431 430 895 894 56 57
+ 516 517 430 429 896 895
+ 57 58 515 516 429 428
+ 897 896 58 59 514 515
+ 428 427 898 897 59 60
+ 513 514 427 426 899 898
+ 60 61 512 513 426 425
+ 900 899 61 62 511 512
+ 425 424 901 900 62 63
+ 510 511 424 423 902 901
+ 63 64 509 510 423 422
+ 903 902 64 65 508 509
+ 422 421 904 903 65 66
+ 507 508 421 420 905 904
+ 66 67 506 507 420 419
+ 906 905 67 68 505 506
+ 419 418 907 906 68 69
+ 504 505 418 166 633 907
+ 69 3 170 504 9 473
+ 908 635 1 11 502 168
+ 473 472 909 908 11 12
+ 501 502 472 471 910 909
+ 12 13 500 501 471 470
+ 911 910 13 14 499 500
+ 470 469 912 911 14 15
+ 498 499 469 468 913 912
+ 15 16 497 498 468 467
+ 914 913 16 17 496 497
+ 467 466 915 914 17 18
+ 495 496 466 465 916 915
+ 18 19 494 495 465 464
+ 917 916 19 20 493 494
+ 464 463 918 917 20 21
+ 492 493 463 462 919 918
+ 21 22 491 492 462 461
+ 920 919 22 23 490 491
+ 461 460 921 920 23 24
+ 489 490 460 459 922 921
+ 24 25 488 489 459 458
+ 923 922 25 26 487 488
+ 458 457 924 923 26 27
+ 486 487 457 456 925 924
+ 27 28 485 486 456 455
+ 926 925 28 29 484 485
+ 455 454 927 926 29 30
+ 483 484 454 453 928 927
+ 30 31 482 483 453 452
+ 929 928 31 32 481 482
+ 452 451 930 929 32 33
+ 480 481 451 450 931 930
+ 33 34 479 480 450 449
+ 932 931 34 35 478 479
+ 449 448 933 932 35 36
+ 477 478 448 447 934 933
+ 36 37 476 477 447 446
+ 935 934 37 38 475 476
+ 446 167 634 935 38 2
+ 169 475
+
+
+ 8 16 24 32 40 48
+ 56 64 72 80 88 96
+ 104 112 120 128 136 144
+ 152 160 168 176 184 192
+ 200 208 216 224 232 240
+ 248 256 264 272 280 288
+ 296 304 312 320 328 336
+ 344 352 360 368 376 384
+ 392 400 408 416 424 432
+ 440 448 456 464 472 480
+ 488 496 504 512 520 528
+ 536 544 552 560 568 576
+ 584 592 600 608 616 624
+ 632 640 648 656 664 672
+ 680 688 696 704 712 720
+ 728 736 744 752 760 768
+ 776 784 792 800 808 816
+ 824 832 840 848 856 864
+ 872 880 888 896 904 912
+ 920 928 936 944 952 960
+ 968 976 984 992 1000 1008
+ 1016 1024 1032 1040 1048 1056
+ 1064 1072 1080 1088 1096 1104
+ 1112 1120 1128 1136 1144 1152
+ 1160 1168 1176 1184 1192 1200
+ 1208 1216 1224 1232 1240 1248
+ 1256 1264 1272 1280 1288 1296
+ 1304 1312 1320 1328 1336 1344
+ 1352 1360 1368 1376 1384 1392
+ 1400 1408 1416 1424 1432 1440
+ 1448 1456 1464 1472 1480 1488
+ 1496 1504 1512 1520 1528 1536
+ 1544 1552 1560 1568 1576 1584
+ 1592 1600 1608 1616 1624 1632
+ 1640 1648 1656 1664 1672 1680
+ 1688 1696 1704 1712 1720 1728
+ 1736 1744 1752 1760 1768 1776
+ 1784 1792 1800 1808 1816 1824
+ 1832 1840 1848 1856 1864 1872
+ 1880 1888 1896 1904 1912 1920
+ 1928 1936 1944 1952 1960 1968
+ 1976 1984 1992 2000 2008 2016
+ 2024 2032 2040 2048 2056 2064
+ 2072 2080 2088 2096 2104 2112
+ 2120 2128 2136 2144 2152 2160
+ 2168 2176 2184 2192 2200 2208
+ 2216 2224 2232 2240 2248 2256
+ 2264 2272 2280 2288 2296 2304
+ 2312 2320 2328 2336 2344 2352
+ 2360 2368 2376 2384 2392 2400
+ 2408 2416 2424 2432 2440 2448
+ 2456 2464 2472 2480
+
+
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12 12 12
+ 12 12 12 12
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/grav_seg_1d_DBC_small_mesh.xml b/inputFiles/coverageSmokes/grav_seg_1d_DBC_small_mesh.xml
new file mode 100644
index 00000000000..7da88f54fd0
--- /dev/null
+++ b/inputFiles/coverageSmokes/grav_seg_1d_DBC_small_mesh.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/isothm_mass_inj_table_fast_outputs.xml b/inputFiles/coverageSmokes/isothm_mass_inj_table_fast_outputs.xml
new file mode 100644
index 00000000000..09e653d7b18
--- /dev/null
+++ b/inputFiles/coverageSmokes/isothm_mass_inj_table_fast_outputs.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/kgdViscosityDominated_small_mesh.xml b/inputFiles/coverageSmokes/kgdViscosityDominated_small_mesh.xml
new file mode 100644
index 00000000000..60b2a1d0d6f
--- /dev/null
+++ b/inputFiles/coverageSmokes/kgdViscosityDominated_small_mesh.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/staircase3d_tet_coverage.vtu b/inputFiles/coverageSmokes/staircase3d_tet_coverage.vtu
new file mode 100644
index 00000000000..72190fb9653
--- /dev/null
+++ b/inputFiles/coverageSmokes/staircase3d_tet_coverage.vtu
@@ -0,0 +1,255 @@
+
+
+
+
+
+
+ 35 37 212 213 214 241
+ 256 257 258 264 414 427
+ 456 468 486 490 491 515
+ 516 539 589 605 630 631
+ 646 661 669 715 718 719
+ 739 809 810 814 818 842
+ 867 870 871 885 918 928
+ 931 945 947 959 960
+
+
+
+
+ 0.11 0.11 0.11 0.11 0.11 0.11
+ 0.11 0.11 0.11 0.11 0.11 0.11
+ 0.11 0.11 0.11 0.11 0.11 0.11
+ 0.11 0.11 0.11 0.11 0.11 0.11
+ 0.11 0.11 0.11 0.11 0.11 0.11
+ 0.11 0.11 0.11 0.11 0.11 0.11
+ 0.11 0.11 0.11 0.11 0.11 0.11
+ 0.11 0.11 0.11 0.11 0.11 0.11
+ 0.11 0.11 0.11 0.11 0.11 0.11
+ 0.11 0.11 0.11 0.11 0.11 0.11
+ 0.11 0.11 0.11 0.11 0.11 0.11
+ 0.11 0.11 0.11
+
+
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16 2.5e-16
+ 2.5e-16 2.5e-16 2.5e-16
+
+
+ 4.3301270189e-16
+
+
+ 4.3301270189e-16
+
+
+
+
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+ 0.005 0.995 0.005 0.995 0.005 0.995
+
+
+ 0.99501256273
+
+
+ 0.99501256273
+
+
+
+
+ 4 4 4 4 4 4
+ 4 4 4 4 4 4
+ 4 4 4 4 4 4
+ 4 4 4 4 4 4
+ 4 4 4 4 4 4
+ 4 4 4 4 4 4
+ 4 4 4 4 4 4
+ 4 4 4 4 4 4
+ 4 4 4 4 4 4
+ 4 4 4 4 4 4
+ 4 4 4 4 4 4
+ 4 4 4
+
+
+ 159 210 490 515 573 598
+ 645 722 740 821 845 956
+ 1003 1019 1117 1134 1264 1290
+ 1322 1454 1464 1469 1631 1637
+ 1689 1753 1763 1828 1838 1887
+ 2084 2189 2196 2203 2218 2466
+ 2495 2536 2552 2593 2598 2626
+ 2632 2683 2700 2723 2733 2764
+ 2789 2793 2795 2833 2841 2855
+ 2860 2866 2867 2872 2878 2898
+ 2899 2903 2931 2944 2949 3083
+ 3084 3095 3096
+
+
+
+
+ 8.31685098013915 0 1.716600433082997 8.736871856380187 1.263128143619813 1.259240372343239
+ 9.272800205258896 0 0.6838575978415236 9.489776110860404 0 1.249676492542829
+ 10 0.4995307143941714 1.249538658589354 8.729998852157943 1.270001147842057 8.75
+ 9.500469285605828 0 8.749538658589353 10 0.511441783475096 8.750019825172702
+ 10 0.7271997947411037 8.18550457900122 8.29447217172432 0 0.7698122537158085
+ 8.805555555555557 0 1.25 9.276530006661368 0 1.813877849958277
+ 10 1.194444444444444 8.75 10 0.7319952536871392 9.314634197207695
+ 8.437711738717965 0.6558977212005124 2.5 10 1.194444444444444 1.25
+ 10 0.724031718907258 0.6837427461074205 9.275968281092743 0 8.18374274610742
+ 8.805555555555555 0 8.75 8.459158235653726 0.6181048510255562 0
+ 10 0.7534376582239992 1.811736108068244 9.246562341776002 0 9.311736108068244
+ 8.313480626727392 0 8.26912314331119 8.437711738717965 0.6558977212005124 7.5
+ 8.61777020418923 1.283719970254023 0 8.436930648841711 0.6556746962602613 10
+ 9.209627182306447 0.7903728176935534 0 9.177689128372437 0.8223108716275627 7.5
+ 9.164583333333333 0.8354166666666671 10 9.177689128372437 0.8223108716275627 2.5
+ 8.137044990826947 0 9.203749981742797 10 0 9.166666666666666
+ 10 0 0.8333333333333333 10 0 8.333333333333334
+ 10 0 1.666666666666667 10 1 10
+ 9 0 0 10 1 7.5
+ 9 0 7.5 9 0 2.5
+ 10 1 0 10 0 0
+ 10 0 7.5 10 0 2.5
+ 10 0 10 10 1 2.5
+ 9 0 10
+
+
+ 8.3301188175
+
+
+ 14.177446879
+
+
+
+
+
+
+ 2 3 4 1 6 7
+ 8 5 3 2 10 1
+ 3 10 11 1 12 8
+ 7 5 7 13 12 5
+ 4 15 16 1 4 16
+ 2 1 6 17 18 5
+ 8 17 6 5 9 10
+ 2 1 4 20 15 1
+ 10 0 11 1 18 21
+ 6 5 18 17 22 5
+ 9 2 19 1 18 25
+ 21 5 26 19 2 1
+ 10 9 0 1 22 17
+ 23 5 2 16 26 1
+ 11 0 14 1 27 17
+ 8 5 11 14 29 1
+ 23 17 27 5 29 20
+ 11 1 18 30 25 5
+ 19 26 24 1 21 28
+ 13 5 21 25 28 5
+ 21 18 30 25 31 7
+ 6 13 4 3 2 32
+ 7 8 33 6 3 34
+ 4 11 19 9 36 2
+ 22 38 23 17 11 39
+ 14 0 26 19 36 2
+ 32 2 4 16 6 8
+ 33 17 38 27 23 17
+ 14 29 39 11 21 31
+ 6 13 20 4 34 11
+ 41 26 2 16 8 27
+ 42 17 32 41 2 16
+ 20 43 29 11 42 33
+ 8 17 13 21 44 28
+ 34 43 20 11 21 44
+ 31 13 27 38 42 17
+ 39 29 43 11 27 42
+ 37 8 41 26 16 40
+ 28 21 46 25 26 36
+ 41 2 33 31 7 6
+ 30 46 21 25 4 34
+ 3 32 44 13 28 35
+ 46 44 21 28 29 45
+ 43 20 11 4 1 20
+ 11 1 4 3 6 13
+ 5 21 5 13 6 7
+
+
+ 4 8 12 16 20 24
+ 28 32 36 40 44 48
+ 52 56 60 64 68 72
+ 76 80 84 88 92 96
+ 100 104 108 112 116 120
+ 124 128 132 136 140 144
+ 148 152 156 160 164 168
+ 172 176 180 184 188 192
+ 196 200 204 208 212 216
+ 220 224 228 232 236 240
+ 244 248 252 256 260 264
+ 268 272 276
+
+
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/staircase_co2_wells_hybrid_small_mesh.xml b/inputFiles/coverageSmokes/staircase_co2_wells_hybrid_small_mesh.xml
new file mode 100644
index 00000000000..43d30aca5be
--- /dev/null
+++ b/inputFiles/coverageSmokes/staircase_co2_wells_hybrid_small_mesh.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/staircase_co2_wells_small_mesh.xml b/inputFiles/coverageSmokes/staircase_co2_wells_small_mesh.xml
new file mode 100644
index 00000000000..f6ccbf353ad
--- /dev/null
+++ b/inputFiles/coverageSmokes/staircase_co2_wells_small_mesh.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/synthetic_isothermal_coverage.vtu b/inputFiles/coverageSmokes/synthetic_isothermal_coverage.vtu
new file mode 100644
index 00000000000..b743a75dfcd
--- /dev/null
+++ b/inputFiles/coverageSmokes/synthetic_isothermal_coverage.vtu
@@ -0,0 +1,542 @@
+
+
+
+
+
+
+
+
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3
+
+
+ 520 791 1836 1932 3376 3644
+ 4803 5039 5192 5246 5411 5430
+ 5457 6072 6296 6696 7346 7676
+ 7798 8227 8618 8935 9690 11258
+ 11522 12212 12338 12766 12842 13299
+ 13918 14737 15324 15950 15991 16289
+ 16572 16818 16930 17104 18006 18026
+ 18291 19030 19412 20093 20099 21006
+ 21295 22046 22093 22562 22629 22681
+ 22725 23205 23431 23562 25652 26402
+ 26672 26751 26785 27262 27381 27573
+ 27600 27861 28174 28354 29093 29164
+ 29261 29886 30264 30364 30716 31017
+ 31473 31625 31676 31715 31809 32225
+ 32254 32320 32428 32843 33290 34400
+ 34610 35074 35357 35479 36099 36410
+ 37005 37038 37869 38563 38623 40861
+ 41504 41505 42392 42631 43001 43732
+ 44198 44321 44688 44732 45067 45283
+ 45505 46935 47772 48916 50187 50373
+ 50378 50724 51326 51424 51847 52177
+ 52846 53359 54878 54961 55469 56865
+ 57191 57535 57726 58250 58645 60025
+ 60085 60477 61275 61415 61639 61951
+ 63405 63476 64415 64819 64884 64994
+ 65688 66195 66206 67560 67936 68220
+ 68741 69025 69188 69372 70174 70185
+ 70206 70457 70976 71131 71311 71357
+ 72638 73005 73677 73800 74087 74090
+ 74171 74931 75181 75248 76166 76559
+ 76990 77988 78045 78084 78161 78303
+ 78406 78448 78540 78686 79432 80122
+ 80465 81843 81987 82145 83476 83881
+ 84294 84551 86307 86759 87869 87962
+ 88153 89260 89309 90426 90466 90467
+ 93414 93704 94204 94262 94423 94680
+ 94984 95010 95082 95456 95511 95742
+ 95789 96217 96316 96809 97554 97862
+ 97877 98047 98053 99228 100741 101124
+ 101556 102039 102753 102868 103387 104732
+ 105396 105731 105884 106104 107035 107201
+ 107607 108063 109278 109478 110309 110370
+ 110739 111625 111654 111896 112143 112153
+ 112517 113218 113359 113574 113912 114181
+ 114872 114960 115373 115757 116347 119531
+ 119979 120367 121216 121700 122251 122371
+ 122738 122982 123281 124548 125654 126070
+ 126705 126821 126851 127294 127715 128193
+ 128468 128572 128901 129276 129303 129581
+ 130369 130598 131265 131604 132496 132677
+ 133714 133715 134087 135335
+
+
+
+
+ 6414.460252962066 1444.205182200484 -500 16106.88076660959 7183.896108400462 -500
+ 6964.846365351352 2124.342194884604 -500 15268.22134232392 7400.008845890813 -500
+ 6047.922558528833 1526.479404759503 -2000 6947.451697584778 1073.778241400413 -2000
+ 14928.25639073351 7372.64062876512 -2000 15927.66025517862 7191.62096586634 -2000
+ 7017.285954144765 2045.79697312343 -2000 6296.9909852321 2221.043479928546 -2000
+ 6419.019235960423 1476.725674451538 -871.811885248 6351.88491047623 1895.647306200925 -867.272606624
+ 15425.12484090007 7309.49984638848 -1557.25766899 15433.53351780857 7550.393954079803 -1557.32988096
+ 15654.61183291513 7428.327763482946 -1558.16154905 15688.90367429904 7219.800498002436 -1558.10101004
+ 15541.85486219347 7039.013576270028 -1557.99751826 7085.435691758681 1919.536678805666 -920.605071616
+ 6942.808439844563 1718.616662903383 -909.657731759 15827.97292406456 7578.191430109337 -1557.3110404
+ 16018.31309188429 7506.424857686015 -1554.75842912 15872.34349239403 6985.663840996416 -1556.87089382
+ 16155.27732517789 7073.743355157525 -1551.81837832 16231.8056830548 7376.564401063879 -1549.75744275
+ 7107.83392674774 1512.226906748931 -922.345885261 6838.631663958862 1987.228371859431 -901.820567586
+ 6727.610494343977 1727.499175183294 -893.62720865 6840.865123573634 1447.020328325743 -901.987111805
+ 6519.295165043368 1725.882008942589 -878.735593292 6598.778845238688 2018.697681279134 -884.339631809
+ 6608.247722237624 1470.500082793522 -885.013808666 15922.53773134394 7313.225873204623 -1556.26057398
+ 6324.278651276398 1687.02599639361 -1015.42933171 6419.019235103957 1476.725675339948 -1021.81188519
+ 6351.884909993321 1895.647307309604 -1017.27260659 15425.1248377579 7309.499845142172 -1707.25766897
+ 15433.53351446838 7550.393952604836 -1707.32988093 15654.61182936576 7428.327761902474 -1708.16154905
+ 15688.90367092147 7219.80049652948 -1708.10101005 15541.85485910991 7039.013575056141 -1707.99751825
+ 7106.284701177496 1715.727156350586 -1072.2252949 7085.435691033348 1919.536678901119 -1070.60507156
+ 6942.808439097741 1718.616663217326 -1059.6577317 15827.97292024998 7578.191428349804 -1707.31104043
+ 15633.98146066355 7650.96960359091 -1708.17291011 16018.31308777003 7506.424855697938 -1704.7584292
+ 15872.34348895454 6985.66383940205 -1706.87089386 16155.27732113549 7073.743353097027 -1701.81837843
+ 7107.833926003175 1512.226906640891 -1072.3458852 6838.631663273449 1987.228372583925 -1051.82056754
+ 6727.61049359255 1727.499175804629 -1043.6272086 6840.865122969956 1447.020328704928 -1051.98711176
+ 6519.295164379726 1725.882009822505 -1028.73559325 6598.778844944249 2018.697682348946 -1034.33963179
+ 6608.247721265633 1470.500083545408 -1035.0138086 15922.53772754576 7313.225871418299 -1706.26057403
+ 15631.7521007776 7539.302538322157 -1198.25596637 15588.35129136617 6679.158504323897 -1357.64658739
+ 15643.63246283868 7770.830822385116 -1391.68963049 6835.555088906219 1550.055426963183 -690.093616723
+ 15461.62201165479 7001.667271148624 -864.896569104 7330.604417249119 1881.808576096243 -796.452357741
+ 16159.80318500536 6996.914529080244 -1435.92019943 16228.21040849824 6991.495037977197 -1268.79449181
+ 6557.238623479765 2286.764955752337 -744.191950809 15567.06969935776 7253.434391288517 -1460.0730734
+ 6144.237572425752 1517.472341711509 -741.965801746 15971.78521805879 7641.868965532365 -1472.46452965
+ 6841.92243156029 1339.022698665665 -794.17099006 15380.93286508278 7421.057931377661 -1028.68522669
+ 16128.19486994485 7728.690277970626 -1301.23409517 16495.07276194134 7668.692740324926 -780.130497947
+ 6729.475085619836 1892.312536545452 -803.607191001 16208.98098672995 7381.992356408715 -1095.99593586
+ 15688.0062973088 6650.509640038711 -1190.50503072 15661.8857759854 7292.545500402554 -1324.2522263
+ 15714.04163206046 8081.51161518462 -1099.43174016 15822.97310683744 6826.312815714578 -689.795592256
+ 16067.62006794119 7457.920527276538 -1399.39500644 15254.79904381521 7369.514562716516 -1395.60362123
+ 15552.46433066739 7585.777078286873 -1497.68147388 15150.51319270264 7083.206093482272 -1141.38821205
+ 7150.867595544617 2223.409156595008 -849.363849666 6377.113885982238 1630.112836719984 -800.511677503
+ 15509.0312886289 6894.254235761543 -1496.35823024 14941.1054517046 6549.430169952467 -1048.0009123
+ 15710.81882013538 6838.963962032019 -1432.12004984 15932.37229415238 7138.95984656683 -1494.58962605
+ 7024.141135999898 1611.777623856522 -843.189075135 15207.78413831427 7044.66428168552 -1334.71402278
+ 15954.36992499789 6572.620671423286 -948.841834276 15286.67888392361 6976.901421598507 -1428.45591986
+ 15913.58235195537 7041.503906388943 -1376.22532424 15092.63384389589 7504.08129636816 -1235.42220432
+ 7018.805623653705 2358.171719639931 -645.579568269 16034.86254689591 7715.727590828663 -907.924185739
+ 16180.45901368084 7480.715669306353 -1223.49432616 16397.69851362052 6909.665773308187 -852.65886241
+ 15619.59167894527 6905.716645096024 -1264.24351644 15303.30030318709 7256.449051501891 -1505.82923813
+ 6271.646307275691 1954.356463044872 -667.061338258 15676.78607514222 7837.556830215353 -1451.01874439
+ 15882.39247652947 7943.670198123707 -678.778784493 6654.972747517825 1577.577030804026 -833.933798399
+ 15861.60797771494 7450.709907998588 -1510.73791507 15668.9110525658 7336.720245216547 -1400.74597627
+ 7162.079473256647 1808.416741296722 -876.161046991 15423.96605542521 7569.822675340642 -1436.88438418
+ 15247.75688072624 7649.314628467463 -789.476409662 15735.63664181413 6982.5989554007 -1110.98954118
+ 15942.68793236336 7073.363581380735 -1017.16449274 15919.85778659095 7319.615563004485 -1446.02692966
+ 16075.59095682218 7341.782373474422 -1337.86553015 15940.48859618625 7732.449457341471 -1026.37991058
+ 15836.59086224634 6820.897632889554 -1510.35098416 6670.383489030558 923.5914071080151 -614.449010889
+ 15629.08549224558 7218.108221210961 -1509.57674085 6947.743618676772 2263.693278239357 -791.404729254
+ 15782.237990741 7524.326609718499 -1267.17083606 16047.74609068493 7016.730328775448 -1196.55247288
+ 15522.081641043 7685.15804370933 -1633.17291005 16143.14836261805 7684.611120512637 -1625.42623808
+ 15669.0919484872 7206.668016394629 -1633.10101004 6565.192092788222 1585.329346831217 -956.540811847
+ 7227.899110159976 1733.786103278724 -1006.51118622 6952.54312852688 2082.922739102815 -985.569589126
+ 6942.905968981767 1424.259148672689 -983.420866922 16068.11177085479 6983.40062545337 -1635.81286343
+ 15969.73666844154 7398.189049176358 -1651.19017377 6879.998298306585 1757.519605207086 -964.233421532
+ 16106.6442347609 7366.151948930444 -1602.07496845 15777.08261067931 6849.039018720965 -1607.73717514
+ 15363.38775905862 7071.156494753336 -1653.12327022 6572.750756365067 1914.528398107353 -975.923615811
+ 15737.8287806317 7489.791276979209 -1606.76737479 15404.35700540844 7430.187248715652 -1604.68064833
+ 15363.63305672098 7451.408446755489 -1660.38135891 15751.20511985038 6786.58795617967 -1659.96683382
+ 7134.283815921423 1437.692890622212 -1024.01833236 15653.6550295367 7540.609603105589 -1666.8269957
+ 7127.751025516122 2014.74781645143 -1024.84737532 15462.0840697585 7029.215372842845 -1607.80299279
+ 6404.251001458512 1698.442238673687 -921.210844444 6855.17357336745 2292.846207078567 -954.286395752
+ 6926.26140494414 1801.87180581976 -1009.72382129 6571.723837631665 1953.762127178193 -929.712959777
+ 6711.321889452413 1602.813537737329 -996.705076484 6472.884946181465 1413.180517726189 -923.151477367
+ 15878.83588765235 7144.556324795994 -1598.31555276 6318.653686513984 1665.10771364739 -973.255436582
+ 7209.028698966282 1522.94105339638 -977.549134224 6815.085330086868 2640.746191513711 -1500.00825858
+ 6405.978124025025 1809.25863360165 -1247.45762526 6068.544611816093 1434.24286217427 -1452.06513568
+ 6140.716111334389 1799.446215848901 -1716.31608807 6719.281951138892 1613.196706236866 -1154.74706156
+ 6896.686222101682 2182.396554935935 -1366.45940414 15890.16254027279 7160.562769208526 -1827.35651757
+ 7216.836689707766 1381.0648062701 -1169.05745533 6946.447891241956 1510.990701369663 -1293.82305236
+ 6678.459690029217 1206.820825478298 -1593.41433653 15415.23591294995 7689.268269266133 -1797.6598104
+ 7252.406911739445 2226.004781416869 -1692.92422277 6130.294398272864 1919.989453119072 -1358.09135085
+ 6881.425239177091 1475.328836214328 -1406.75750532 6944.452908297177 2051.195507667388 -1140.12984529
+ 7325.852342027493 2105.321119781407 -1463.34405688 6512.08434843468 1896.350933679797 -1102.54951022
+ 7031.572727504059 1774.693679319474 -1213.52003938 6900.09968913371 1166.638350572197 -1131.07251892
+ 7181.702458615456 967.3715626867134 -1725.43287628 15446.42948966427 6774.292626715782 -1783.76426724
+ 6833.165538098204 2146.90887196728 -1269.79940945 6961.133823287662 1984.991270176379 -1574.90784392
+ 6278.009316875565 2160.268501707647 -1463.91000643 6375.481012012487 1444.193265154005 -1318.26096394
+ 6416.155902527928 1938.412327275521 -1168.88275385 6915.097771970057 1755.535062391683 -1832.77813737
+ 15869.83475373037 7733.671023234994 -1768.27292918 15561.77002257561 7305.163572556931 -1771.16490029
+ 15746.81498876912 6592.70557226214 -1903.03071132 7038.123594209419 1614.849124078946 -1125.3249288
+ 6821.855069844492 2242.925960274513 -1201.28226463 6897.958632811826 1367.633460976954 -1211.95736679
+ 7012.453443632098 1450.78301709367 -1508.91300888 6679.579727551853 1287.729350424415 -1091.79629531
+ 7337.705517591025 1885.842954230408 -1369.05710949 6391.411031127648 1578.65837112766 -1068.94433694
+ 7370.613126134303 1639.495288861858 -1295.00453171 6734.914875700279 1628.506195531478 -1099.47810844
+ 6252.52632653283 1708.733619249893 -1548.53817627 6650.453252625375 1856.883410116217 -1323.8429703
+ 15804.13639802059 6826.230931620317 -1760.03319914 6785.394652048754 2107.240875565308 -1097.40244726
+ 7332.999234005124 1567.320253544101 -1646.08221763 16020.71003904426 7134.495278261501 -1754.14124927
+ 6491.811938440174 1576.943974336548 -1202.10716548 6399.577154464807 1627.881314403 -1132.46160907
+ 6352.375655789807 1381.079828154743 -1252.38927247
+
+
+ 6372.1967096
+
+
+ 18207.275396
+
+
+
+
+
+
+ 63 92 75 112 25 29
+ 26 72 73 76 56 109
+ 57 75 92 105 57 89
+ 75 105 93 98 75 118
+ 59 61 72 117 56 74
+ 81 98 69 76 73 109
+ 25 82 72 106 58 75
+ 79 105 26 29 28 103
+ 25 72 26 88 85 109
+ 69 110 59 64 72 83
+ 58 105 78 112 25 72
+ 88 106 60 69 95 110
+ 25 88 26 106 59 72
+ 64 117 60 77 71 108
+ 58 75 105 112 65 101
+ 107 111 80 104 67 111
+ 59 61 68 72 56 74
+ 98 119 14 13 12 80
+ 65 104 80 111 65 86
+ 84 87 58 78 105 111
+ 56 98 93 118 59 72
+ 68 83 75 63 112 118
+ 56 109 74 119 95 110
+ 69 113 60 95 90 110
+ 12 14 80 116 26 72
+ 29 103 63 75 98 118
+ 56 81 74 109 2 94
+ 59 100 56 73 109 119
+ 65 86 87 111 65 91
+ 86 105 62 87 86 111
+ 59 68 66 83 63 118
+ 98 119 23 31 87 104
+ 60 90 95 97 27 24
+ 18 88 1 77 3 102
+ 65 87 84 116 62 86
+ 92 111 26 88 72 103
+ 56 98 118 119 68 72
+ 61 88 86 105 92 111
+ 65 105 86 111 0 2
+ 59 100 65 107 105 111
+ 27 26 30 88 16 114
+ 21 116 65 80 104 116
+ 0 59 2 115 21 87
+ 15 116 80 14 104 116
+ 31 15 21 87 31 15
+ 87 116 65 104 87 116
+ 69 109 73 110 65 87
+ 104 111 18 25 26 106
+ 22 87 21 114 73 69
+ 110 113 70 75 112 118
+ 11 72 83 103 21 114
+ 87 116 16 21 15 116
+ 78 105 92 112 92 105
+ 75 112 65 99 80 116
+ 56 118 96 119 30 88
+ 26 103 27 18 26 88
+ 31 21 22 87 65 84
+ 99 116 87 114 84 116
+ 92 105 78 111 31 87
+ 104 116 15 104 14 116
+ 68 83 72 103 18 26
+ 88 106 31 104 15 116
+ 31 14 15 104 12 15
+ 14 116 68 72 88 103
+ 12 16 15 116 28 30
+ 26 103 31 19 14 104
+ 30 28 10 103 20 19
+ 31 104 80 111 101 65
+ 103 29 11 72 103 11
+ 29 28 122 127 46 128
+ 25 143 129 145 27 126
+ 123 129 38 136 122 139
+ 35 122 38 136 35 136
+ 38 139 27 24 126 129
+ 35 38 122 132 46 122
+ 38 137 39 38 122 137
+ 125 133 129 145 122 128
+ 38 139 39 122 132 137
+ 15 134 122 135 15 135
+ 122 141 27 123 18 129
+ 47 55 46 128 120 122
+ 136 139 124 129 125 144
+ 39 46 38 137 122 120
+ 134 139 39 122 38 132
+ 122 134 120 135 27 18
+ 24 129 26 18 123 129
+ 26 129 123 145 37 35
+ 38 139 122 134 15 148
+ 55 38 128 139 26 18
+ 129 145 35 39 38 132
+ 14 134 15 135 53 125
+ 133 144 25 129 18 145
+ 122 15 141 148 126 144
+ 138 146 52 133 32 146
+ 121 128 130 134 138 144
+ 51 146 27 26 18 123
+ 17 18 25 129 52 144
+ 133 146 49 125 53 144
+ 127 128 122 148 53 133
+ 52 144 126 144 129 150
+ 27 26 123 147 26 142
+ 123 147 12 14 15 135
+ 55 37 38 139 122 134
+ 128 139 123 142 26 145
+ 42 51 138 144 123 129
+ 126 146 123 146 33 149
+ 129 133 125 144 127 122
+ 131 148 49 140 125 144
+ 129 133 123 145 128 134
+ 122 148 20 130 31 134
+ 128 130 134 148 19 20
+ 31 134 27 30 26 147
+ 16 131 141 148 16 12
+ 15 141 30 142 26 147
+ 51 144 50 146 123 133
+ 142 145 16 141 15 148
+ 55 128 43 139 133 144
+ 129 146 26 25 18 145
+ 53 52 50 144 43 55
+ 45 128 133 146 123 149
+ 42 50 51 144 21 131
+ 16 148 50 144 52 146
+ 31 134 130 148 14 31
+ 15 134 129 144 126 146
+ 37 36 35 139 31 15
+ 134 148 55 43 37 139
+ 122 141 131 148 42 140
+ 49 144 49 42 41 140
+ 123 133 129 146 52 53
+ 34 133 19 31 14 134
+ 21 16 15 148 51 50
+ 54 146 49 53 50 144
+ 30 28 26 142 28 26
+ 142 145 28 29 26 145
+ 31 22 21 148 42 49
+ 50 144 54 52 33 146
+ 25 26 29 145 15 31
+ 21 148 54 50 52 146
+ 143 17 25 129 38 128
+ 46 55 46 128 38 122
+ 156 166 164 174 163 164
+ 156 191 176 182 168 196
+ 166 164 174 184 156 164
+ 163 174 152 183 168 196
+ 165 168 155 182 165 155
+ 176 182 152 159 168 183
+ 152 168 182 196 152 182
+ 176 196 152 183 196 198
+ 168 176 155 182 40 41
+ 181 189 40 42 41 189
+ 153 174 164 184 155 176
+ 168 196 163 175 164 191
+ 152 168 159 172 152 159
+ 183 198 43 55 37 179
+ 181 189 41 193 152 168
+ 172 182 160 154 173 194
+ 165 181 41 193 155 168
+ 165 181 162 173 154 194
+ 160 173 154 190 44 37
+ 36 179 159 186 164 191
+ 154 177 162 194 155 167
+ 165 176 41 189 42 193
+ 165 189 181 193 159 164
+ 175 191 155 168 158 183
+ 165 167 155 189 154 170
+ 177 194 152 172 159 191
+ 167 189 165 193 42 189
+ 49 193 41 42 49 193
+ 6 7 157 180 38 37
+ 55 179 43 178 55 179
+ 159 152 175 198 45 55
+ 178 195 52 53 50 167
+ 155 185 189 197 50 52
+ 167 187 169 181 155 189
+ 50 167 49 189 49 189
+ 167 193 164 186 156 191
+ 165 155 181 189 53 49
+ 50 167 50 49 42 189
+ 152 159 175 191 168 183
+ 155 196 50 187 167 189
+ 40 48 42 189 54 187
+ 50 189 39 38 46 192
+ 40 181 48 189 167 176
+ 155 197 54 50 51 189
+ 39 179 38 192 36 37
+ 35 179 55 179 178 195
+ 55 38 179 195 4 8
+ 9 177 167 189 187 197
+ 38 39 35 179 46 192
+ 38 195 155 189 167 197
+ 173 184 160 194 157 178
+ 161 179 42 48 51 189
+ 38 192 179 195 38 35
+ 37 179 176 196 155 197
+ 173 160 184 190 4 5
+ 8 177 178 179 157 195
+ 50 54 52 187 51 50
+ 42 189 55 46 38 195
+ 157 179 171 192 179 192
+ 157 195 55 47 46 195
+ 190 151 184 173 190 184
+ 151 174 172 191 188 159
+ 179 43 44 37
+
+
+ 4 8 12 16 20 24
+ 28 32 36 40 44 48
+ 52 56 60 64 68 72
+ 76 80 84 88 92 96
+ 100 104 108 112 116 120
+ 124 128 132 136 140 144
+ 148 152 156 160 164 168
+ 172 176 180 184 188 192
+ 196 200 204 208 212 216
+ 220 224 228 232 236 240
+ 244 248 252 256 260 264
+ 268 272 276 280 284 288
+ 292 296 300 304 308 312
+ 316 320 324 328 332 336
+ 340 344 348 352 356 360
+ 364 368 372 376 380 384
+ 388 392 396 400 404 408
+ 412 416 420 424 428 432
+ 436 440 444 448 452 456
+ 460 464 468 472 476 480
+ 484 488 492 496 500 504
+ 508 512 516 520 524 528
+ 532 536 540 544 548 552
+ 556 560 564 568 572 576
+ 580 584 588 592 596 600
+ 604 608 612 616 620 624
+ 628 632 636 640 644 648
+ 652 656 660 664 668 672
+ 676 680 684 688 692 696
+ 700 704 708 712 716 720
+ 724 728 732 736 740 744
+ 748 752 756 760 764 768
+ 772 776 780 784 788 792
+ 796 800 804 808 812 816
+ 820 824 828 832 836 840
+ 844 848 852 856 860 864
+ 868 872 876 880 884 888
+ 892 896 900 904 908 912
+ 916 920 924 928 932 936
+ 940 944 948 952 956 960
+ 964 968 972 976 980 984
+ 988 992 996 1000 1004 1008
+ 1012 1016 1020 1024 1028 1032
+ 1036 1040 1044 1048 1052 1056
+ 1060 1064 1068 1072 1076 1080
+ 1084 1088 1092 1096 1100 1104
+ 1108 1112 1116 1120 1124 1128
+ 1132 1136 1140 1144 1148 1152
+ 1156 1160 1164 1168 1172 1176
+ 1180 1184 1188 1192 1196 1200
+ 1204 1208 1212 1216
+
+
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10
+
+
+
+
+
diff --git a/inputFiles/coverageSmokes/synthetic_thermal_coverage.vtu b/inputFiles/coverageSmokes/synthetic_thermal_coverage.vtu
new file mode 100644
index 00000000000..22a4b7267e2
--- /dev/null
+++ b/inputFiles/coverageSmokes/synthetic_thermal_coverage.vtu
@@ -0,0 +1,505 @@
+
+
+
+
+
+
+
+
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 1 1 1 1 1 1
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 2
+ 2 2 2 2 2 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3 3 3 3
+ 3 3 3
+
+
+ 520 975 1836 1932 2098 3376
+ 3644 4178 5039 5192 5411 6072
+ 6696 7676 8618 8935 9690 11258
+ 12212 12338 12766 12842 13299 13918
+ 14854 15324 15950 15991 16257 16289
+ 16572 16930 17104 18026 18291 18771
+ 19030 19412 20099 20306 21006 21295
+ 22093 22562 22601 22629 22725 23431
+ 23562 23710 24402 26402 26751 26785
+ 26802 27381 27440 27573 27600 27861
+ 28174 28354 29093 29261 29886 30264
+ 30716 30898 31017 31473 31625 31676
+ 31715 32195 32254 32320 32428 32588
+ 32843 33290 33820 34400 35070 35357
+ 35479 36099 36291 37005 37140 37160
+ 37197 37869 38623 38677 39076 40861
+ 42392 43732 44198 44321 44687 44732
+ 45067 45283 46337 46935 47772 48574
+ 48706 48916 49106 49174 50187 50378
+ 50541 50724 51424 51847 52177 52846
+ 54868 55469 55564 55825 56090 56091
+ 56865 57191 57469 57661 57726 57991
+ 58250 60085 61415 61551 62037 62324
+ 63092 64707 64819 65497 65499 66206
+ 67103 67260 67467 67560 67936 68207
+ 69563 70156 70174 70457 70976 71131
+ 71311 71647 72638 73005 73009 73570
+ 73900 74090 74171 74385 74526 75396
+ 76045 76166 77438 77672 77988 78084
+ 78161 78263 78406 78448 78540 78686
+ 79661 80009 80765 81987 82145 82812
+ 82882 83831 85522 86220 86759 87284
+ 87925 89260 90466 90467 93090 93909
+ 94722 95217 95290 95854 96760 96843
+ 97566 98053 98846 99298 99545 99931
+ 100093 100618 101653 102032 103387 103400
+ 103790 104165 105077 105870 106107 108215
+ 108218 108537 109348 109715 110012 110348
+ 110599 110739 111625 111654 111682 111689
+ 111808 112143 112451 113305 113868 114381
+ 114605 114784 114794 115094 115816 116958
+ 117422 117497 117588 117853 118299 119386
+ 121216 121449 122738 122982 123281 123881
+ 124521 124548 124604 125261 126093 126705
+ 126821 127594 127715 128439 128468 128572
+ 128599 129581 131265 131604 132496 132677
+ 133770 133771 135335
+
+
+
+
+ 4965.035032908142 2264.617872445472 -500 4239.623819534134 3176.757827165824 -500
+ 16106.88076660959 7183.896108400462 -500 5044.746611305187 3128.0676533446 -500
+ 15268.22134232392 7400.008845890813 -500 4621.861658340067 2088.00322509168 -2000
+ 14928.25639073351 7372.64062876512 -2000 15927.66025517862 7191.62096586634 -2000
+ 4424.939030918991 3337.98909972347 -2000 4910.799373610424 2801.438541614166 -2000
+ 4854.935623733791 2360.778028641292 -796.753607987 5027.030973577464 2490.707181355228 -800.695073783
+ 4824.372786902584 2654.83291518825 -796.168541165 15425.12484090007 7309.49984638848 -1557.25766899
+ 15433.53351780857 7550.393954079803 -1557.32988096 4538.908320929658 2735.994878495637 -792.453857678
+ 4585.770140659286 2442.022734212771 -792.840201573 15654.61183291513 7428.327763482946 -1558.16154905
+ 15688.90367429904 7219.800498002436 -1558.10101004 15541.85486219347 7039.013576270028 -1557.99751826
+ 4352.849201125719 2577.930554330891 -791.832178832 15827.97292406456 7578.191430109337 -1557.3110404
+ 16018.31309188429 7506.424857686015 -1554.75842912 5180.696022101536 2982.383337143748 -805.33603948
+ 15872.34349239403 6985.663840996416 -1556.87089382 16155.27732517789 7073.743355157525 -1551.81837832
+ 16231.8056830548 7376.564401063879 -1549.75744275 4930.847681726383 2947.193353553401 -798.35686337
+ 4755.434803995762 3141.428332389566 -794.978717806 4693.229255967135 2917.204831717634 -794.062341692
+ 5103.749889229026 2719.124790403613 -802.870636219 4443.519939434904 3048.430669923569 -791.950226558
+ 15922.53773134394 7313.225873204623 -1556.26057398 5027.030971339942 2490.707181337909 -950.695073724
+ 4824.372784553089 2654.832915040218 -946.168541122 15425.1248377579 7309.499845142172 -1707.25766897
+ 15433.53351446838 7550.393952604836 -1707.32988093 4538.908318000885 2735.994878539103 -942.453857657
+ 4585.770137630408 2442.02273425241 -942.840201545 15654.61182936576 7428.327761902474 -1708.16154905
+ 15688.90367092147 7219.80049652948 -1708.10101005 15541.85485910991 7039.013575056141 -1707.99751825
+ 15827.97292024998 7578.191428349804 -1707.31104043 15633.98146066355 7650.96960359091 -1708.17291011
+ 16018.31308777003 7506.424855697938 -1704.7584292 5180.696020758819 2982.383335988904 -955.336039434
+ 15872.34348895454 6985.66383940205 -1706.87089386 16155.27732113549 7073.743353097027 -1701.81837843
+ 4930.847679898498 2947.1933528748 -948.356863329 4755.434801783967 3141.428331779618 -944.978717771
+ 4693.229253610685 2917.204831456121 -944.062341661 5103.74988764245 2719.124789976916 -952.870636171
+ 4443.519936573133 3048.430670019949 -941.950226549 15922.53772754576 7313.225871418299 -1706.26057403
+ 15631.7521007776 7539.302538322157 -1198.25596637 15588.35129136617 6679.158504323897 -1357.64658739
+ 15643.63246283868 7770.830822385116 -1391.68963049 15461.62201165479 7001.667271148624 -864.896569104
+ 4428.166920219104 2704.268309130888 -640.791184835 16159.80318500536 6996.914529080244 -1435.92019943
+ 16228.21040849824 6991.495037977197 -1268.79449181 4917.943991933625 2031.751442316643 -674.967832164
+ 15567.06969935776 7253.434391288517 -1460.0730734 15971.78521805879 7641.868965532365 -1472.46452965
+ 15380.93286508278 7421.057931377661 -1028.68522669 5208.658525969562 2879.504681475467 -688.374547851
+ 16128.19486994485 7728.690277970626 -1301.23409517 16495.07276194134 7668.692740324926 -780.130497947
+ 16208.98098672995 7381.992356408715 -1095.99593586 4505.064439706515 3196.091156712393 -705.650182757
+ 15688.0062973088 6650.509640038711 -1190.50503072 15661.8857759854 7292.545500402554 -1324.2522263
+ 15714.04163206046 8081.51161518462 -1099.43174016 15822.97310683744 6826.312815714578 -689.795592256
+ 16067.62006794119 7457.920527276538 -1399.39500644 4797.658194608125 2496.648630791328 -719.654216036
+ 15254.79904381521 7369.514562716516 -1395.60362123 15552.46433066739 7585.777078286873 -1497.68147388
+ 15150.51319270264 7083.206093482272 -1141.38821205 15509.0312886289 6894.254235761543 -1496.35823024
+ 14941.1054517046 6549.430169952467 -1048.0009123 15710.81882013538 6838.963962032019 -1432.12004984
+ 4224.124366521088 2674.120262010353 -726.006039536 15932.37229415238 7138.95984656683 -1494.58962605
+ 15207.78413831427 7044.66428168552 -1334.71402278 15954.36992499789 6572.620671423286 -948.841834276
+ 15286.67888392361 6976.901421598507 -1428.45591986 15913.58235195537 7041.503906388943 -1376.22532424
+ 15092.63384389589 7504.08129636816 -1235.42220432 16034.86254689591 7715.727590828663 -907.924185739
+ 16180.45901368084 7480.715669306353 -1223.49432616 16397.69851362052 6909.665773308187 -852.65886241
+ 15619.59167894527 6905.716645096024 -1264.24351644 15303.30030318709 7256.449051501891 -1505.82923813
+ 15676.78607514222 7837.556830215353 -1451.01874439 15882.39247652947 7943.670198123707 -678.778784493
+ 15861.60797771494 7450.709907998588 -1510.73791507 15668.9110525658 7336.720245216547 -1400.74597627
+ 15423.96605542521 7569.822675340642 -1436.88438418 15247.75688072624 7649.314628467463 -789.476409662
+ 15735.63664181413 6982.5989554007 -1110.98954118 15942.68793236336 7073.363581380735 -1017.16449274
+ 15919.85778659095 7319.615563004485 -1446.02692966 16075.59095682218 7341.782373474422 -1337.86553015
+ 15940.48859618625 7732.449457341471 -1026.37991058 15836.59086224634 6820.897632889554 -1510.35098416
+ 4866.240980360214 2822.202778660051 -746.013094152 15629.08549224558 7218.108221210961 -1509.57674085
+ 15782.237990741 7524.326609718499 -1267.17083606 4454.168411227873 2468.905674602678 -746.463351659
+ 5214.327154006901 2420.596253121998 -761.343798459 16047.74609068493 7016.730328775448 -1196.55247288
+ 15522.081641043 7685.15804370933 -1633.17291005 16143.14836261805 7684.611120512637 -1625.42623808
+ 5113.119218155823 2650.776221203797 -878.01300148 4438.502098004604 2827.054569494013 -866.876904924
+ 15669.0919484872 7206.668016394629 -1633.10101004 4932.68393626033 3091.933201311761 -867.310283501
+ 16068.11177085479 6983.40062545337 -1635.81286343 4842.861556866336 2420.7882716663 -854.299500285
+ 15969.73666844154 7398.189049176358 -1651.19017377 4774.989413562239 2790.891114275366 -892.076153195
+ 16106.6442347609 7366.151948930444 -1602.07496845 15777.08261067931 6849.039018720965 -1607.73717514
+ 4626.315198216476 3116.441152568739 -894.967245743 15363.38775905862 7071.156494753336 -1653.12327022
+ 15737.8287806317 7489.791276979209 -1606.76737479 15404.35700540844 7430.187248715652 -1604.68064833
+ 15363.63305672098 7451.408446755489 -1660.38135891 15751.20511985038 6786.58795617967 -1659.96683382
+ 4895.692949037504 2788.187451423158 -843.782989232 4783.888260069965 2471.042768079352 -901.889718059
+ 5137.013252143095 3099.127467516122 -907.966421585 4655.474897541904 3020.152471776383 -841.433431038
+ 15653.6550295367 7540.609603105589 -1666.8269957 4607.109415101526 2622.068933021229 -837.64935387
+ 15462.0840697585 7029.215372842845 -1607.80299279 15878.83588765235 7144.556324795994 -1598.31555276
+ 4497.423854589558 2494.704663571886 -900.312088875 5090.193000199215 2517.755364875075 -841.318200864
+ 4680.142194418026 2247.799765940356 -1104.03815646 5180.62002465031 2840.372048415013 -1426.38393825
+ 4711.565458381153 3012.112225486221 -1058.73660069 5210.713403346416 3071.074390369278 -1225.76020641
+ 5103.910093366013 3315.942235574666 -1623.55757529 15890.16254027279 7160.562769208526 -1827.35651757
+ 4325.456363519454 2813.044200010701 -1239.70846503 4912.962995930453 2352.69126479524 -1772.92335146
+ 4331.983469469798 2765.452757463227 -1127.14468242 4975.552645516593 2809.061832523604 -1143.12621502
+ 4747.959624655766 3233.903210469751 -1325.46932153 15415.23591294995 7689.268269266133 -1797.6598104
+ 4285.754067726776 2922.58189451297 -1016.52850313 4651.712507124432 2557.29906919196 -1017.80914255
+ 4303.078058492369 3126.302364182408 -1437.45367321 4199.853775008943 3387.278009362184 -1810.86927332
+ 5220.643360624215 2443.350086703095 -1211.71553476 4204.603099199282 2796.366310848328 -1669.61260111
+ 4619.843173551247 2441.254518502499 -1187.11682689 5083.277609415716 2670.142739337042 -1304.19165914
+ 15446.42948966427 6774.292626715782 -1783.76426724 4282.706004601011 2751.798665379018 -1340.74135634
+ 5072.725087103064 2577.950964907845 -1009.66352762 4724.269344933862 2240.577313565794 -1366.45485389
+ 4308.751283514604 2459.953969418568 -995.285011106 4759.396877877594 2245.130263936703 -995.704891418
+ 4318.802333840854 3308.788765154862 -1565.26339506 15869.83475373037 7733.671023234994 -1768.27292918
+ 15561.77002257561 7305.163572556931 -1771.16490029 4863.779019897612 2609.252823507924 -1076.14901471
+ 15746.81498876912 6592.70557226214 -1903.03071132 4625.517328255863 2531.499860370648 -1487.01775615
+ 4907.328358698085 3403.617876850702 -1482.8225864 4768.523540012982 2922.213103096319 -1001.95196947
+ 4719.472838927672 3034.171181326662 -1201.65544467 5080.581172948033 3004.965604640774 -1089.13768028
+ 4439.836328388086 2718.112629755352 -1062.87981432 4072.909365349799 2432.266693825141 -1855.97606747
+ 5394.324870014113 2946.503029414001 -1532.18029669 4472.011585418446 3192.374940973824 -989.446448706
+ 4649.604395435363 3055.594646981211 -1122.43312308 5328.251249968101 2991.314982693548 -1864.61390066
+ 15804.13639802059 6826.230931620317 -1760.03319914 4483.819662547135 2360.183722401803 -1056.07107237
+ 4269.158182149175 2344.506364523959 -1231.0681833 4841.880390204386 2656.809356025562 -1242.06865008
+ 4735.662909395504 2744.623240825328 -1588.61237499 4995.773918247291 2850.454100091352 -1039.92636722
+ 16020.71003904426 7134.495278261501 -1754.14124927 4787.955751426627 2782.543484338947 -1370.59832268
+ 5283.356206276125 2240.903612790237 -1335.14721689 4515.784977979964 2489.68483577903 -1290.12356728
+
+
+ 5023.7387023
+
+
+ 18207.275396
+
+
+
+
+
+
+ 60 87 71 103 28 31
+ 29 106 68 72 54 100
+ 55 71 87 97 29 31
+ 15 106 55 84 71 97
+ 88 92 71 108 31 82
+ 15 106 54 70 78 92
+ 64 72 68 100 56 71
+ 76 97 80 100 64 101
+ 56 97 74 103 57 64
+ 89 101 57 73 67 99
+ 56 71 97 103 62 94
+ 98 102 77 96 63 102
+ 54 70 92 111 17 14
+ 13 77 62 96 77 102
+ 62 81 79 83 56 74
+ 97 102 54 92 88 108
+ 58 65 61 75 71 60
+ 103 108 54 100 70 111
+ 89 101 64 104 10 11
+ 12 75 57 89 85 101
+ 13 17 77 107 60 71
+ 92 108 54 78 70 100
+ 54 68 100 111 62 81
+ 83 102 65 75 69 106
+ 62 86 81 97 59 83
+ 81 102 60 108 92 111
+ 31 69 82 106 26 32
+ 83 96 57 85 89 91
+ 2 73 4 95 62 83
+ 79 107 15 106 82 109
+ 59 81 87 102 54 92
+ 108 111 81 97 87 102
+ 62 97 81 102 15 16
+ 12 109 11 12 75 110
+ 62 98 97 102 19 105
+ 24 107 62 77 96 107
+ 75 82 69 106 24 83
+ 18 107 3 0 58 65
+ 77 17 96 107 32 18
+ 24 83 32 18 83 107
+ 62 96 83 107 64 100
+ 68 101 62 83 96 102
+ 25 83 24 105 68 64
+ 101 104 66 71 103 108
+ 24 105 83 107 15 12
+ 106 109 19 24 18 107
+ 74 97 87 103 87 97
+ 71 103 62 93 77 107
+ 54 108 90 111 82 106
+ 75 109 32 24 25 83
+ 62 79 93 107 83 105
+ 79 107 58 69 65 75
+ 87 97 74 102 32 83
+ 96 107 16 75 12 109
+ 18 96 17 107 12 106
+ 75 110 32 96 18 107
+ 32 17 18 96 13 18
+ 17 107 27 28 29 106
+ 13 19 18 107 12 30
+ 27 106 75 106 12 109
+ 12 29 15 106 32 21
+ 17 96 22 21 32 96
+ 3 0 1 58 27 29
+ 12 106 77 102 94 62
+ 116 118 46 120 40 128
+ 116 134 35 116 40 128
+ 35 128 40 134 33 114
+ 34 131 35 40 116 125
+ 46 116 40 129 41 40
+ 116 129 34 114 51 121
+ 116 120 40 134 41 116
+ 125 129 48 124 121 132
+ 114 121 117 132 18 126
+ 116 127 114 121 34 131
+ 115 121 37 124 18 127
+ 116 136 47 53 46 120
+ 52 115 37 124 112 116
+ 128 134 41 46 40 129
+ 116 112 126 134 41 116
+ 40 125 116 126 112 127
+ 48 34 51 121 39 35
+ 40 134 48 50 121 124
+ 131 135 121 138 37 121
+ 50 124 37 121 115 138
+ 116 126 18 137 53 40
+ 120 134 15 115 133 135
+ 119 135 131 138 35 41
+ 40 125 121 131 119 135
+ 17 126 18 127 116 18
+ 136 137 113 120 122 126
+ 115 121 135 138 117 121
+ 114 130 52 37 50 124
+ 119 121 114 131 23 27
+ 117 130 118 120 116 137
+ 114 121 119 130 11 119
+ 12 139 13 17 18 127
+ 119 130 121 135 130 133
+ 121 135 115 121 133 135
+ 53 39 40 134 116 126
+ 120 134 117 130 27 133
+ 119 130 12 139 37 131
+ 121 138 118 116 123 137
+ 120 126 116 137 22 122
+ 32 126 120 122 126 137
+ 21 22 32 126 10 12
+ 119 135 19 123 136 137
+ 19 13 18 136 121 124
+ 115 133 29 133 130 135
+ 12 130 119 135 19 136
+ 18 137 53 120 42 134
+ 31 29 15 133 34 121
+ 37 131 29 15 133 135
+ 121 130 117 133 42 53
+ 44 120 114 130 119 139
+ 29 130 12 135 24 123
+ 19 137 32 126 122 137
+ 17 32 18 126 37 38
+ 131 138 39 36 35 134
+ 32 18 126 137 53 42
+ 39 134 116 136 123 137
+ 10 16 12 135 50 34
+ 48 121 34 37 38 131
+ 21 32 17 126 24 19
+ 18 137 50 37 34 121
+ 29 27 130 133 29 12
+ 15 135 29 27 12 130
+ 16 20 15 135 32 25
+ 24 137 27 30 12 130
+ 15 12 16 135 18 32
+ 24 137 40 120 46 53
+ 46 120 40 116 30 139
+ 12 130 140 149 148 169
+ 37 153 152 173 148 149
+ 140 158 150 143 174 185
+ 146 150 174 185 144 157
+ 147 186 37 173 152 179
+ 153 162 34 165 42 53
+ 39 168 149 174 148 180
+ 140 169 148 176 148 169
+ 149 180 146 150 185 191
+ 150 159 143 185 149 158
+ 148 174 48 45 51 173
+ 37 153 38 164 43 39
+ 36 168 51 162 34 173
+ 34 162 153 173 148 176
+ 169 180 34 153 37 173
+ 37 34 38 153 150 159
+ 185 191 143 149 174 185
+ 152 173 153 176 9 8
+ 177 181 142 169 176 180
+ 156 158 149 185 163 141
+ 189 190 141 171 163 189
+ 152 142 173 176 6 7
+ 145 170 40 39 53 168
+ 42 167 53 168 150 161
+ 154 189 147 177 155 181
+ 5 9 177 181 44 53
+ 167 188 48 51 34 173
+ 34 37 50 173 141 154
+ 171 189 158 174 149 185
+ 141 171 154 172 159 150
+ 189 191 153 173 142 176
+ 142 175 169 180 166 172
+ 171 186 172 178 171 186
+ 142 153 169 187 169 175
+ 149 180 142 173 153 187
+ 161 189 150 191 142 169
+ 153 176 162 169 153 187
+ 41 40 46 182 162 153
+ 173 187 41 168 40 182
+ 36 39 35 168 53 168
+ 167 188 158 146 174 185
+ 158 184 146 185 53 40
+ 168 188 48 34 50 173
+ 153 176 169 183 159 189
+ 163 191 40 41 35 168
+ 46 182 40 188 142 169
+ 175 187 145 167 151 168
+ 50 49 48 173 40 182
+ 168 188 40 35 39 168
+ 163 189 161 191 167 168
+ 145 188 53 46 40 188
+ 145 168 160 182 168 182
+ 145 188 53 47 46 188
+ 179 50 37 173 179 37
+ 50 52 168 42 43 39
+
+
+ 4 8 12 16 20 24
+ 28 32 36 40 44 48
+ 52 56 60 64 68 72
+ 76 80 84 88 92 96
+ 100 104 108 112 116 120
+ 124 128 132 136 140 144
+ 148 152 156 160 164 168
+ 172 176 180 184 188 192
+ 196 200 204 208 212 216
+ 220 224 228 232 236 240
+ 244 248 252 256 260 264
+ 268 272 276 280 284 288
+ 292 296 300 304 308 312
+ 316 320 324 328 332 336
+ 340 344 348 352 356 360
+ 364 368 372 376 380 384
+ 388 392 396 400 404 408
+ 412 416 420 424 428 432
+ 436 440 444 448 452 456
+ 460 464 468 472 476 480
+ 484 488 492 496 500 504
+ 508 512 516 520 524 528
+ 532 536 540 544 548 552
+ 556 560 564 568 572 576
+ 580 584 588 592 596 600
+ 604 608 612 616 620 624
+ 628 632 636 640 644 648
+ 652 656 660 664 668 672
+ 676 680 684 688 692 696
+ 700 704 708 712 716 720
+ 724 728 732 736 740 744
+ 748 752 756 760 764 768
+ 772 776 780 784 788 792
+ 796 800 804 808 812 816
+ 820 824 828 832 836 840
+ 844 848 852 856 860 864
+ 868 872 876 880 884 888
+ 892 896 900 904 908 912
+ 916 920 924 928 932 936
+ 940 944 948 952 956 960
+ 964 968 972 976 980 984
+ 988 992 996 1000 1004 1008
+ 1012 1016 1020 1024 1028 1032
+ 1036 1040 1044 1048 1052 1056
+ 1060 1064 1068 1072 1076 1080
+ 1084 1088 1092 1096 1100 1104
+ 1108 1112 1116
+
+
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10 10 10 10
+ 10 10 10
+
+
+
+
+
diff --git a/inputFiles/multiscalePreconditioner/cube_coverage_smoke.xml b/inputFiles/multiscalePreconditioner/cube_coverage_smoke.xml
new file mode 100644
index 00000000000..68bf39931e7
--- /dev/null
+++ b/inputFiles/multiscalePreconditioner/cube_coverage_smoke.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inputFiles/wavePropagation/acous3D_dg_order2_coverage.xml b/inputFiles/wavePropagation/acous3D_dg_order2_coverage.xml
new file mode 100644
index 00000000000..0ce33323759
--- /dev/null
+++ b/inputFiles/wavePropagation/acous3D_dg_order2_coverage.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inputFiles/wavePropagation/acous3D_dg_order3_overlay.xml b/inputFiles/wavePropagation/acous3D_dg_order3_overlay.xml
new file mode 100644
index 00000000000..f50c3ae6a86
--- /dev/null
+++ b/inputFiles/wavePropagation/acous3D_dg_order3_overlay.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inputFiles/wavePropagation/acous3D_dg_setup_only_overlay.xml b/inputFiles/wavePropagation/acous3D_dg_setup_only_overlay.xml
new file mode 100644
index 00000000000..b360f5fe23f
--- /dev/null
+++ b/inputFiles/wavePropagation/acous3D_dg_setup_only_overlay.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/scripts/check_coverage_thresholds.py b/scripts/check_coverage_thresholds.py
new file mode 100755
index 00000000000..5d95baf09c7
--- /dev/null
+++ b/scripts/check_coverage_thresholds.py
@@ -0,0 +1,869 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-only
+
+"""Validate and enforce repository-owned LLVM coverage thresholds."""
+
+import argparse
+import hashlib
+import html
+import json
+import math
+import pathlib
+import re
+import sys
+
+
+COVERAGE_SUMMARY_SCHEMA_VERSION = 3
+COVERAGE_POLICY_SCHEMA_VERSION = 1
+COVERAGE_CONTRACT_ID = "geos-llvm-source-coverage-v1"
+CANONICAL_METRICS = ( "regions", "functions", "lines", "branches" )
+DISPLAY_METRICS = ( "lines", "functions", "branches", "regions" )
+SUPPLEMENTAL_METRICS = (
+ ( "native_branch_outcomes", "Native branch outcomes" ),
+)
+if set( DISPLAY_METRICS ) != set( CANONICAL_METRICS ):
+ raise RuntimeError( "display metrics must match canonical coverage metrics" )
+
+METRIC_SEMANTICS = {
+ "canonical_metrics": "llvm-cov-summary-instantiation-groups-v1",
+ "native_branch_outcomes": "llvm-cov-lcov-emitted-brda-records-v1",
+ "source_selection": "llvm-cov-sources-and-ignore-regex-v1",
+}
+OBJECT_SELECTION = {
+ "contract_version": 1,
+ "primary_object": "bin/geosx",
+ "additional_object_directory": "lib",
+ "additional_object_globs": [ "*.dll", "*.dylib", "*.so", "*.so.*" ],
+ "excluded_library_basenames": [ "libgtest*", "libtestingUtilities.*" ],
+ "required_object_basenames": [ "geosx", "libmainInterface" ],
+ "coverage_mapping_section": "__llvm_covmap",
+ "test_executables": "excluded",
+}
+MEASUREMENT_KEYS = {
+ "commit_sha",
+ "tree_sha",
+ "contract_id",
+ "contract_fingerprint",
+ "container",
+ "toolchain",
+ "build_config",
+ "metric_semantics",
+ "object_selection",
+}
+CMAKE_FIXED_CONFIG_NAMES = {
+ "BLT_CXX_STD",
+ "BUILD_SHARED_LIBS",
+ "CMAKE_BUILD_TYPE",
+ "CMAKE_CUDA_ARCHITECTURES",
+ "CMAKE_C_EXTENSIONS",
+ "CMAKE_C_STANDARD",
+ "CMAKE_C_STANDARD_REQUIRED",
+ "CMAKE_CXX_EXTENSIONS",
+ "CMAKE_CXX_STANDARD",
+ "CMAKE_CXX_STANDARD_REQUIRED",
+ "CMAKE_INTERPROCEDURAL_OPTIMIZATION",
+ "CMAKE_POSITION_INDEPENDENT_CODE",
+ "CMAKE_UNITY_BUILD",
+ "GEOS_GLOBALINDEX_TYPE",
+ "GEOS_GLOBALINDEX_TYPE_FLAG",
+ "GEOS_LA_INTERFACE",
+ "GEOS_LA_INTERFACE_HYPRE",
+ "GEOS_LOCALINDEX_TYPE",
+ "GEOS_LOCALINDEX_TYPE_FLAG",
+}
+CMAKE_REQUIRED_CONFIG_NAMES = {
+ "BLT_CXX_STD",
+ "BUILD_SHARED_LIBS",
+ "CMAKE_BUILD_TYPE",
+ "ENABLE_COVERAGE",
+ "ENABLE_CUDA",
+ "ENABLE_HIP",
+ "ENABLE_HYPRE",
+ "ENABLE_MPI",
+ "ENABLE_OPENMP",
+ "ENABLE_TRILINOS",
+ "GEOS_BUILD_SHARED_LIBS",
+ "GEOS_ENABLE_BOUNDS_CHECK",
+ "GEOS_ENABLE_LLVM_SOURCE_COVERAGE",
+ "GEOS_GLOBALINDEX_TYPE",
+ "GEOS_LA_INTERFACE",
+ "GEOS_LOCALINDEX_TYPE",
+ "LVARRAY_BOUNDS_CHECK",
+ "RAJA_ENABLE_CUDA",
+ "RAJA_ENABLE_HIP",
+ "RAJA_ENABLE_OPENMP",
+}
+
+
+def require_schema_version(
+ document: dict, description: str, expected_version: int
+) -> None:
+ version = document.get( "schema_version" )
+ if (
+ isinstance( version, bool )
+ or not isinstance( version, int )
+ or version != expected_version
+ ):
+ raise ValueError(
+ f"{description} schema_version must be the integer {expected_version}"
+ )
+
+
+def load_json( path: pathlib.Path ) -> dict:
+ try:
+ with path.open( encoding="utf-8" ) as stream:
+ document = json.load( stream )
+ except ( OSError, json.JSONDecodeError ) as error:
+ raise ValueError( f"cannot read {path}: {error}" ) from error
+ if not isinstance( document, dict ):
+ raise ValueError( f"{path} must contain a JSON object" )
+ return document
+
+
+def require_integer( value, description: str, minimum: int = 0 ) -> int:
+ if isinstance( value, bool ) or not isinstance( value, int ) or value < minimum:
+ raise ValueError( f"{description} must be an integer >= {minimum}" )
+ return value
+
+
+def require_exact_keys( value: dict, expected: set[str], description: str ) -> None:
+ actual = set( value )
+ if actual != expected:
+ missing = sorted( expected - actual )
+ unexpected = sorted( actual - expected )
+ details = []
+ if missing:
+ details.append( "missing " + ", ".join( missing ) )
+ if unexpected:
+ details.append( "unexpected " + ", ".join( unexpected ) )
+ raise ValueError( f"{description} fields are invalid: {'; '.join( details )}" )
+
+
+def require_string( value: object, description: str, allow_newlines=False ) -> str:
+ if not isinstance( value, str ) or not value:
+ raise ValueError( f"{description} must be a nonempty string" )
+ forbidden = "\0" if allow_newlines else "\0\r\n"
+ if any( character in value for character in forbidden ):
+ raise ValueError( f"{description} contains a control character" )
+ if value != value.rstrip():
+ raise ValueError( f"{description} must not have trailing whitespace" )
+ return value
+
+
+def validate_repo_path( value: object, description: str ) -> str:
+ path = require_string( value, description )
+ normalized = pathlib.PurePosixPath( path )
+ if (
+ normalized.is_absolute()
+ or normalized.as_posix() != path
+ or ".." in normalized.parts
+ or "\\" in path
+ ):
+ raise ValueError( f"{description} must be a normalized repository-relative path" )
+ return path
+
+
+def validate_scoped_path( value: object, scope: str, description: str ) -> str:
+ path = validate_repo_path( value, description )
+ scope_prefix = f"{scope}/"
+ if not path.startswith( scope_prefix ) or len( path ) == len( scope_prefix ):
+ raise ValueError( f"{description} is outside the scope" )
+ return path
+
+
+def validate_metric(
+ metric: object,
+ description: str,
+ minimum_total: int = 1,
+ allowed_extra_fields: set[str] | None = None,
+) -> dict:
+ if not isinstance( metric, dict ):
+ raise ValueError( f"{description} must be an object" )
+ expected_fields = { "covered", "total", "not_covered", "percent" }
+ if allowed_extra_fields:
+ expected_fields.update( allowed_extra_fields )
+ require_exact_keys( metric, expected_fields, description )
+
+ covered = require_integer( metric.get( "covered" ), f"{description}.covered" )
+ total = require_integer(
+ metric.get( "total" ), f"{description}.total", minimum_total
+ )
+ not_covered = require_integer(
+ metric.get( "not_covered" ), f"{description}.not_covered"
+ )
+ if covered > total:
+ raise ValueError( f"{description}.covered cannot exceed total" )
+ if not_covered != total - covered:
+ raise ValueError( f"{description}.not_covered is inconsistent" )
+
+ percent = metric.get( "percent" )
+ if isinstance( percent, bool ) or not isinstance( percent, ( int, float ) ):
+ raise ValueError( f"{description}.percent must be numeric" )
+ if not math.isfinite( percent ):
+ raise ValueError( f"{description}.percent must be finite" )
+ expected_percent = round( 100.0 * covered / total if total else 0.0, 6 )
+ if abs( float( percent ) - expected_percent ) > 1.0e-6:
+ raise ValueError( f"{description}.percent is inconsistent with its counts" )
+
+ return {
+ "covered": covered,
+ "total": total,
+ "not_covered": not_covered,
+ "percent": expected_percent,
+ }
+
+
+def validate_container( raw_container: object ) -> dict:
+ if not isinstance( raw_container, dict ):
+ raise ValueError( "measurement.container must be an object" )
+ require_exact_keys(
+ raw_container,
+ { "image", "image_id", "image_digests" },
+ "measurement.container",
+ )
+ image = require_string( raw_container.get( "image" ), "measurement.container.image" )
+ if any( character.isspace() for character in image ):
+ raise ValueError( "measurement.container.image must be a container reference" )
+ image_id = require_string(
+ raw_container.get( "image_id" ), "measurement.container.image_id"
+ )
+ if re.fullmatch( r"sha256:[0-9a-f]{64}", image_id ) is None:
+ raise ValueError( "measurement.container.image_id must be an immutable SHA256 ID" )
+
+ raw_digests = raw_container.get( "image_digests" )
+ if not isinstance( raw_digests, list ) or not raw_digests:
+ raise ValueError(
+ "measurement.container.image_digests must be a nonempty array"
+ )
+ digests = []
+ for index, raw_digest in enumerate( raw_digests ):
+ digest = require_string(
+ raw_digest, f"measurement.container.image_digests[{index}]"
+ )
+ if re.fullmatch( r"[^\s@]+@sha256:[0-9a-f]{64}", digest ) is None:
+ raise ValueError(
+ "measurement.container.image_digests entries must be immutable SHA256 digests"
+ )
+ digests.append( digest )
+ if digests != sorted( set( digests ) ):
+ raise ValueError(
+ "measurement.container.image_digests must be sorted and unique"
+ )
+ return { "image": image, "image_id": image_id, "image_digests": digests }
+
+
+def tool_major_from_version( version: str, description: str ) -> int:
+ match = re.search( r"\bversion\s+([0-9]+)(?:[.\s]|$)", version )
+ if match is None:
+ raise ValueError( f"{description} does not contain a version number" )
+ return int( match.group( 1 ) )
+
+
+def validate_toolchain( raw_toolchain: object, expected_major: int ) -> dict:
+ if not isinstance( raw_toolchain, dict ):
+ raise ValueError( "measurement.toolchain must be an object" )
+ keys = {
+ "c_compiler_version",
+ "cxx_compiler_version",
+ "llvm_cov_version",
+ "llvm_package_versions",
+ "compiler_target",
+ }
+ require_exact_keys( raw_toolchain, keys, "measurement.toolchain" )
+ string_keys = keys - { "llvm_package_versions" }
+ toolchain = {
+ name: require_string(
+ raw_toolchain.get( name ),
+ f"measurement.toolchain.{name}",
+ allow_newlines=name.endswith( "_version" ),
+ )
+ for name in string_keys
+ }
+ raw_packages = raw_toolchain.get( "llvm_package_versions" )
+ if not isinstance( raw_packages, list ) or not raw_packages:
+ raise ValueError(
+ "measurement.toolchain.llvm_package_versions must be a nonempty array"
+ )
+ packages = []
+ for index, raw_package in enumerate( raw_packages ):
+ package = require_string(
+ raw_package,
+ f"measurement.toolchain.llvm_package_versions[{index}]",
+ )
+ if re.fullmatch( r"[A-Za-z0-9.+-]+=[^\s=]+", package ) is None:
+ raise ValueError(
+ "measurement.toolchain.llvm_package_versions entries must be package=version"
+ )
+ packages.append( package )
+ if packages != sorted( set( packages ) ):
+ raise ValueError(
+ "measurement.toolchain.llvm_package_versions must be sorted and unique"
+ )
+ toolchain["llvm_package_versions"] = packages
+ target = toolchain["compiler_target"]
+ if any( character.isspace() for character in target ):
+ raise ValueError( "measurement.toolchain.compiler_target is invalid" )
+ for name in (
+ "c_compiler_version",
+ "cxx_compiler_version",
+ "llvm_cov_version",
+ ):
+ if tool_major_from_version(
+ toolchain[name], f"measurement.toolchain.{name}"
+ ) != expected_major:
+ raise ValueError(
+ f"measurement.toolchain.{name} does not match tool.major"
+ )
+ return { name: toolchain[name] for name in sorted( toolchain ) }
+
+
+def validate_build_config( raw_config: object ) -> dict:
+ if not isinstance( raw_config, dict ) or not raw_config:
+ raise ValueError( "measurement.build_config must be a nonempty object" )
+ build_type = raw_config.get( "CMAKE_BUILD_TYPE" )
+ if not isinstance( build_type, str ) or not build_type:
+ raise ValueError(
+ "measurement.build_config.CMAKE_BUILD_TYPE must be a nonempty string"
+ )
+ build_type_suffix = re.sub( r"[^A-Za-z0-9]", "_", build_type ).upper()
+ active_flag_names = {
+ "CMAKE_C_FLAGS",
+ "CMAKE_CXX_FLAGS",
+ "CMAKE_EXE_LINKER_FLAGS",
+ "CMAKE_MODULE_LINKER_FLAGS",
+ "CMAKE_SHARED_LINKER_FLAGS",
+ f"CMAKE_C_FLAGS_{build_type_suffix}",
+ f"CMAKE_CXX_FLAGS_{build_type_suffix}",
+ f"CMAKE_EXE_LINKER_FLAGS_{build_type_suffix}",
+ f"CMAKE_MODULE_LINKER_FLAGS_{build_type_suffix}",
+ f"CMAKE_SHARED_LINKER_FLAGS_{build_type_suffix}",
+ }
+
+ config = {}
+ for name, value in raw_config.items():
+ if not isinstance( name, str ) or re.fullmatch( r"[A-Z][A-Z0-9_]*", name ) is None:
+ raise ValueError( "measurement.build_config has an invalid CMake key" )
+ relevant = (
+ name in CMAKE_FIXED_CONFIG_NAMES
+ or name in active_flag_names
+ or name.startswith( "ENABLE_" )
+ or name.startswith( "GEOS_BUILD_" )
+ or name.startswith( "GEOS_ENABLE_" )
+ or name.startswith( "LVARRAY_" )
+ or name.startswith( "RAJA_ENABLE_" )
+ )
+ if not relevant:
+ raise ValueError(
+ f"measurement.build_config contains an irrelevant key: {name}"
+ )
+ if isinstance( value, bool ):
+ config[name] = value
+ elif isinstance( value, str ):
+ if value != value.strip() or any(
+ character in value for character in "\0\r\n"
+ ):
+ raise ValueError(
+ f"measurement.build_config.{name} is not normalized"
+ )
+ config[name] = value
+ else:
+ raise ValueError(
+ f"measurement.build_config.{name} must be a string or boolean"
+ )
+
+ missing = sorted( CMAKE_REQUIRED_CONFIG_NAMES - config.keys() )
+ if missing:
+ raise ValueError(
+ "measurement.build_config is missing required keys: "
+ + ", ".join( missing )
+ )
+ if config["ENABLE_COVERAGE"] is not False:
+ raise ValueError( "measurement.build_config.ENABLE_COVERAGE must be false" )
+ if config["GEOS_ENABLE_LLVM_SOURCE_COVERAGE"] is not True:
+ raise ValueError(
+ "measurement.build_config.GEOS_ENABLE_LLVM_SOURCE_COVERAGE must be true"
+ )
+ if config["BUILD_SHARED_LIBS"] is not True or \
+ config["GEOS_BUILD_SHARED_LIBS"] is not True:
+ raise ValueError( "measurement.build_config must select shared libraries" )
+ return dict( sorted( config.items() ) )
+
+
+def contract_payload( scope: str, excluded_regex: str, measurement: dict ) -> dict:
+ container = measurement["container"]
+ return {
+ "summary_schema_version": COVERAGE_SUMMARY_SCHEMA_VERSION,
+ "contract_id": measurement["contract_id"],
+ "scope": scope,
+ "excluded_regex": excluded_regex,
+ "metric_semantics": measurement["metric_semantics"],
+ "container": {
+ "image_id": container["image_id"],
+ "image_digests": container["image_digests"],
+ },
+ "toolchain": measurement["toolchain"],
+ "build_config": measurement["build_config"],
+ "object_selection": measurement["object_selection"],
+ }
+
+
+def compute_contract_fingerprint(
+ scope: str, excluded_regex: str, measurement: dict
+) -> str:
+ serialized = json.dumps(
+ contract_payload( scope, excluded_regex, measurement ),
+ allow_nan=False,
+ ensure_ascii=True,
+ separators=( ",", ":" ),
+ sort_keys=True,
+ ).encode( "utf-8" )
+ return hashlib.sha256( serialized ).hexdigest()
+
+
+def validate_measurement(
+ raw_measurement: object, scope: str, excluded_regex: str, tool_major: int
+) -> dict:
+ if not isinstance( raw_measurement, dict ):
+ raise ValueError( "coverage summary measurement must be an object" )
+ require_exact_keys( raw_measurement, MEASUREMENT_KEYS, "measurement" )
+
+ commit_sha = require_string(
+ raw_measurement.get( "commit_sha" ), "measurement.commit_sha"
+ )
+ tree_sha = require_string(
+ raw_measurement.get( "tree_sha" ), "measurement.tree_sha"
+ )
+ git_object_id = r"(?:[0-9a-f]{40}|[0-9a-f]{64})"
+ if re.fullmatch( git_object_id, commit_sha ) is None:
+ raise ValueError( "measurement.commit_sha must be a canonical Git object ID" )
+ if re.fullmatch( git_object_id, tree_sha ) is None:
+ raise ValueError( "measurement.tree_sha must be a canonical Git object ID" )
+
+ contract_id = require_string(
+ raw_measurement.get( "contract_id" ), "measurement.contract_id"
+ )
+ if contract_id != COVERAGE_CONTRACT_ID:
+ raise ValueError(
+ f"measurement.contract_id must be {COVERAGE_CONTRACT_ID!r}"
+ )
+ fingerprint = require_string(
+ raw_measurement.get( "contract_fingerprint" ),
+ "measurement.contract_fingerprint",
+ )
+ if re.fullmatch( r"[0-9a-f]{64}", fingerprint ) is None:
+ raise ValueError(
+ "measurement.contract_fingerprint must be a lowercase SHA256 digest"
+ )
+
+ container = validate_container( raw_measurement.get( "container" ) )
+ toolchain = validate_toolchain(
+ raw_measurement.get( "toolchain" ), tool_major
+ )
+ build_config = validate_build_config( raw_measurement.get( "build_config" ) )
+ if raw_measurement.get( "metric_semantics" ) != METRIC_SEMANTICS:
+ raise ValueError( "measurement.metric_semantics is not the v1 contract" )
+ if raw_measurement.get( "object_selection" ) != OBJECT_SELECTION:
+ raise ValueError( "measurement.object_selection is not the v1 contract" )
+
+ measurement = {
+ "commit_sha": commit_sha,
+ "tree_sha": tree_sha,
+ "contract_id": contract_id,
+ "contract_fingerprint": fingerprint,
+ "container": container,
+ "toolchain": toolchain,
+ "build_config": build_config,
+ "metric_semantics": dict( METRIC_SEMANTICS ),
+ "object_selection": dict( OBJECT_SELECTION ),
+ }
+ expected_fingerprint = compute_contract_fingerprint(
+ scope, excluded_regex, measurement
+ )
+ if fingerprint != expected_fingerprint:
+ raise ValueError(
+ "measurement.contract_fingerprint does not match the coverage contract"
+ )
+ return measurement
+
+
+def validate_per_file_metrics(
+ raw_files: object, scope: str, canonical: dict
+) -> list[dict]:
+ if not isinstance( raw_files, list ) or not raw_files:
+ raise ValueError( "per_file_metrics must be a nonempty array" )
+ files = []
+ seen_paths = set()
+ aggregates = {
+ name: { "covered": 0, "total": 0, "not_covered": 0 }
+ for name in CANONICAL_METRICS
+ }
+ for index, raw_file in enumerate( raw_files ):
+ if not isinstance( raw_file, dict ):
+ raise ValueError( f"per_file_metrics[{index}] must be an object" )
+ require_exact_keys(
+ raw_file, { "path", "metrics" }, f"per_file_metrics[{index}]"
+ )
+ path = validate_scoped_path(
+ raw_file.get( "path" ), scope, f"per_file_metrics[{index}].path"
+ )
+ if path in seen_paths:
+ raise ValueError( "per_file_metrics paths must be unique" )
+ seen_paths.add( path )
+ raw_metrics = raw_file.get( "metrics" )
+ if not isinstance( raw_metrics, dict ):
+ raise ValueError( f"per_file_metrics[{index}].metrics must be an object" )
+ require_exact_keys(
+ raw_metrics,
+ set( CANONICAL_METRICS ),
+ f"per_file_metrics[{index}].metrics",
+ )
+ metrics = {
+ name: validate_metric(
+ raw_metrics[name],
+ f"per_file_metrics[{index}].metrics.{name}",
+ minimum_total=0,
+ )
+ for name in CANONICAL_METRICS
+ }
+ for name, metric in metrics.items():
+ for count_name in ( "covered", "total", "not_covered" ):
+ aggregates[name][count_name] += metric[count_name]
+ files.append( { "path": path, "metrics": metrics } )
+
+ if files != sorted( files, key=lambda source: source["path"] ):
+ raise ValueError( "per_file_metrics must be sorted by path" )
+ for name in CANONICAL_METRICS:
+ for count_name in ( "covered", "total", "not_covered" ):
+ if aggregates[name][count_name] != canonical[name][count_name]:
+ raise ValueError(
+ f"per_file_metrics {name} {count_name} does not match canonical totals"
+ )
+ return files
+
+
+def validate_branch_gaps( raw_gaps: object, scope: str ) -> list[dict]:
+ if not isinstance( raw_gaps, list ) or len( raw_gaps ) > 5:
+ raise ValueError( "top_branch_gaps must be an array with at most 5 entries" )
+
+ gaps = []
+ seen_paths = set()
+ for index, raw_gap in enumerate( raw_gaps ):
+ if not isinstance( raw_gap, dict ):
+ raise ValueError( f"top_branch_gaps[{index}] must be an object" )
+ path = validate_scoped_path(
+ raw_gap.get( "path" ), scope, f"top_branch_gaps[{index}].path"
+ )
+ if path in seen_paths:
+ raise ValueError( "top_branch_gaps paths must be normalized and unique" )
+ seen_paths.add( path )
+
+ gap = validate_metric(
+ raw_gap,
+ f"top_branch_gaps[{index}]",
+ allowed_extra_fields={ "path" },
+ )
+ if gap["not_covered"] == 0:
+ raise ValueError( "top_branch_gaps entries must have missed branches" )
+ gaps.append( { "path": path, **gap } )
+
+ expected_order = sorted(
+ gaps, key=lambda gap: ( -gap["not_covered"], gap["path"] )
+ )
+ if gaps != expected_order:
+ raise ValueError( "top_branch_gaps must be sorted by missed branches" )
+ return gaps
+
+
+def validate_summary( document: dict ) -> dict:
+ require_schema_version(
+ document, "coverage summary", COVERAGE_SUMMARY_SCHEMA_VERSION
+ )
+ scope = validate_repo_path( document.get( "scope" ), "coverage summary scope" )
+
+ require_exact_keys(
+ document,
+ {
+ "schema_version",
+ "scope",
+ "excluded_regex",
+ "measurement",
+ "tool",
+ "inputs",
+ "metrics",
+ "per_file_metrics",
+ "top_branch_gaps",
+ "supplemental",
+ },
+ "coverage summary",
+ )
+ excluded_regex = require_string(
+ document.get( "excluded_regex" ), "coverage summary excluded_regex"
+ )
+
+ tool = document.get( "tool" )
+ if not isinstance( tool, dict ) or tool.get( "name" ) != "llvm-cov":
+ raise ValueError( "coverage summary tool.name must be llvm-cov" )
+ require_exact_keys( tool, { "name", "major" }, "tool" )
+ tool_major = require_integer( tool.get( "major" ), "tool.major", 1 )
+
+ measurement = validate_measurement(
+ document.get( "measurement" ), scope, excluded_regex, tool_major
+ )
+
+ raw_inputs = document.get( "inputs" )
+ if not isinstance( raw_inputs, dict ):
+ raise ValueError( "coverage summary inputs must be an object" )
+ require_exact_keys(
+ raw_inputs,
+ { "profiles", "coverage_objects", "zero_hash_mappings" },
+ "inputs",
+ )
+ inputs = {
+ "profiles": require_integer( raw_inputs.get( "profiles" ), "inputs.profiles", 1 ),
+ "coverage_objects": require_integer(
+ raw_inputs.get( "coverage_objects" ), "inputs.coverage_objects", 1
+ ),
+ "zero_hash_mappings": require_integer(
+ raw_inputs.get( "zero_hash_mappings" ), "inputs.zero_hash_mappings"
+ ),
+ }
+
+ raw_metrics = document.get( "metrics" )
+ if not isinstance( raw_metrics, dict ):
+ raise ValueError( "coverage summary metrics must be an object" )
+ require_exact_keys( raw_metrics, set( CANONICAL_METRICS ), "metrics" )
+ metrics = {
+ name: validate_metric( raw_metrics.get( name ), f"metrics.{name}" )
+ for name in CANONICAL_METRICS
+ }
+
+ per_file_metrics = validate_per_file_metrics(
+ document.get( "per_file_metrics" ), scope, metrics
+ )
+
+ raw_supplemental = document.get( "supplemental" )
+ if not isinstance( raw_supplemental, dict ):
+ raise ValueError( "coverage summary supplemental must be an object" )
+ require_exact_keys(
+ raw_supplemental,
+ { name for name, _ in SUPPLEMENTAL_METRICS },
+ "supplemental",
+ )
+ supplemental = {
+ name: validate_metric( raw_supplemental.get( name ), f"supplemental.{name}" )
+ for name, _ in SUPPLEMENTAL_METRICS
+ }
+ branch_gaps = validate_branch_gaps( document.get( "top_branch_gaps" ), scope )
+ if sum( gap["total"] for gap in branch_gaps ) > metrics["branches"]["total"]:
+ raise ValueError( "top_branch_gaps totals exceed canonical branch coverage" )
+ expected_branch_gaps = []
+ for source in per_file_metrics:
+ branches = source["metrics"]["branches"]
+ if branches["not_covered"]:
+ expected_branch_gaps.append( { "path": source["path"], **branches } )
+ expected_branch_gaps.sort(
+ key=lambda gap: ( -gap["not_covered"], gap["path"] )
+ )
+ if branch_gaps != expected_branch_gaps[:5]:
+ raise ValueError(
+ "top_branch_gaps does not match canonical per-file branch metrics"
+ )
+ return {
+ "scope": scope,
+ "excluded_regex": excluded_regex,
+ "measurement": measurement,
+ "tool": { "name": "llvm-cov", "major": tool_major },
+ "inputs": inputs,
+ "metrics": metrics,
+ "per_file_metrics": per_file_metrics,
+ "top_branch_gaps": branch_gaps,
+ "supplemental": supplemental,
+ }
+
+
+def validate_policy( document: dict, expected_scope: str ) -> dict:
+ require_schema_version(
+ document, "coverage policy", COVERAGE_POLICY_SCHEMA_VERSION
+ )
+ if document.get( "scope" ) != expected_scope:
+ raise ValueError(
+ f"coverage policy scope {document.get('scope')!r} does not match "
+ f"summary scope {expected_scope!r}"
+ )
+
+ raw_thresholds = document.get( "minimum_basis_points" )
+ if not isinstance( raw_thresholds, dict ) or not raw_thresholds:
+ raise ValueError( "coverage policy minimum_basis_points must be nonempty" )
+
+ thresholds = {}
+ for name, value in raw_thresholds.items():
+ if name not in CANONICAL_METRICS:
+ raise ValueError( f"unsupported coverage metric in policy: {name}" )
+ thresholds[name] = require_integer(
+ value, f"minimum_basis_points.{name}"
+ )
+ if thresholds[name] > 10000:
+ raise ValueError( f"minimum_basis_points.{name} cannot exceed 10000" )
+ return thresholds
+
+
+def percent_text( covered: int, total: int ) -> str:
+ return f"{100.0 * covered / total:.2f}%"
+
+
+def build_markdown( summary: dict, thresholds: dict ) -> tuple[str, bool]:
+ threshold_results = {
+ name: summary["metrics"][name]["covered"] * 10000
+ >= threshold * summary["metrics"][name]["total"]
+ for name, threshold in thresholds.items()
+ }
+ all_passed = all( threshold_results.values() )
+ display_names = {
+ "regions": "Regions",
+ "functions": "Functions",
+ "lines": "Lines",
+ "branches": "Canonical branches",
+ }
+ metric_rows = []
+ for name in DISPLAY_METRICS:
+ metric = summary["metrics"][name]
+ threshold = thresholds.get( name )
+ if threshold is None:
+ requirement = "—"
+ result = "ℹ️ Measured"
+ else:
+ passed = threshold_results[name]
+ requirement = f"≥ {threshold / 100:.2f}%"
+ margin = 100.0 * metric["covered"] / metric["total"] - threshold / 100
+ if 0 < abs( margin ) < 0.01:
+ direction = "above" if margin > 0 else "below"
+ margin_display = f"<0.01 pp {direction}"
+ else:
+ margin_display = f"{margin:+.2f} pp"
+ result = f"{'✅ Pass' if passed else '❌ Fail'} ({margin_display})"
+ metric_rows.append(
+ f"| {display_names[name]} | {metric['covered']:,} | "
+ f"{metric['not_covered']:,} | {metric['total']:,} | "
+ f"{percent_text(metric['covered'], metric['total'])} | "
+ f"{requirement} | {result} |"
+ )
+
+ policy_result = "passed" if all_passed else "failed"
+ policy_icon = "✅" if all_passed else "❌"
+ rows = [
+ f"### {policy_icon} Coverage policy {policy_result}",
+ "",
+ f"Production scope: `{summary['scope']}` · "
+ f"Reporter: `{summary['tool']['name']} {summary['tool']['major']}`",
+ "",
+ "| Metric | Covered | Missed | Total | Coverage | Requirement | Result |",
+ "|---|---:|---:|---:|---:|---:|:---:|",
+ *metric_rows,
+ "",
+ "Percentages are rounded for display; policy thresholds use exact covered/total counts.",
+ ]
+ branch_gaps = summary["top_branch_gaps"]
+ if branch_gaps:
+ rows.extend(
+ (
+ "",
+ "### Largest branch gaps",
+ "",
+ "| Source file | Covered | Missed | Total | Coverage |",
+ "|---|---:|---:|---:|---:|",
+ )
+ )
+ for gap in branch_gaps:
+ scope_prefix = f"{summary['scope'].rstrip( '/' )}/"
+ display_path = html.escape( gap["path"][len( scope_prefix ):] )
+ display_path = display_path.replace( "|", "|" )
+ display_path = display_path.replace( "\n", "
" ).replace(
+ "\r", "
"
+ )
+ rows.append(
+ f"| {display_path} | {gap['covered']:,} | "
+ f"{gap['not_covered']:,} | {gap['total']:,} | "
+ f"{percent_text(gap['covered'], gap['total'])} |"
+ )
+ rows.extend(
+ (
+ "",
+ "Ranked by missed canonical branch outcomes within the production scope.",
+ )
+ )
+
+ rows.extend(
+ (
+ "",
+ "### Coverage collection",
+ "",
+ "| Input | Value |",
+ "|---|---:|",
+ f"| Profile files merged | {summary['inputs']['profiles']:,} |",
+ f"| Instrumented product objects | {summary['inputs']['coverage_objects']:,} |",
+ f"| Audited zero-hash mappings | {summary['inputs']['zero_hash_mappings']:,} |",
+ "",
+ "Zero-hash mappings have no profile counters and are audited during report "
+ "generation; any nonzero or unaudited mismatch fails the job.",
+ "",
+ "",
+ "Branch diagnostic (not gated) ",
+ "",
+ "| Diagnostic | Covered | Total | Coverage |",
+ "|---|---:|---:|---:|",
+ )
+ )
+ for name, display_name in SUPPLEMENTAL_METRICS:
+ metric = summary["supplemental"][name]
+ rows.append(
+ f"| {display_name} | {metric['covered']:,} | "
+ f"{metric['total']:,} | {percent_text(metric['covered'], metric['total'])} |"
+ )
+
+ rows.extend(
+ (
+ "",
+ "Native outcomes are instantiation-weighted: C++ template and inline-function "
+ "instances may appear more than once. This diagnostic is not the policy metric.",
+ "",
+ " ",
+ )
+ )
+ return "\n".join( rows ) + "\n", all_passed
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument( "summary", type=pathlib.Path )
+ parser.add_argument( "policy", type=pathlib.Path )
+ parser.add_argument( "--markdown", type=pathlib.Path )
+ args = parser.parse_args()
+
+ if args.markdown is not None:
+ try:
+ args.markdown.unlink( missing_ok=True )
+ except OSError as error:
+ print(
+ f"coverage threshold error: cannot clear {args.markdown}: {error}",
+ file=sys.stderr,
+ )
+ return 2
+
+ try:
+ summary = validate_summary( load_json( args.summary ) )
+ thresholds = validate_policy( load_json( args.policy ), summary["scope"] )
+ markdown, passed = build_markdown( summary, thresholds )
+ except ValueError as error:
+ print( f"coverage threshold error: {error}", file=sys.stderr )
+ return 2
+
+ if args.markdown is not None:
+ args.markdown.parent.mkdir( parents=True, exist_ok=True )
+ args.markdown.write_text( markdown, encoding="utf-8" )
+ else:
+ print( markdown, end="" )
+ return 0 if passed else 1
+
+
+if __name__ == "__main__":
+ sys.exit( main() )
diff --git a/scripts/ci_build_and_test_in_container.sh b/scripts/ci_build_and_test_in_container.sh
index d1136071eee..d58562ae58b 100755
--- a/scripts/ci_build_and_test_in_container.sh
+++ b/scripts/ci_build_and_test_in_container.sh
@@ -154,8 +154,14 @@ Usage: $0
One of Debug, Release, RelWithDebInfo and MinSizeRel. Forwarded to CMAKE_BUILD_TYPE.
--cmake-cuda-architectures ...
Optional override for CMAKE_CUDA_ARCHITECTURES.
- --code-coverage
- run a code build and test.
+ --coverage-output-dir /path/to/output
+ Directory where LLVM source-coverage inputs and the HTML report are written.
+ --coverage-base-sha SHA
+ Exact pull-request base commit used for advisory patch coverage.
+ --coverage-head-sha SHA
+ Exact commit being built and measured by the coverage job.
+ --coverage-baseline-summary /path/to/coverage-summary.json
+ Optional trusted summary for the exact base commit.
--ctest-parallel-level N
Number of tests ctest may run in parallel.
--data-basename output.tar.gz
@@ -177,6 +183,9 @@ Usage: $0
The host-config. Path is relative to the root of the repository.
--install-dir-basename GEOS-e42ffc1
GEOS installation basename.
+ --llvm-source-coverage
+ Build with Clang source coverage, run the coverage smoke suite, and
+ enforce the repository coverage policy.
--makefile
Use "Unix Makefiles" as build system generator.
--ninja
@@ -208,7 +217,7 @@ exit 1
# Then we'll move to the build dir.
or_die cd $(dirname $0)/..
-args=$(or_die getopt -a -o h --long build-exe-only,cmake-build-type:,cmake-cuda-architectures:,code-coverage,ctest-parallel-level:,data-basename:,geos-enable-bounds-check:,enable-hypre:,enable-hypredrv:,enable-hypre-device:,enable-trilinos:,exchange-dir:,host-config:,install-dir-basename:,makefile,ninja,no-install-schema,no-run-unit-tests,nproc:,repository:,run-integrated-tests,sccache-credentials:,test-code-style,test-documentation,use-native-architecture,use-sccache,help -- "$@")
+args=$(or_die getopt -a -o h --long build-exe-only,cmake-build-type:,cmake-cuda-architectures:,coverage-base-sha:,coverage-baseline-summary:,coverage-head-sha:,coverage-output-dir:,ctest-parallel-level:,data-basename:,geos-enable-bounds-check:,enable-hypre:,enable-hypredrv:,enable-hypre-device:,enable-trilinos:,exchange-dir:,host-config:,install-dir-basename:,llvm-source-coverage,makefile,ninja,no-install-schema,no-run-unit-tests,nproc:,repository:,run-integrated-tests,sccache-credentials:,test-code-style,test-documentation,use-native-architecture,use-sccache,help -- "$@")
# Variables with default values
BUILD_EXE_ONLY=false
@@ -225,7 +234,11 @@ UPLOAD_TEST_BASELINES=false
TEST_CODE_STYLE=false
TEST_DOCUMENTATION=false
ENABLE_TRILINOS=OFF
-CODE_COVERAGE=false
+LLVM_SOURCE_COVERAGE=false
+COVERAGE_OUTPUT_DIR=""
+COVERAGE_BASE_SHA=""
+COVERAGE_HEAD_SHA=""
+COVERAGE_BASELINE_SUMMARY=""
CTEST_PARALLEL_LEVEL_ARG=""
NPROC="$(nproc)"
GEOS_ENABLE_BOUNDS_CHECK=ON
@@ -235,7 +248,6 @@ USE_SCCACHE=false
CMAKE_CUDA_ARCHITECTURES_ARGS=()
CMAKE_NATIVE_ARCHITECTURE_ARGS=()
ATS_CMAKE_ARGS=()
-LCOV_CMAKE_ARGS=""
eval set -- ${args}
while :
@@ -249,6 +261,10 @@ do
--cmake-cuda-architectures)
CMAKE_CUDA_ARCHITECTURES_ARGS+=("-DCMAKE_CUDA_ARCHITECTURES=$2")
shift 2;;
+ --coverage-base-sha) COVERAGE_BASE_SHA=$2; shift 2;;
+ --coverage-baseline-summary) COVERAGE_BASELINE_SUMMARY=$2; shift 2;;
+ --coverage-head-sha) COVERAGE_HEAD_SHA=$2; shift 2;;
+ --coverage-output-dir) COVERAGE_OUTPUT_DIR=$2; shift 2;;
--ninja)
BUILD_GENERATOR=$1;
shift;;
@@ -270,6 +286,7 @@ do
--exchange-dir) DATA_EXCHANGE_DIR=$2; shift 2;;
--host-config) HOST_CONFIG=$2; shift 2;;
--install-dir-basename) GEOS_DIR=${GEOSX_TPL_DIR}/../$2; shift 2;;
+ --llvm-source-coverage) LLVM_SOURCE_COVERAGE=true; shift;;
--makefile) BUILD_GENERATOR=""; shift;;
--no-install-schema) GEOS_INSTALL_SCHEMA=false; shift;;
--no-run-unit-tests) RUN_UNIT_TESTS=false; shift;;
@@ -281,7 +298,6 @@ do
--repository) GEOS_SRC_DIR=$2; shift 2;;
--run-integrated-tests) RUN_INTEGRATED_TESTS=true; shift;;
--upload-test-baselines) UPLOAD_TEST_BASELINES=true; shift;;
- --code-coverage) CODE_COVERAGE=true; shift;;
--ctest-parallel-level)
CTEST_PARALLEL_LEVEL_ARG=$2
shift 2;;
@@ -419,39 +435,69 @@ if [[ "${RUN_INTEGRATED_TESTS}" = true ]]; then
fi
-if [[ "${CODE_COVERAGE}" = true ]]; then
- or_die apt-get update
- or_die apt-get install -y lcov
+if [[ "${LLVM_SOURCE_COVERAGE}" = true ]]; then
+ if [[ -z "${COVERAGE_OUTPUT_DIR}" ]]; then
+ echo "--llvm-source-coverage requires --coverage-output-dir." >&2
+ exit 1
+ fi
+ if [[ ! "${COVERAGE_HEAD_SHA}" =~ ^[0-9a-f]{40}$ ||
+ "$(git -c "safe.directory=${GEOS_SRC_DIR}" -C "${GEOS_SRC_DIR}" rev-parse HEAD)" != "${COVERAGE_HEAD_SHA}" ]]; then
+ echo "--coverage-head-sha must identify the checked-out commit." >&2
+ exit 1
+ fi
+ if [[ -n "${COVERAGE_BASE_SHA}" ]]; then
+ if [[ ! "${COVERAGE_BASE_SHA}" =~ ^[0-9a-f]{40}$ ]] ||
+ ! git -c "safe.directory=${GEOS_SRC_DIR}" -C "${GEOS_SRC_DIR}" \
+ cat-file -e "${COVERAGE_BASE_SHA}^{commit}" 2>/dev/null; then
+ echo "--coverage-base-sha must identify an available commit." >&2
+ exit 1
+ fi
+ fi
+ if [[ -n "${COVERAGE_BASELINE_SUMMARY}" ]]; then
+ if [[ -z "${COVERAGE_BASE_SHA}" ||
+ ! -f "${COVERAGE_BASELINE_SUMMARY}" ||
+ -L "${COVERAGE_BASELINE_SUMMARY}" ]]; then
+ echo "--coverage-baseline-summary requires a safe file and a base SHA." >&2
+ exit 1
+ fi
+ fi
- LCOV_REAL=$(command -v lcov || true)
- if [[ -n "${LCOV_REAL}" ]]; then
- export GEOS_REAL_LCOV="${LCOV_REAL}"
- LCOV_WRAPPER=/tmp/geos-lcov-wrapper
- cat > "${LCOV_WRAPPER}" <<'EOF'
-#!/bin/bash
-set -e
-
-extra_args=()
-if "${GEOS_REAL_LCOV}" --version 2>&1 | grep -Eq 'LCOV version ([2-9]|[1-9][0-9])\.'; then
- for arg in "$@"; do
- case "${arg}" in
- --capture|-c)
- extra_args=(--ignore-errors mismatch,empty)
- break
- ;;
- --remove|-r)
- extra_args=(--ignore-errors unused)
- break
- ;;
- esac
+ # The pinned Clang CI image intentionally contains the compiler and TPLs but
+ # not the compiler-native coverage reporter or profile runtime. Install the
+ # matching LLVM-major packages in this ephemeral container.
+ or_die apt-get update -qq
+ or_die env DEBIAN_FRONTEND=noninteractive apt-get install -y \
+ --no-install-recommends llvm-20 libclang-rt-20-dev
+ export GEOS_CI_LLVM_PACKAGE_VERSIONS="$({
+ dpkg-query -W -f='${Package}=${Version}\n' llvm-20 libclang-rt-20-dev
+ } | LC_ALL=C sort)"
+ or_die test -n "${GEOS_CI_LLVM_PACKAGE_VERSIONS}"
+
+ for llvm_tool in llvm-cov-20 llvm-profdata-20 llvm-readelf-20; do
+ or_die command -v "${llvm_tool}"
done
-fi
-
-exec "${GEOS_REAL_LCOV}" "${extra_args[@]}" "$@"
-EOF
- or_die chmod +x "${LCOV_WRAPPER}"
- LCOV_CMAKE_ARGS="-DLCOV_EXECUTABLE=${LCOV_WRAPPER}"
- fi
+ or_die mkdir -p "${COVERAGE_OUTPUT_DIR}"
+ for coverage_artifact in \
+ CMakeCache.txt \
+ branch-summary.txt \
+ coverage-objects.txt \
+ coverage-status.md \
+ coverage-summary.json \
+ coverage-summary.md \
+ ctest.log \
+ index.html \
+ llvm-summary.json \
+ mapping-integrity.log \
+ native-export.log \
+ pr-coverage.json \
+ pr-coverage.md \
+ summary-export.log \
+ toolchain.txt; do
+ or_die rm -f -- "${COVERAGE_OUTPUT_DIR}/${coverage_artifact}"
+ done
+ or_die python3 -m unittest discover \
+ -s "${GEOS_SRC_DIR}/scripts/tests" \
+ -p 'test_*coverage*.py'
fi
# The -DBLT_MPI_COMMAND_APPEND="--allow-run-as-root;--oversubscribe" option is added for OpenMPI.
@@ -468,6 +514,20 @@ fi
# This will tells OpenMPI to discover the number of hardware threads on the node,
# and use that as the number of slots available. (There is a distinction between threads and cores).
GEOS_BUILD_DIR=/tmp/geos-build
+LLVM_COVERAGE_CMAKE_ARGS=(
+ -DENABLE_COVERAGE=OFF
+ -DGEOS_ENABLE_LLVM_SOURCE_COVERAGE=$([[ "${LLVM_SOURCE_COVERAGE}" = true ]] && echo ON || echo OFF)
+)
+if [[ "${LLVM_SOURCE_COVERAGE}" = true ]]; then
+ LLVM_COVERAGE_CMAKE_ARGS+=(
+ -DGEOS_BUILD_SHARED_LIBS=ON
+ -DENABLE_DOXYGEN=OFF
+ )
+ LLVM_BUILD_PROFILE_DIR=/tmp/geos-coverage-build-profiles
+ or_die mkdir -p "${LLVM_BUILD_PROFILE_DIR}"
+ export LLVM_PROFILE_FILE="${LLVM_BUILD_PROFILE_DIR}/%p-%m.profraw"
+fi
+
or_die python3 scripts/config-build.py \
-hc ${HOST_CONFIG} \
-bt ${CMAKE_BUILD_TYPE} \
@@ -481,12 +541,11 @@ or_die python3 scripts/config-build.py \
-DENABLE_HYPRE_DEVICE=${ENABLE_HYPRE_DEVICE} \
-DENABLE_TRILINOS=${ENABLE_TRILINOS} \
-DGEOS_LA_INTERFACE:PATH=${GEOS_LA_INTERFACE} \
- -DENABLE_COVERAGE=$([[ "${CODE_COVERAGE}" = true ]] && echo 1 || echo 0) \
-DGEOS_ENABLE_BOUNDS_CHECK=${GEOS_ENABLE_BOUNDS_CHECK} \
"${CMAKE_CUDA_ARCHITECTURES_ARGS[@]}" \
"${CMAKE_NATIVE_ARCHITECTURE_ARGS[@]}" \
${SCCACHE_CMAKE_ARGS} \
- ${LCOV_CMAKE_ARGS} \
+ "${LLVM_COVERAGE_CMAKE_ARGS[@]}" \
"${ATS_CMAKE_ARGS[@]}"
# The configuration step is now over, we can now move to the build directory for the build!
@@ -506,9 +565,9 @@ fi
# Performing the requested build.
if [[ "${BUILD_EXE_ONLY}" = true ]]; then
- or_die cmake --build . -j $NPROC --target geosx
+ or_die cmake --build . --parallel "${NPROC}" --target geosx
else
- or_die cmake --build . -j $NPROC
+ or_die cmake --build . --parallel "${NPROC}"
#or_die cmake --install .
if [[ ! -z "${DATA_BASENAME_WE}" ]]; then
@@ -536,10 +595,274 @@ if [[ -n "${SCCACHE_BIN}" ]]; then
fi
fi
-if [[ "${CODE_COVERAGE}" = true ]]; then
+if [[ "${LLVM_SOURCE_COVERAGE}" = true ]]; then
+ LLVM_TEST_PROFILE_DIR=/tmp/geos-coverage-profiles
+ LLVM_REPORT_DIR=/tmp/geos-coverage-report
+ LLVM_CTEST_PARALLEL="${CTEST_PARALLEL_LEVEL_ARG:-12}"
+ or_die mkdir -p "${LLVM_TEST_PROFILE_DIR}" "${LLVM_REPORT_DIR}"
+
export OMP_NUM_THREADS=1
- or_die cmake --build . --target coreComponents_coverage
- or_die cp -r ${GEOS_BUILD_DIR}/coreComponents_coverage.info.cleaned ${GEOS_SRC_DIR}/geos_coverage.info.cleaned
+ export OPENBLAS_NUM_THREADS=1
+ export LLVM_PROFILE_FILE="${LLVM_TEST_PROFILE_DIR}/%p-%m.profraw"
+
+ # The CI container runs as root so it can install the compiler-matched LLVM
+ # runtime. Open MPI requires this explicit, two-part acknowledgement before
+ # it will launch the three MPI coverage smokes.
+ if [[ "$(id -u)" -eq 0 ]]; then
+ export OMPI_ALLOW_RUN_AS_ROOT=1
+ export OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1
+ fi
+
+ {
+ printf 'container image: %s\n' "${GEOS_CI_CONTAINER_IMAGE:-unknown}"
+ printf 'container image ID: %s\n' "${GEOS_CI_CONTAINER_IMAGE_ID:-unknown}"
+ printf 'container image digests: %s\n' "${GEOS_CI_CONTAINER_IMAGE_DIGESTS:-unknown}"
+ or_die /usr/bin/clang-20 --version
+ or_die llvm-cov-20 --version
+ or_die llvm-profdata-20 --version
+ or_die llvm-readelf-20 --version
+ or_die cmake --version
+ or_die dpkg-query -W -f='${Package}=${Version}\n' llvm-20 libclang-rt-20-dev
+ } > "${COVERAGE_OUTPUT_DIR}/toolchain.txt"
+ or_die test -s "${COVERAGE_OUTPUT_DIR}/toolchain.txt"
+
+ coverage_ctest_status=0
+ ctest --test-dir "${GEOS_BUILD_DIR}" \
+ --parallel "${LLVM_CTEST_PARALLEL}" \
+ --progress \
+ --output-on-failure \
+ --output-log "${COVERAGE_OUTPUT_DIR}/ctest.log" \
+ -L '^coverage_smoke$' || coverage_ctest_status=$?
+
+ coverage_report_status=0
+ "${GEOS_SRC_DIR}/scripts/llvm_source_branch_coverage.sh" \
+ "${GEOS_BUILD_DIR}" \
+ "${LLVM_TEST_PROFILE_DIR}" \
+ "${LLVM_REPORT_DIR}" || coverage_report_status=$?
+
+ coverage_pr_status=0
+ coverage_pr_ran=false
+ if [[ ${coverage_report_status} -eq 0 && -n "${COVERAGE_BASE_SHA}" ]]; then
+ coverage_pr_ran=true
+ coverage_pr_args=(
+ --repository "${GEOS_SRC_DIR}"
+ --base-sha "${COVERAGE_BASE_SHA}"
+ --head-sha "${COVERAGE_HEAD_SHA}"
+ --native-info "${LLVM_REPORT_DIR}/native.info"
+ --candidate-summary "${LLVM_REPORT_DIR}/coverage-summary.json"
+ --output-json "${COVERAGE_OUTPUT_DIR}/pr-coverage.json"
+ --output-markdown "${COVERAGE_OUTPUT_DIR}/pr-coverage.md"
+ )
+ if [[ -n "${COVERAGE_BASELINE_SUMMARY}" ]]; then
+ coverage_pr_args+=(--baseline-summary "${COVERAGE_BASELINE_SUMMARY}")
+ fi
+ python3 "${GEOS_SRC_DIR}/scripts/compare_pr_coverage.py" \
+ "${coverage_pr_args[@]}" || coverage_pr_status=$?
+ if [[ ${coverage_pr_status} -eq 0 ]]; then
+ echo "LLVM pull-request coverage report written to ${COVERAGE_OUTPUT_DIR}/pr-coverage.md"
+ fi
+ fi
+
+ coverage_gate_status=0
+ coverage_gate_ran=false
+ if [[ ${coverage_report_status} -eq 0 ]]; then
+ coverage_gate_ran=true
+ python3 "${GEOS_SRC_DIR}/scripts/check_coverage_thresholds.py" \
+ "${LLVM_REPORT_DIR}/coverage-summary.json" \
+ "${GEOS_SRC_DIR}/.github/coverage-thresholds.json" \
+ --markdown "${COVERAGE_OUTPUT_DIR}/coverage-summary.md" \
+ || coverage_gate_status=$?
+ if [[ ${coverage_gate_status} -eq 0 ]]; then
+ echo "LLVM coverage policy report written to ${COVERAGE_OUTPUT_DIR}/coverage-summary.md"
+ fi
+ python3 "${GEOS_SRC_DIR}/scripts/render_coverage_review_summary.py" \
+ --coverage-summary "${LLVM_REPORT_DIR}/coverage-summary.json" \
+ --thresholds "${GEOS_SRC_DIR}/.github/coverage-thresholds.json" \
+ --console || echo "LLVM coverage console summary unavailable." >&2
+ fi
+
+ for report_file in \
+ branch-summary.txt \
+ coverage-summary.json \
+ coverage-objects.txt \
+ llvm-summary.json \
+ mapping-integrity.log \
+ native-export.log \
+ summary-export.log; do
+ if [[ -f "${LLVM_REPORT_DIR}/${report_file}" ]]; then
+ cp -- "${LLVM_REPORT_DIR}/${report_file}" "${COVERAGE_OUTPUT_DIR}/" \
+ || coverage_report_status=1
+ fi
+ done
+ cp -- "${GEOS_BUILD_DIR}/CMakeCache.txt" "${COVERAGE_OUTPUT_DIR}/" \
+ || coverage_report_status=1
+
+ coverage_overall_status=PASS
+ if [[ ${coverage_ctest_status} -ne 0 ||
+ ${coverage_report_status} -ne 0 ||
+ ${coverage_gate_status} -ne 0 ||
+ ${coverage_pr_status} -ne 0 ]]; then
+ coverage_overall_status=FAIL
+ fi
+
+ coverage_phase_status()
+ {
+ if [[ "${2:-true}" != true ]]; then
+ printf '⏭️ Not run'
+ return
+ fi
+ if [[ "$1" -eq 0 ]]; then
+ printf '✅ Pass'
+ else
+ printf '❌ Fail (%d)' "$1"
+ fi
+ }
+
+ coverage_ctest_detail="See the retained CTest log."
+ coverage_ctest_summary="$(
+ grep -E '^[0-9]+% tests passed, [0-9]+ tests failed out of [0-9]+$' \
+ "${COVERAGE_OUTPUT_DIR}/ctest.log" | tail -n 1 || true
+ )"
+ coverage_ctest_elapsed_line="$(
+ grep -E '^Total Test time \(real\) = +[0-9.]+ sec$' \
+ "${COVERAGE_OUTPUT_DIR}/ctest.log" | tail -n 1 || true
+ )"
+ coverage_ctest_summary_regex='^[0-9]+% tests passed, ([0-9]+) tests failed out of ([0-9]+)$'
+ coverage_ctest_elapsed_regex='^Total Test time \(real\) = +([0-9.]+) sec$'
+ if [[ "${coverage_ctest_summary}" =~ ${coverage_ctest_summary_regex} ]]; then
+ coverage_ctest_failed="${BASH_REMATCH[1]}"
+ coverage_ctest_total="${BASH_REMATCH[2]}"
+ coverage_ctest_passed=$(( coverage_ctest_total - coverage_ctest_failed ))
+ coverage_ctest_detail="${coverage_ctest_passed}/${coverage_ctest_total} passed"
+ if (( coverage_ctest_failed > 0 )); then
+ coverage_ctest_detail+=", ${coverage_ctest_failed} failed"
+ fi
+ if [[ "${coverage_ctest_elapsed_line}" =~ ${coverage_ctest_elapsed_regex} ]]; then
+ coverage_ctest_detail+=" in ${BASH_REMATCH[1]} s"
+ fi
+ coverage_ctest_detail+=" with ${LLVM_CTEST_PARALLEL}-way scheduling"
+ fi
+
+ if [[ ${coverage_report_status} -eq 0 ]]; then
+ coverage_report_detail="Profile mappings and report inputs validated"
+ else
+ coverage_report_detail="Report generation or artifact staging failed; inspect the logs"
+ fi
+ if [[ "${coverage_gate_ran}" != true ]]; then
+ coverage_gate_detail="Skipped because the LLVM report was unavailable"
+ elif [[ ${coverage_gate_status} -eq 0 ]]; then
+ coverage_gate_detail="Exact repository thresholds evaluated below"
+ else
+ coverage_gate_detail="Repository threshold check failed"
+ fi
+ if [[ "${coverage_pr_ran}" != true ]]; then
+ if [[ -n "${COVERAGE_BASE_SHA}" ]]; then
+ coverage_pr_detail="Skipped because the LLVM report was unavailable"
+ else
+ coverage_pr_detail="Not a pull-request coverage run"
+ fi
+ elif [[ ${coverage_pr_status} -eq 0 ]]; then
+ coverage_pr_detail="Changed-code coverage and exact-base comparison rendered below"
+ else
+ coverage_pr_detail="Patch-specific coverage analysis failed"
+ fi
+
+ coverage_status_markdown="${COVERAGE_OUTPUT_DIR}/coverage-status.md"
+ coverage_status_tmp="${coverage_status_markdown}.tmp"
+ coverage_image_id="${GEOS_CI_CONTAINER_IMAGE_ID:-unknown}"
+ coverage_image_id_display="${coverage_image_id}"
+ if (( ${#coverage_image_id_display} > 20 )); then
+ coverage_image_id_display="${coverage_image_id_display:0:20}…"
+ fi
+ coverage_clang_version="$(
+ /usr/bin/clang-20 --version | sed -nE \
+ '1s/.*version[[:space:]]+([0-9]+([.][0-9]+)+).*/\1/p'
+ )"
+ if [[ -z "${coverage_clang_version}" ]]; then
+ coverage_clang_version=20
+ fi
+ coverage_llvm_version="$(
+ llvm-cov-20 --version | sed -nE \
+ '1s/.*version[[:space:]]+([0-9]+([.][0-9]+)+).*/\1/p'
+ )"
+ if [[ -z "${coverage_llvm_version}" ]]; then
+ coverage_llvm_version=20
+ fi
+ coverage_reviewer_args=(
+ --coverage-summary "${LLVM_REPORT_DIR}/coverage-summary.json"
+ --thresholds "${GEOS_SRC_DIR}/.github/coverage-thresholds.json"
+ --tests-result "$(coverage_phase_status "${coverage_ctest_status}")"
+ --tests-detail "${coverage_ctest_detail}"
+ )
+ if [[ "${coverage_pr_ran}" = true &&
+ -f "${COVERAGE_OUTPUT_DIR}/pr-coverage.json" &&
+ ! -L "${COVERAGE_OUTPUT_DIR}/pr-coverage.json" ]]; then
+ coverage_reviewer_args+=(--pr-report "${COVERAGE_OUTPUT_DIR}/pr-coverage.json")
+ fi
+ coverage_reviewer_summary_status=0
+ coverage_reviewer_summary="$(
+ python3 "${GEOS_SRC_DIR}/scripts/render_coverage_review_summary.py" \
+ "${coverage_reviewer_args[@]}"
+ )" || coverage_reviewer_summary_status=$?
+ if [[ ${coverage_reviewer_summary_status} -ne 0 ]]; then
+ coverage_reviewer_summary=$'### Reviewer summary\n\nCoverage reviewer summary unavailable; inspect the detailed reports below.'
+ fi
+ {
+ if [[ "${coverage_overall_status}" = PASS ]]; then
+ printf '### ✅ LLVM source coverage passed\n\n'
+ else
+ printf '### ❌ LLVM source coverage failed\n\n'
+ fi
+ printf '%s\n' "${coverage_reviewer_summary}"
+ printf '### Validation details\n\n'
+ printf '| Validation | Result | Details |\n'
+ printf '|---|:---:|---|\n'
+ printf '| Coverage smoke suite | %s | %s |\n' \
+ "$(coverage_phase_status "${coverage_ctest_status}")" "${coverage_ctest_detail}"
+ printf '| LLVM report integrity | %s | %s |\n' \
+ "$(coverage_phase_status "${coverage_report_status}")" "${coverage_report_detail}"
+ printf '| Repository coverage policy | %s | %s |\n' \
+ "$(coverage_phase_status "${coverage_gate_status}" "${coverage_gate_ran}")" \
+ "${coverage_gate_detail}"
+ printf '| PR coverage analysis | %s | %s |\n' \
+ "$(coverage_phase_status "${coverage_pr_status}" "${coverage_pr_ran}")" \
+ "${coverage_pr_detail}"
+ printf '\n'
+ printf '### Run configuration\n\n'
+ printf '| Item | Value |\n'
+ printf '|---|---|\n'
+ printf '| Runner | `%s` |\n' "${GEOS_CI_RUNNER_LABEL:-unknown}"
+ printf '| Container | `%s` |\n' "${GEOS_CI_CONTAINER_IMAGE:-unknown}"
+ printf '| Immutable image ID (short) | `%s` |\n' "${coverage_image_id_display}"
+ printf '| Toolchain | `Clang %s / LLVM %s` |\n' \
+ "${coverage_clang_version}" "${coverage_llvm_version}"
+ printf '| Build | `%s`, %s-way compilation |\n' "${CMAKE_BUILD_TYPE}" "${NPROC}"
+ printf '| Test scheduling | %s CTest slots |\n' "${LLVM_CTEST_PARALLEL}"
+ printf '\n'
+ } > "${coverage_status_tmp}"
+ or_die mv -- "${coverage_status_tmp}" "${coverage_status_markdown}"
+
+ coverage_html_status=0
+ python3 "${GEOS_SRC_DIR}/scripts/render_coverage_report_html.py" \
+ --artifact-dir "${COVERAGE_OUTPUT_DIR}" \
+ --output "${COVERAGE_OUTPUT_DIR}/index.html" \
+ || coverage_html_status=$?
+ if [[ ${coverage_html_status} -eq 0 ]]; then
+ echo "LLVM source coverage HTML report written to ${COVERAGE_OUTPUT_DIR}/index.html"
+ else
+ echo "LLVM source coverage HTML report generation failed." >&2
+ fi
+
+ if [[ "${coverage_overall_status}" = FAIL ]]; then
+ echo "LLVM source coverage failed: ctest=${coverage_ctest_status}, " \
+ "report=${coverage_report_status}, gate=${coverage_gate_status}, " \
+ "pr=${coverage_pr_status}, html=${coverage_html_status}." >&2
+ exit 1
+ fi
+ if [[ ${coverage_html_status} -ne 0 ]]; then
+ exit 1
+ fi
+ exit 0
fi
# Run the unit tests (excluding previously ran checks).
diff --git a/scripts/compare_pr_coverage.py b/scripts/compare_pr_coverage.py
new file mode 100755
index 00000000000..8f0a7e5e8a9
--- /dev/null
+++ b/scripts/compare_pr_coverage.py
@@ -0,0 +1,1215 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-only
+
+"""Build a vendor-independent, advisory pull-request coverage report."""
+
+from __future__ import annotations
+
+import argparse
+import html
+import json
+import math
+import os
+import pathlib
+import re
+import subprocess
+import sys
+import tempfile
+from typing import Iterable
+
+
+SCRIPT_DIRECTORY = pathlib.Path( __file__ ).resolve().parent
+if str( SCRIPT_DIRECTORY ) not in sys.path:
+ sys.path.insert( 0, str( SCRIPT_DIRECTORY ) )
+import check_coverage_thresholds as coverage_contract
+
+
+REPORT_SCHEMA_VERSION = 1
+SUPPORTED_SUMMARY_SCHEMA_VERSIONS = ( 2, 3 )
+PRODUCTION_SCOPE = "src/coreComponents"
+CANONICAL_METRICS = ( "regions", "functions", "lines", "branches" )
+DISPLAY_METRICS = ( "lines", "functions", "branches", "regions" )
+SHA_PATTERN = re.compile( r"^[0-9a-fA-F]{40}$" )
+HUNK_PATTERN = re.compile(
+ rb"^@@ -[0-9]+(?:,[0-9]+)? \+([0-9]+)(?:,([0-9]+))? @@"
+)
+MAX_IMPACTED_FILES = 10
+MAX_IMPACTED_LOCATIONS = 10
+MAX_FUNCTION_NAMES = 3
+
+
+def require_integer( value, description: str, minimum: int = 0 ) -> int:
+ if isinstance( value, bool ) or not isinstance( value, int ) or value < minimum:
+ raise ValueError( f"{description} must be an integer >= {minimum}" )
+ return value
+
+
+def validate_sha( value: str, description: str ) -> str:
+ if not isinstance( value, str ) or SHA_PATTERN.fullmatch( value ) is None:
+ raise ValueError( f"{description} must be a full 40-character Git SHA" )
+ return value.lower()
+
+
+def read_json( path: pathlib.Path, description: str ) -> dict:
+ try:
+ with path.open( encoding="utf-8" ) as stream:
+ document = json.load( stream )
+ except ( OSError, UnicodeError, json.JSONDecodeError ) as error:
+ raise ValueError( f"cannot read {description} {path}: {error}" ) from error
+ if not isinstance( document, dict ):
+ raise ValueError( f"{description} must contain a JSON object" )
+ return document
+
+
+def validate_metric( raw_metric: object, description: str ) -> dict:
+ if not isinstance( raw_metric, dict ):
+ raise ValueError( f"{description} must be an object" )
+ covered = require_integer( raw_metric.get( "covered" ), f"{description}.covered" )
+ total = require_integer( raw_metric.get( "total" ), f"{description}.total", 1 )
+ not_covered = require_integer(
+ raw_metric.get( "not_covered" ), f"{description}.not_covered"
+ )
+ if covered > total or not_covered != total - covered:
+ raise ValueError( f"{description} counts are inconsistent" )
+ percent = raw_metric.get( "percent" )
+ if isinstance( percent, bool ) or not isinstance( percent, ( int, float ) ):
+ raise ValueError( f"{description}.percent must be numeric" )
+ expected_percent = round( 100.0 * covered / total, 6 )
+ if not math.isfinite( float( percent ) ) or abs(
+ float( percent ) - expected_percent
+ ) > 1.0e-6:
+ raise ValueError( f"{description}.percent is inconsistent with its counts" )
+ return {
+ "covered": covered,
+ "not_covered": not_covered,
+ "total": total,
+ "percent": expected_percent,
+ }
+
+
+def validate_summary( document: dict, description: str ) -> dict:
+ schema_version = document.get( "schema_version" )
+ if (
+ isinstance( schema_version, bool )
+ or not isinstance( schema_version, int )
+ or schema_version not in SUPPORTED_SUMMARY_SCHEMA_VERSIONS
+ ):
+ supported = ", ".join( str( value ) for value in SUPPORTED_SUMMARY_SCHEMA_VERSIONS )
+ raise ValueError(
+ f"{description}.schema_version must be one of the integers {supported}"
+ )
+ if schema_version == coverage_contract.COVERAGE_SUMMARY_SCHEMA_VERSION:
+ try:
+ validated = coverage_contract.validate_summary( document )
+ except ValueError as error:
+ raise ValueError( f"{description} is invalid: {error}" ) from error
+ return {
+ "schema_version": schema_version,
+ "scope": validated["scope"],
+ "excluded_regex": validated["excluded_regex"],
+ "tool": validated["tool"],
+ "metrics": validated["metrics"],
+ "measurement": validated["measurement"],
+ "per_file_metrics": validated["per_file_metrics"],
+ }
+ scope = document.get( "scope" )
+ excluded_regex = document.get( "excluded_regex" )
+ if not isinstance( scope, str ) or not scope:
+ raise ValueError( f"{description}.scope must be a nonempty string" )
+ if not isinstance( excluded_regex, str ) or not excluded_regex:
+ raise ValueError( f"{description}.excluded_regex must be a nonempty string" )
+ tool = document.get( "tool" )
+ if not isinstance( tool, dict ) or tool.get( "name" ) != "llvm-cov":
+ raise ValueError( f"{description}.tool.name must be llvm-cov" )
+ tool_major = require_integer( tool.get( "major" ), f"{description}.tool.major", 1 )
+ raw_metrics = document.get( "metrics" )
+ if not isinstance( raw_metrics, dict ):
+ raise ValueError( f"{description}.metrics must be an object" )
+ metrics = {
+ name: validate_metric( raw_metrics.get( name ), f"{description}.metrics.{name}" )
+ for name in CANONICAL_METRICS
+ }
+
+ raw_measurement = document.get( "measurement" )
+ measurement = None
+ if raw_measurement is not None:
+ raise ValueError( f"{description}.measurement is not valid in schema 2" )
+
+ return {
+ "schema_version": schema_version,
+ "scope": scope,
+ "excluded_regex": excluded_regex,
+ "tool": { "name": "llvm-cov", "major": tool_major },
+ "metrics": metrics,
+ "measurement": measurement,
+ "per_file_metrics": None,
+ }
+
+
+def checked_input_file( path: pathlib.Path, description: str ) -> pathlib.Path:
+ if path.is_symlink():
+ raise ValueError( f"{description} must not be a symbolic link: {path}" )
+ try:
+ resolved = path.resolve( strict=True )
+ except OSError as error:
+ raise ValueError( f"cannot resolve {description} {path}: {error}" ) from error
+ if not resolved.is_file():
+ raise ValueError( f"{description} is not a regular file: {path}" )
+ return resolved
+
+
+def run_git( repository: pathlib.Path, arguments: Iterable[str] ) -> bytes:
+ command = [
+ "git",
+ "-c",
+ f"safe.directory={repository}",
+ "-C",
+ str( repository ),
+ *arguments,
+ ]
+ try:
+ result = subprocess.run(
+ command,
+ check=False,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+ except OSError as error:
+ raise ValueError( f"cannot execute Git: {error}" ) from error
+ if result.returncode != 0:
+ message = result.stderr.decode( "utf-8", errors="replace" ).strip()
+ raise ValueError( f"Git command failed: {message or 'unknown error'}" )
+ return result.stdout
+
+
+def validate_repository( repository: pathlib.Path ) -> pathlib.Path:
+ try:
+ resolved = repository.resolve( strict=True )
+ except OSError as error:
+ raise ValueError( f"cannot resolve repository {repository}: {error}" ) from error
+ if not resolved.is_dir():
+ raise ValueError( f"repository is not a directory: {resolved}" )
+ top_level = run_git( resolved, ( "rev-parse", "--show-toplevel" ) )
+ try:
+ git_root = pathlib.Path( top_level.decode( "utf-8" ).strip() ).resolve(
+ strict=True
+ )
+ except ( UnicodeError, OSError ) as error:
+ raise ValueError( f"cannot resolve Git repository root: {error}" ) from error
+ if git_root != resolved:
+ raise ValueError( f"--repository must name the Git top level: {git_root}" )
+ return resolved
+
+
+def validate_commit( repository: pathlib.Path, sha: str, description: str ) -> str:
+ normalized = validate_sha( sha, description )
+ resolved = run_git(
+ repository, ( "rev-parse", "--verify", f"{normalized}^{{commit}}" )
+ ).decode( "ascii", errors="strict" ).strip()
+ if resolved.lower() != normalized:
+ raise ValueError( f"{description} does not resolve to the requested commit" )
+ return normalized
+
+
+def commit_tree( repository: pathlib.Path, sha: str ) -> str:
+ tree = run_git(
+ repository, ( "rev-parse", "--verify", f"{sha}^{{tree}}" )
+ ).decode( "ascii", errors="strict" ).strip()
+ return validate_sha( tree, f"tree for {sha}" )
+
+
+def decode_git_path( value: bytes ) -> str:
+ try:
+ path = value.decode( "utf-8", errors="strict" )
+ except UnicodeError as error:
+ raise ValueError( "Git paths must be valid UTF-8" ) from error
+ normalized = pathlib.PurePosixPath( path )
+ if (
+ not path
+ or normalized.is_absolute()
+ or ".." in normalized.parts
+ or normalized.as_posix() != path
+ ):
+ raise ValueError( f"Git returned an unsafe or non-normalized path: {path!r}" )
+ return path
+
+
+def changed_line_ranges(
+ repository: pathlib.Path, base_sha: str, head_sha: str
+) -> list[dict]:
+ raw_names = run_git(
+ repository,
+ (
+ "diff",
+ "--name-status",
+ "-z",
+ "--find-renames",
+ "--find-copies-harder",
+ "--diff-filter=ACMR",
+ base_sha,
+ head_sha,
+ "--",
+ PRODUCTION_SCOPE,
+ ),
+ )
+ fields = raw_names.split( b"\0" )
+ if fields and fields[-1] == b"":
+ fields.pop()
+
+ records = []
+ index = 0
+ scope_prefix = f"{PRODUCTION_SCOPE}/"
+ while index < len( fields ):
+ try:
+ status_text = fields[index].decode( "ascii", errors="strict" )
+ except UnicodeError as error:
+ raise ValueError( "Git returned a non-ASCII change status" ) from error
+ index += 1
+ if not status_text or status_text[0] not in "ACMR":
+ raise ValueError( f"Git returned an unexpected change status: {status_text!r}" )
+ status = status_text[0]
+ if status in "RC":
+ if index + 1 >= len( fields ):
+ raise ValueError( "Git returned a truncated rename/copy record" )
+ old_path = decode_git_path( fields[index] )
+ path = decode_git_path( fields[index + 1] )
+ index += 2
+ diff_paths = ( old_path, path )
+ else:
+ if index >= len( fields ):
+ raise ValueError( "Git returned a truncated change record" )
+ path = decode_git_path( fields[index] )
+ index += 1
+ old_path = None
+ diff_paths = ( path, )
+
+ # A rename out of the production tree can be selected by its old path.
+ # It has no head lines in scope and therefore is not a patch denominator.
+ if not path.startswith( scope_prefix ):
+ continue
+
+ patch = run_git(
+ repository,
+ (
+ "diff",
+ "--unified=0",
+ "--no-color",
+ "--no-ext-diff",
+ "--find-renames",
+ "--find-copies-harder",
+ base_sha,
+ head_sha,
+ "--",
+ *diff_paths,
+ ),
+ )
+ lines = set()
+ for patch_line in patch.splitlines():
+ match = HUNK_PATTERN.match( patch_line )
+ if match is None:
+ continue
+ start = int( match.group( 1 ) )
+ count = int( match.group( 2 ) or b"1" )
+ if count:
+ lines.update( range( start, start + count ) )
+ records.append(
+ {
+ "status": status,
+ "path": path,
+ "old_path": old_path,
+ "lines": frozenset( lines ),
+ }
+ )
+
+ records.sort( key=lambda record: record["path"] )
+ seen_paths = set()
+ for record in records:
+ if record["path"] in seen_paths:
+ raise ValueError( f"duplicate changed path from Git: {record['path']}" )
+ seen_paths.add( record["path"] )
+ return records
+
+
+def normalize_lcov_source( source: str, repository: pathlib.Path ) -> str | None:
+ candidate = pathlib.Path( source )
+ if not candidate.is_absolute():
+ candidate = repository / candidate
+ try:
+ resolved = candidate.resolve( strict=False )
+ relative = resolved.relative_to( repository )
+ except ( OSError, ValueError ):
+ return None
+ path = relative.as_posix()
+ if not path.startswith( f"{PRODUCTION_SCOPE}/" ):
+ return None
+ return path
+
+
+def parse_nonnegative_integer( value: str, description: str, minimum: int = 0 ) -> int:
+ try:
+ parsed = int( value, 10 )
+ except ValueError as error:
+ raise ValueError( f"{description} must be an integer" ) from error
+ if parsed < minimum:
+ raise ValueError( f"{description} must be >= {minimum}" )
+ return parsed
+
+
+def parse_native_lcov(
+ native_info: pathlib.Path,
+ repository: pathlib.Path,
+ changed_files: list[dict],
+) -> dict:
+ changed_lines = {
+ record["path"]: set( record["lines"] ) for record in changed_files
+ }
+ line_counts: dict[tuple[str, int], int] = {}
+ branch_outcomes: dict[tuple[str, int, int, int], bool] = {}
+ function_names: dict[tuple[str, int], set[str]] = {}
+ relevant_function_names: dict[tuple[str, str], set[int]] = {}
+ function_counts: dict[tuple[str, str], int] = {}
+ current_path = None
+
+ try:
+ stream = native_info.open( encoding="utf-8", errors="strict" )
+ except OSError as error:
+ raise ValueError( f"cannot read native LCOV input {native_info}: {error}" ) from error
+ try:
+ with stream:
+ for line_number, raw_line in enumerate( stream, 1 ):
+ line = raw_line.rstrip( "\r\n" )
+ if line.startswith( "SF:" ):
+ current_path = normalize_lcov_source( line[3:], repository )
+ if current_path not in changed_lines:
+ current_path = None
+ continue
+ if line == "end_of_record":
+ current_path = None
+ continue
+ if current_path is None:
+ continue
+
+ if line.startswith( "DA:" ):
+ fields = line[3:].split( ",", 2 )
+ if len( fields ) < 2:
+ raise ValueError( f"malformed DA record at LCOV line {line_number}" )
+ source_line = parse_nonnegative_integer(
+ fields[0], f"DA source line at LCOV line {line_number}", 1
+ )
+ count = parse_nonnegative_integer(
+ fields[1], f"DA count at LCOV line {line_number}"
+ )
+ if source_line in changed_lines[current_path]:
+ key = ( current_path, source_line )
+ line_counts[key] = max( line_counts.get( key, 0 ), count )
+ continue
+
+ if line.startswith( "FN:" ):
+ fields = line[3:].split( ",", 1 )
+ if len( fields ) != 2 or not fields[1]:
+ raise ValueError( f"malformed FN record at LCOV line {line_number}" )
+ source_line = parse_nonnegative_integer(
+ fields[0], f"FN source line at LCOV line {line_number}", 1
+ )
+ if source_line in changed_lines[current_path]:
+ name = fields[1]
+ function_names.setdefault(
+ ( current_path, source_line ), set()
+ ).add( name )
+ relevant_function_names.setdefault(
+ ( current_path, name ), set()
+ ).add( source_line )
+ continue
+
+ if line.startswith( "FNDA:" ):
+ fields = line[5:].split( ",", 1 )
+ if len( fields ) != 2 or not fields[1]:
+ raise ValueError( f"malformed FNDA record at LCOV line {line_number}" )
+ name = fields[1]
+ if ( current_path, name ) not in relevant_function_names:
+ continue
+ count = parse_nonnegative_integer(
+ fields[0], f"FNDA count at LCOV line {line_number}"
+ )
+ key = ( current_path, name )
+ function_counts[key] = max( function_counts.get( key, 0 ), count )
+ continue
+
+ if line.startswith( "BRDA:" ):
+ fields = line[5:].split( ",", 3 )
+ if len( fields ) != 4:
+ raise ValueError( f"malformed BRDA record at LCOV line {line_number}" )
+ source_line = parse_nonnegative_integer(
+ fields[0], f"BRDA source line at LCOV line {line_number}", 1
+ )
+ if source_line not in changed_lines[current_path]:
+ continue
+ pair_id = parse_nonnegative_integer(
+ fields[1], f"BRDA pair id at LCOV line {line_number}"
+ )
+ branch_id = parse_nonnegative_integer(
+ fields[2], f"BRDA branch id at LCOV line {line_number}"
+ )
+ if fields[3] == "-":
+ covered = False
+ else:
+ covered = parse_nonnegative_integer(
+ fields[3], f"BRDA count at LCOV line {line_number}"
+ ) > 0
+ key = ( current_path, source_line, pair_id, branch_id )
+ branch_outcomes[key] = branch_outcomes.get( key, False ) or covered
+ except UnicodeError as error:
+ raise ValueError( f"native LCOV input must be valid UTF-8: {error}" ) from error
+
+ function_sites = {}
+ for site, names in function_names.items():
+ covered = any(
+ function_counts.get( ( site[0], name ), 0 ) > 0 for name in names
+ )
+ function_sites[site] = {
+ "covered": covered,
+ "names": sorted( names ),
+ }
+ return {
+ "line_counts": line_counts,
+ "branch_outcomes": branch_outcomes,
+ "function_sites": function_sites,
+ }
+
+
+def patch_metric( covered: int, total: int ) -> dict:
+ return {
+ "covered": covered,
+ "not_covered": total - covered,
+ "total": total,
+ "percent": round( 100.0 * covered / total, 6 ) if total else None,
+ }
+
+
+def location_hint(
+ line_missed: bool,
+ missed_branches: int,
+ covered_branches: int,
+ function_missed: bool,
+) -> str:
+ if line_missed and function_missed:
+ if missed_branches:
+ return "Call this function in a coverage smoke, then exercise its decision outcomes."
+ return "Add a coverage smoke that calls this function entry."
+ if line_missed:
+ if missed_branches:
+ return "Reach this executable line, then exercise its decision outcomes."
+ return "Add a coverage smoke that reaches this executable line."
+ if missed_branches:
+ if covered_branches:
+ return "Add cases that exercise the remaining decision outcome(s)."
+ return "Add cases that take each unobserved decision outcome."
+ if function_missed:
+ return "Add a coverage smoke that calls this function entry."
+ return "Add a focused coverage smoke for this location."
+
+
+def build_patch_report( changed_files: list[dict], lcov: dict ) -> dict:
+ line_counts = lcov["line_counts"]
+ branch_outcomes = lcov["branch_outcomes"]
+ function_sites = lcov["function_sites"]
+ file_results = []
+ locations = []
+
+ for changed_file in changed_files:
+ path = changed_file["path"]
+ changed = set( changed_file["lines"] )
+ file_lines = {
+ line: count
+ for ( source_path, line ), count in line_counts.items()
+ if source_path == path
+ }
+ file_branches = {
+ key: covered
+ for key, covered in branch_outcomes.items()
+ if key[0] == path
+ }
+ file_functions = {
+ line: details
+ for ( source_path, line ), details in function_sites.items()
+ if source_path == path
+ }
+ line_metric = patch_metric(
+ sum( count > 0 for count in file_lines.values() ), len( file_lines )
+ )
+ function_metric = patch_metric(
+ sum( details["covered"] for details in file_functions.values() ),
+ len( file_functions ),
+ )
+ branch_metric = patch_metric(
+ sum( file_branches.values() ), len( file_branches )
+ )
+ file_results.append(
+ {
+ "path": path,
+ "old_path": changed_file["old_path"],
+ "status": changed_file["status"],
+ "changed_lines": len( changed ),
+ "non_instrumented_changed_lines": len( changed - set( file_lines ) ),
+ "metrics": {
+ "executable_lines": line_metric,
+ "function_start_sites": function_metric,
+ "native_branch_outcomes": branch_metric,
+ },
+ }
+ )
+
+ candidate_lines = set( file_lines ) | set( file_functions )
+ candidate_lines.update( key[1] for key in file_branches )
+ for line in candidate_lines:
+ executable = line in file_lines
+ line_covered = file_lines.get( line, 0 ) > 0 if executable else None
+ branch_values = [
+ covered for key, covered in file_branches.items() if key[1] == line
+ ]
+ covered_branches = sum( branch_values )
+ missed_branches = len( branch_values ) - covered_branches
+ function = file_functions.get( line )
+ function_missed = function is not None and not function["covered"]
+ line_missed = executable and not line_covered
+ if not ( line_missed or missed_branches or function_missed ):
+ continue
+ names = function["names"] if function is not None else []
+ locations.append(
+ {
+ "path": path,
+ "line": line,
+ "executable_line": executable,
+ "line_covered": line_covered,
+ "native_branch_outcomes": patch_metric(
+ covered_branches, len( branch_values )
+ ),
+ "function_start_covered": (
+ function["covered"] if function is not None else None
+ ),
+ "function_names": names[:MAX_FUNCTION_NAMES],
+ "additional_function_names": max(
+ 0, len( names ) - MAX_FUNCTION_NAMES
+ ),
+ "hint": location_hint(
+ line_missed,
+ missed_branches,
+ covered_branches,
+ function_missed,
+ ),
+ }
+ )
+
+ file_results.sort(
+ key=lambda result: (
+ -result["metrics"]["native_branch_outcomes"]["not_covered"],
+ -result["metrics"]["executable_lines"]["not_covered"],
+ -result["metrics"]["function_start_sites"]["not_covered"],
+ result["path"],
+ )
+ )
+ locations.sort(
+ key=lambda location: (
+ -location["native_branch_outcomes"]["not_covered"],
+ -( location["executable_line"] and not location["line_covered"] ),
+ -( location["function_start_covered"] is False ),
+ location["path"],
+ location["line"],
+ )
+ )
+
+ executable_total = len( line_counts )
+ function_total = len( function_sites )
+ branch_total = len( branch_outcomes )
+ all_changed_lines = sum( len( record["lines"] ) for record in changed_files )
+ return {
+ "changed_files": len( changed_files ),
+ "changed_lines": all_changed_lines,
+ "non_instrumented_changed_lines": all_changed_lines - executable_total,
+ "metrics": {
+ "executable_lines": patch_metric(
+ sum( count > 0 for count in line_counts.values() ), executable_total
+ ),
+ "function_start_sites": patch_metric(
+ sum( details["covered"] for details in function_sites.values() ),
+ function_total,
+ ),
+ "native_branch_outcomes": patch_metric(
+ sum( branch_outcomes.values() ), branch_total
+ ),
+ },
+ "top_impacted_files": file_results[:MAX_IMPACTED_FILES],
+ "top_impacted_locations": locations[:MAX_IMPACTED_LOCATIONS],
+ }
+
+
+def unavailable_comparison( reason: str, candidate: dict ) -> dict:
+ return {
+ "comparable": False,
+ "reason": reason,
+ "baseline": None,
+ "candidate": candidate["metrics"],
+ "metrics": {},
+ "changed_file_metrics": [],
+ "provenance": None,
+ }
+
+
+def build_aggregate_comparison(
+ candidate: dict,
+ baseline: dict | None,
+ baseline_error: str | None,
+ base_sha: str,
+ head_sha: str,
+ base_tree: str,
+ head_tree: str,
+ changed_files: list[dict],
+) -> dict:
+ if baseline_error is not None:
+ return unavailable_comparison( baseline_error, candidate )
+ if baseline is None:
+ return unavailable_comparison(
+ "No exact-base coverage summary was provided.", candidate
+ )
+ if baseline["schema_version"] != 3 or candidate["schema_version"] != 3:
+ return unavailable_comparison(
+ "N/A: exact comparison requires schema-3 measurement provenance "
+ "on both summaries.",
+ candidate,
+ )
+
+ compatibility_fields = (
+ ( "scope", "production scopes differ" ),
+ ( "excluded_regex", "source exclusion contracts differ" ),
+ )
+ for field, reason in compatibility_fields:
+ if baseline[field] != candidate[field]:
+ return unavailable_comparison( f"N/A: {reason}.", candidate )
+ if baseline["tool"] != candidate["tool"]:
+ return unavailable_comparison( "N/A: LLVM coverage tools differ.", candidate )
+
+ baseline_measurement = baseline["measurement"]
+ candidate_measurement = candidate["measurement"]
+ if baseline_measurement is None or candidate_measurement is None:
+ raise ValueError( "schema-3 coverage summaries are missing validated provenance" )
+ if baseline_measurement["commit_sha"] != base_sha:
+ return unavailable_comparison(
+ "N/A: baseline measurement commit does not match --base-sha.", candidate
+ )
+ if candidate_measurement["commit_sha"] != head_sha:
+ return unavailable_comparison(
+ "N/A: candidate measurement commit does not match --head-sha.", candidate
+ )
+ if baseline_measurement["tree_sha"] != base_tree:
+ return unavailable_comparison(
+ "N/A: baseline measurement tree does not match the base commit.", candidate
+ )
+ if candidate_measurement["tree_sha"] != head_tree:
+ return unavailable_comparison(
+ "N/A: candidate measurement tree does not match the head commit.", candidate
+ )
+ if baseline_measurement["contract_id"] != candidate_measurement["contract_id"]:
+ return unavailable_comparison(
+ "N/A: coverage measurement contract IDs differ.", candidate
+ )
+ if (
+ baseline_measurement["contract_fingerprint"]
+ != candidate_measurement["contract_fingerprint"]
+ ):
+ return unavailable_comparison(
+ "N/A: coverage measurement contract fingerprints differ.", candidate
+ )
+
+ comparisons = {}
+ for name in CANONICAL_METRICS:
+ base_metric = baseline["metrics"][name]
+ head_metric = candidate["metrics"][name]
+ comparisons[name] = {
+ "baseline": base_metric,
+ "candidate": head_metric,
+ "covered_delta": head_metric["covered"] - base_metric["covered"],
+ "not_covered_delta": (
+ head_metric["not_covered"] - base_metric["not_covered"]
+ ),
+ "total_delta": head_metric["total"] - base_metric["total"],
+ "percentage_point_delta": round(
+ head_metric["percent"] - base_metric["percent"], 6
+ ),
+ }
+ baseline_files = {
+ source["path"]: source["metrics"]
+ for source in baseline["per_file_metrics"]
+ }
+ candidate_files = {
+ source["path"]: source["metrics"]
+ for source in candidate["per_file_metrics"]
+ }
+ empty_metric = { "covered": 0, "not_covered": 0, "total": 0, "percent": 0.0 }
+ changed_file_metrics = []
+ for changed_file in changed_files:
+ path = changed_file["path"]
+ baseline_path = changed_file["old_path"] or path
+ baseline_metrics = baseline_files.get( baseline_path, {} )
+ candidate_metrics = candidate_files.get( path, {} )
+ metric_changes = {}
+ for name in CANONICAL_METRICS:
+ base_metric = baseline_metrics.get( name, empty_metric )
+ head_metric = candidate_metrics.get( name, empty_metric )
+ metric_changes[name] = {
+ "baseline": base_metric,
+ "candidate": head_metric,
+ "covered_delta": head_metric["covered"] - base_metric["covered"],
+ "not_covered_delta": (
+ head_metric["not_covered"] - base_metric["not_covered"]
+ ),
+ "total_delta": head_metric["total"] - base_metric["total"],
+ }
+ changed_file_metrics.append(
+ {
+ "path": path,
+ "baseline_path": baseline_path,
+ "metrics": metric_changes,
+ }
+ )
+ changed_file_metrics.sort(
+ key=lambda source: (
+ -source["metrics"]["branches"]["not_covered_delta"],
+ -source["metrics"]["lines"]["not_covered_delta"],
+ source["path"],
+ )
+ )
+ return {
+ "comparable": True,
+ "reason": "Exact commit, tree, and coverage measurement contract match.",
+ "baseline": baseline["metrics"],
+ "candidate": candidate["metrics"],
+ "metrics": comparisons,
+ "changed_file_metrics": changed_file_metrics[:MAX_IMPACTED_FILES],
+ "provenance": {
+ "baseline_commit_sha": base_sha,
+ "candidate_commit_sha": head_sha,
+ "contract_id": candidate_measurement["contract_id"],
+ "contract_fingerprint": candidate_measurement["contract_fingerprint"],
+ },
+ }
+
+
+def markdown_escape( value: str ) -> str:
+ escaped = html.escape( value, quote=True )
+ return (
+ escaped.replace( "|", "|" )
+ .replace( "\r", "
" )
+ .replace( "\n", "
" )
+ )
+
+
+def percent_text( metric: dict ) -> str:
+ if metric["total"] == 0:
+ return "N/A"
+ return f"{100.0 * metric['covered'] / metric['total']:.2f}%"
+
+
+def compact_metric_text( metric: dict ) -> str:
+ if metric["total"] == 0:
+ return "N/A"
+ return (
+ f"{metric['covered']:,}/{metric['total']:,} "
+ f"({percent_text( metric )})"
+ )
+
+
+def signed_integer( value: int ) -> str:
+ return f"{value:+,}"
+
+
+def build_markdown( report: dict ) -> str:
+ aggregate = report["aggregate_comparison"]
+ patch = report["patch"]
+ display_names = {
+ "regions": "Regions",
+ "functions": "Functions",
+ "lines": "Lines",
+ "branches": "Canonical branches",
+ }
+ rows = [
+ "### Pull-request coverage",
+ "",
+ f"Effective source diff: `{report['base_sha'][:12]}` → "
+ f"`{report['head_sha'][:12]}` · Production scope: "
+ f"`{markdown_escape( report['scope'] )}`",
+ "",
+ "#### Project coverage change",
+ "",
+ ]
+ if aggregate["comparable"]:
+ rows.extend(
+ (
+ "| Metric | Exact base | Candidate | Coverage change | "
+ "Covered Δ | Missed Δ | Total Δ |",
+ "|---|---:|---:|---:|---:|---:|---:|",
+ )
+ )
+ for name in DISPLAY_METRICS:
+ comparison = aggregate["metrics"][name]
+ rows.append(
+ f"| {display_names[name]} | "
+ f"{compact_metric_text( comparison['baseline'] )} | "
+ f"{compact_metric_text( comparison['candidate'] )} | "
+ f"{comparison['percentage_point_delta']:+.2f} pp | "
+ f"{signed_integer( comparison['covered_delta'] )} | "
+ f"{signed_integer( comparison['not_covered_delta'] )} | "
+ f"{signed_integer( comparison['total_delta'] )} |"
+ )
+ rows.extend(
+ (
+ "",
+ "Comparison is exact because commit, tree, and measurement-contract "
+ "provenance match.",
+ )
+ )
+ changed_file_metrics = aggregate["changed_file_metrics"]
+ if changed_file_metrics:
+ rows.extend(
+ (
+ "",
+ "##### Canonical movement in changed files",
+ "",
+ "| Source file | Base branches | Candidate branches | "
+ "Missed branch Δ | Missed line Δ |",
+ "|---|---:|---:|---:|---:|",
+ )
+ )
+ for source in changed_file_metrics:
+ branch_change = source["metrics"]["branches"]
+ line_change = source["metrics"]["lines"]
+ display_path = source["path"]
+ if source["baseline_path"] != source["path"]:
+ display_path = f"{source['baseline_path']} → {source['path']}"
+ rows.append(
+ f"| {markdown_escape( display_path )} | "
+ f"{compact_metric_text( branch_change['baseline'] )} | "
+ f"{compact_metric_text( branch_change['candidate'] )} | "
+ f"{signed_integer( branch_change['not_covered_delta'] )} | "
+ f"{signed_integer( line_change['not_covered_delta'] )} |"
+ )
+ rows.extend(
+ (
+ "",
+ "This is whole-file canonical context, ranked by newly missed "
+ "branches; the changed-code table below is the hunk-specific signal.",
+ )
+ )
+ else:
+ rows.extend(
+ (
+ "| Comparison | Result | Reason |",
+ "|---|:---:|---|",
+ "| Exact base → candidate | ℹ️ N/A | "
+ f"{markdown_escape( aggregate['reason'] )} |",
+ )
+ )
+
+ patch_metrics = patch["metrics"]
+ rows.extend(
+ (
+ "",
+ "#### Changed-code coverage",
+ "",
+ "| Signal | Covered | Missed | Total | Coverage | Interpretation |",
+ "|---|---:|---:|---:|---:|---|",
+ f"| Executable changed lines | "
+ f"{patch_metrics['executable_lines']['covered']:,} | "
+ f"{patch_metrics['executable_lines']['not_covered']:,} | "
+ f"{patch_metrics['executable_lines']['total']:,} | "
+ f"{percent_text( patch_metrics['executable_lines'] )} | "
+ "Exact LLVM line mapping on added/modified head lines |",
+ f"| Changed function start sites | "
+ f"{patch_metrics['function_start_sites']['covered']:,} | "
+ f"{patch_metrics['function_start_sites']['not_covered']:,} | "
+ f"{patch_metrics['function_start_sites']['total']:,} | "
+ f"{percent_text( patch_metrics['function_start_sites'] )} | "
+ "Advisory; C++ instantiations may share a source definition |",
+ f"| Native changed branch outcomes | "
+ f"{patch_metrics['native_branch_outcomes']['covered']:,} | "
+ f"{patch_metrics['native_branch_outcomes']['not_covered']:,} | "
+ f"{patch_metrics['native_branch_outcomes']['total']:,} | "
+ f"{percent_text( patch_metrics['native_branch_outcomes'] )} | "
+ "Advisory and instantiation-weighted; not the canonical branch gate |",
+ "",
+ f"Git identified **{patch['changed_lines']:,}** added/modified head lines "
+ f"across **{patch['changed_files']:,}** production files. "
+ f"**{patch['non_instrumented_changed_lines']:,}** changed lines have no "
+ "LLVM line mapping; these may be comments, declarations, braces, or "
+ "configuration-dependent code.",
+ )
+ )
+ if patch["excluded_changed_files"]:
+ rows.append(
+ f"The production coverage contract excludes an additional "
+ f"**{patch['excluded_changed_lines']:,}** changed lines across "
+ f"**{patch['excluded_changed_files']:,}** test/example/support files."
+ )
+
+ impacted_files = patch["top_impacted_files"]
+ if impacted_files:
+ rows.extend(
+ (
+ "",
+ "#### Changed files with the largest coverage gaps",
+ "",
+ "| Source file | Change | Changed lines | Executable lines | "
+ "Function starts | Native branches |",
+ "|---|:---:|---:|---:|---:|---:|",
+ )
+ )
+ for file_result in impacted_files:
+ metrics = file_result["metrics"]
+ rows.append(
+ f"| {markdown_escape( file_result['path'] )} | "
+ f"{file_result['status']} | {file_result['changed_lines']:,} | "
+ f"{compact_metric_text( metrics['executable_lines'] )} | "
+ f"{compact_metric_text( metrics['function_start_sites'] )} | "
+ f"{compact_metric_text( metrics['native_branch_outcomes'] )} |"
+ )
+
+ impacted_locations = patch["top_impacted_locations"]
+ rows.extend( ( "", "#### Highest-impact changed locations", "" ) )
+ if impacted_locations:
+ rows.extend(
+ (
+ "| Location | Coverage gap | Suggested test |",
+ "|---|---|---|",
+ )
+ )
+ for location in impacted_locations:
+ gaps = []
+ if location["executable_line"] and not location["line_covered"]:
+ gaps.append( "executable line not hit" )
+ missed_branches = location["native_branch_outcomes"]["not_covered"]
+ if missed_branches:
+ gaps.append(
+ f"{missed_branches:,} native branch outcome"
+ f"{'s' if missed_branches != 1 else ''} missed"
+ )
+ if location["function_start_covered"] is False:
+ function_detail = "function start not entered"
+ if location["function_names"]:
+ names = ", ".join(
+ f"{markdown_escape( name )}"
+ for name in location["function_names"]
+ )
+ function_detail += f" ({names}"
+ if location["additional_function_names"]:
+ function_detail += (
+ f", +{location['additional_function_names']:,} more"
+ )
+ function_detail += ")"
+ gaps.append( function_detail )
+ rows.append(
+ f"| {markdown_escape( location['path'] )}:"
+ f"{location['line']} | {'; '.join( gaps )} | "
+ f"{markdown_escape( location['hint'] )} |"
+ )
+ else:
+ rows.append(
+ "No uncovered instrumented changed locations were found. If the patch "
+ "has no executable changed lines, this section is informationally empty."
+ )
+
+ rows.extend(
+ (
+ "",
+ "Changed-line coverage is exact. Function-start and native branch details "
+ "are diagnostics because LLVM 20's detailed export retains C++ template "
+ "and inline instantiations.",
+ "",
+ "This report is advisory and does not enforce a threshold.",
+ )
+ )
+ return "\n".join( rows ) + "\n"
+
+
+def analyze(
+ repository: pathlib.Path,
+ base_sha: str,
+ head_sha: str,
+ native_info: pathlib.Path,
+ candidate_summary_path: pathlib.Path,
+ baseline_summary_path: pathlib.Path | None = None,
+) -> tuple[dict, str]:
+ repository = validate_repository( repository )
+ base_sha = validate_commit( repository, base_sha, "--base-sha" )
+ head_sha = validate_commit( repository, head_sha, "--head-sha" )
+ base_tree = commit_tree( repository, base_sha )
+ head_tree = commit_tree( repository, head_sha )
+ native_info = checked_input_file( native_info, "--native-info" )
+ candidate_summary_path = checked_input_file(
+ candidate_summary_path, "--candidate-summary"
+ )
+ candidate = validate_summary(
+ read_json( candidate_summary_path, "candidate summary" ),
+ "candidate summary",
+ )
+ if candidate["scope"] != PRODUCTION_SCOPE:
+ raise ValueError(
+ f"candidate summary scope {candidate['scope']!r} does not match "
+ f"{PRODUCTION_SCOPE!r}"
+ )
+
+ baseline = None
+ baseline_error = None
+ if baseline_summary_path is not None:
+ try:
+ baseline_summary_path = checked_input_file(
+ baseline_summary_path, "--baseline-summary"
+ )
+ baseline = validate_summary(
+ read_json( baseline_summary_path, "baseline summary" ),
+ "baseline summary",
+ )
+ except ValueError as error:
+ baseline_error = f"N/A: baseline summary is invalid: {error}"
+
+ changed_files = changed_line_ranges( repository, base_sha, head_sha )
+ try:
+ exclusion_pattern = re.compile( candidate["excluded_regex"] )
+ except re.error as error:
+ raise ValueError(
+ f"candidate summary excluded_regex is invalid: {error}"
+ ) from error
+ included_changed_files = []
+ excluded_changed_files = []
+ for changed_file in changed_files:
+ absolute_source = ( repository / changed_file["path"] ).as_posix()
+ if exclusion_pattern.search( absolute_source ) is not None:
+ excluded_changed_files.append( changed_file )
+ else:
+ included_changed_files.append( changed_file )
+ changed_files = included_changed_files
+ lcov = parse_native_lcov( native_info, repository, changed_files )
+ patch = build_patch_report( changed_files, lcov )
+ patch["excluded_changed_files"] = len( excluded_changed_files )
+ patch["excluded_changed_lines"] = sum(
+ len( changed_file["lines"] ) for changed_file in excluded_changed_files
+ )
+ aggregate = build_aggregate_comparison(
+ candidate,
+ baseline,
+ baseline_error,
+ base_sha,
+ head_sha,
+ base_tree,
+ head_tree,
+ changed_files,
+ )
+ report = {
+ "schema_version": REPORT_SCHEMA_VERSION,
+ "scope": PRODUCTION_SCOPE,
+ "base_sha": base_sha,
+ "head_sha": head_sha,
+ "base_tree_sha": base_tree,
+ "head_tree_sha": head_tree,
+ "candidate_summary_schema_version": candidate["schema_version"],
+ "aggregate_comparison": aggregate,
+ "patch": patch,
+ }
+ return report, build_markdown( report )
+
+
+def checked_output_path(
+ path: pathlib.Path,
+ inputs: set[pathlib.Path],
+ description: str,
+) -> pathlib.Path:
+ if path.is_symlink():
+ raise ValueError( f"{description} must not be a symbolic link: {path}" )
+ absolute = path.absolute()
+ if absolute in inputs:
+ raise ValueError( f"{description} must not overwrite an input file" )
+ return absolute
+
+
+def write_atomic( path: pathlib.Path, contents: str ) -> None:
+ path.parent.mkdir( parents=True, exist_ok=True )
+ if path.is_symlink():
+ raise ValueError( f"refusing to replace symbolic link: {path}" )
+ descriptor, temporary_name = tempfile.mkstemp(
+ prefix=f".{path.name}.", dir=path.parent
+ )
+ temporary_path = pathlib.Path( temporary_name )
+ try:
+ # The CI analyzer runs as root in Docker while the GitHub runner reads
+ # these files from a bind mount. mkstemp defaults to 0600, so make the
+ # final compact report deliberately host-readable before replacement.
+ os.fchmod( descriptor, 0o644 )
+ with os.fdopen( descriptor, "w", encoding="utf-8" ) as stream:
+ stream.write( contents )
+ os.replace( temporary_path, path )
+ except BaseException:
+ temporary_path.unlink( missing_ok=True )
+ raise
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(
+ description="Report advisory source coverage for a Git pull-request diff."
+ )
+ parser.add_argument( "--repository", required=True, type=pathlib.Path )
+ parser.add_argument( "--base-sha", required=True )
+ parser.add_argument( "--head-sha", required=True )
+ parser.add_argument( "--native-info", required=True, type=pathlib.Path )
+ parser.add_argument( "--candidate-summary", required=True, type=pathlib.Path )
+ parser.add_argument( "--baseline-summary", type=pathlib.Path )
+ parser.add_argument( "--output-json", type=pathlib.Path )
+ parser.add_argument( "--output-markdown", type=pathlib.Path )
+ args = parser.parse_args()
+
+ try:
+ input_paths = {
+ checked_input_file( args.native_info, "--native-info" ),
+ checked_input_file( args.candidate_summary, "--candidate-summary" ),
+ }
+ if args.baseline_summary is not None:
+ # A malformed baseline remains advisory, but an output must never be
+ # allowed to overwrite the caller-provided path.
+ input_paths.add( args.baseline_summary.absolute() )
+ output_json = (
+ checked_output_path( args.output_json, input_paths, "--output-json" )
+ if args.output_json is not None
+ else None
+ )
+ output_markdown = (
+ checked_output_path(
+ args.output_markdown, input_paths, "--output-markdown"
+ )
+ if args.output_markdown is not None
+ else None
+ )
+ if output_json is not None and output_json == output_markdown:
+ raise ValueError( "--output-json and --output-markdown must differ" )
+
+ report, markdown = analyze(
+ args.repository,
+ args.base_sha,
+ args.head_sha,
+ args.native_info,
+ args.candidate_summary,
+ args.baseline_summary,
+ )
+ json_text = json.dumps(
+ report, indent=2, sort_keys=True, ensure_ascii=False
+ ) + "\n"
+ if output_json is not None:
+ write_atomic( output_json, json_text )
+ if output_markdown is not None:
+ write_atomic( output_markdown, markdown )
+ else:
+ print( markdown, end="" )
+ return 0
+ except ( OSError, ValueError ) as error:
+ print( f"PR coverage comparison error: {error}", file=sys.stderr )
+ return 2
+
+
+if __name__ == "__main__":
+ sys.exit( main() )
diff --git a/scripts/fetch_coverage_baseline.py b/scripts/fetch_coverage_baseline.py
new file mode 100755
index 00000000000..f737735dfa3
--- /dev/null
+++ b/scripts/fetch_coverage_baseline.py
@@ -0,0 +1,802 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-only
+
+"""Fetch an exact-base artifact published by a successful coverage job."""
+
+import argparse
+import http.client
+import io
+import json
+import os
+import pathlib
+import re
+import stat
+import sys
+import tempfile
+import urllib.error
+import urllib.parse
+import urllib.request
+import zipfile
+
+
+SCRIPT_DIRECTORY = pathlib.Path( __file__ ).resolve().parent
+if str( SCRIPT_DIRECTORY ) not in sys.path:
+ sys.path.insert( 0, str( SCRIPT_DIRECTORY ) )
+import check_coverage_thresholds as coverage_contract
+import zlib
+
+
+API_ROOT = "https://api.github.com"
+API_VERSION = "2022-11-28"
+USER_AGENT = "geos-coverage-baseline-fetch/1"
+DOWNLOAD_LIMIT = 10 * 1024 * 1024
+JSON_RESPONSE_LIMIT = 2 * 1024 * 1024
+PER_PAGE = 100
+MAX_PAGES = 10
+REQUEST_TIMEOUT_SECONDS = 30
+READ_CHUNK_SIZE = 64 * 1024
+EXPECTED_FILES = frozenset(
+ ( "coverage-summary.json", "llvm-summary.json", "toolchain.txt" )
+)
+REQUIRED_FILES = frozenset( ( "coverage-summary.json", ) )
+STATUS_FILE = "coverage-baseline-status.json"
+SHA_RE = re.compile( r"^[0-9a-fA-F]{40}$" )
+REPOSITORY_PART_RE = re.compile( r"^[A-Za-z0-9_.-]+$" )
+WORKFLOW_RE = re.compile( r"^[A-Za-z0-9_.-]+$" )
+ENVIRONMENT_NAME_RE = re.compile( r"^[A-Za-z_][A-Za-z0-9_]*$" )
+TOKEN_RE = re.compile( r"^[\x21-\x7e]+$" )
+ALLOWED_ZIP_CONTENT_TYPES = frozenset(
+ ( "application/octet-stream", "application/x-zip-compressed", "application/zip" )
+)
+SENSITIVE_REDIRECT_HEADERS = frozenset(
+ (
+ "authorization",
+ "cookie",
+ "cookie2",
+ "proxy-authorization",
+ "x-github-api-version",
+ )
+)
+
+
+class BaselineFetchError( Exception ):
+ """A malformed input, unsafe artifact, or GitHub API failure."""
+
+
+class BaselineMissing( Exception ):
+ """The exact-base run or artifact is not available."""
+
+ def __init__( self, reason_code: str, message: str ) -> None:
+ super().__init__( message )
+ self.reason_code = reason_code
+
+
+def _https_url_parts( url: str, description: str ):
+ if not isinstance( url, str ) or any(
+ ord( character ) < 32 or ord( character ) == 127 for character in url
+ ):
+ raise BaselineFetchError( f"{description} is not a safe HTTPS URL" )
+ try:
+ parsed = urllib.parse.urlsplit( url )
+ hostname = parsed.hostname
+ port = parsed.port
+ username = parsed.username
+ password = parsed.password
+ except ( TypeError, ValueError ) as error:
+ raise BaselineFetchError( f"{description} is not a safe HTTPS URL" ) from error
+ if (
+ parsed.scheme != "https"
+ or not hostname
+ or username is not None
+ or password is not None
+ ):
+ raise BaselineFetchError( f"{description} is not a safe HTTPS URL" )
+ return parsed, hostname.lower(), 443 if port is None else port
+
+
+class SafeRedirectHandler( urllib.request.HTTPRedirectHandler ):
+ """Allow HTTPS redirects without forwarding credentials across origins."""
+
+ def redirect_request( self, request, file_pointer, code, message, headers, url ):
+ del file_pointer, code, message, headers
+ try:
+ redirected_url = urllib.parse.urljoin( request.full_url, url )
+ except ( TypeError, ValueError ) as error:
+ raise BaselineFetchError(
+ "GitHub redirect URL is not a safe HTTPS URL"
+ ) from error
+ old, old_hostname, old_port = _https_url_parts(
+ request.full_url, "GitHub request URL"
+ )
+ _, new_hostname, new_port = _https_url_parts(
+ redirected_url, "GitHub redirect URL"
+ )
+
+ redirected_headers = {}
+ same_origin = ( old.scheme, old_hostname, old_port ) == (
+ "https",
+ new_hostname,
+ new_port,
+ )
+ for name, value in request.header_items():
+ if not same_origin and name.lower() in SENSITIVE_REDIRECT_HEADERS:
+ continue
+ redirected_headers[name] = value
+ return urllib.request.Request(
+ redirected_url,
+ headers=redirected_headers,
+ origin_req_host=request.origin_req_host,
+ unverifiable=True,
+ method="GET",
+ )
+
+
+def _default_opener():
+ return urllib.request.build_opener( SafeRedirectHandler() )
+
+
+def _require_content_length( headers, limit: int ) -> None:
+ value = headers.get( "Content-Length" )
+ if value is None:
+ return
+ if not isinstance( value, str ) or not re.fullmatch( r"[0-9]+", value ):
+ raise BaselineFetchError( "HTTP response has an invalid Content-Length" )
+ length = int( value, 10 )
+ if length > limit:
+ raise BaselineFetchError(
+ f"HTTP response exceeds the {limit}-byte download limit"
+ )
+
+
+def _read_bounded( response, limit: int ) -> bytes:
+ _require_content_length( response.headers, limit )
+ chunks = []
+ size = 0
+ while True:
+ chunk = response.read( min( READ_CHUNK_SIZE, limit + 1 - size ) )
+ if not isinstance( chunk, bytes ):
+ raise BaselineFetchError( "HTTP response returned non-byte content" )
+ if not chunk:
+ return b"".join( chunks )
+ chunks.append( chunk )
+ size += len( chunk )
+ if size > limit:
+ raise BaselineFetchError(
+ f"HTTP response exceeds the {limit}-byte download limit"
+ )
+
+
+def _content_type( headers ) -> str:
+ value = headers.get( "Content-Type", "" )
+ return value.split( ";", maxsplit=1 )[0].strip().lower()
+
+
+class GitHubClient:
+ def __init__( self, token: str, opener=None ) -> None:
+ if not isinstance( token, str ) or not TOKEN_RE.fullmatch( token ):
+ raise BaselineFetchError( "GitHub token is empty or invalid" )
+ self._token = token
+ self._opener = opener if opener is not None else _default_opener()
+
+ def _request( self, path: str, query=None ):
+ if not path.startswith( "/" ):
+ raise BaselineFetchError( "GitHub API path must be absolute" )
+ url = f"{API_ROOT}{path}"
+ if query:
+ url = f"{url}?{urllib.parse.urlencode( query )}"
+ request = urllib.request.Request(
+ url,
+ headers={
+ "Accept": "application/vnd.github+json",
+ "Authorization": f"Bearer {self._token}",
+ "User-Agent": USER_AGENT,
+ "X-GitHub-Api-Version": API_VERSION,
+ },
+ method="GET",
+ )
+ try:
+ return self._opener.open( request, timeout=REQUEST_TIMEOUT_SECONDS )
+ except urllib.error.HTTPError:
+ raise
+ except BaselineFetchError:
+ raise
+ except (
+ OSError,
+ urllib.error.URLError,
+ http.client.HTTPException,
+ ) as error:
+ raise BaselineFetchError( f"GitHub request failed: {error}" ) from error
+
+ def get_json( self, path: str, query=None ) -> dict:
+ try:
+ response = self._request( path, query )
+ with response:
+ status = getattr( response, "status", response.getcode() )
+ if status != 200:
+ raise BaselineFetchError(
+ f"GitHub JSON request returned HTTP {status}"
+ )
+ _, final_hostname, final_port = _https_url_parts(
+ response.geturl(), "GitHub JSON response URL"
+ )
+ if final_hostname != "api.github.com" or final_port != 443:
+ raise BaselineFetchError( "GitHub JSON request left api.github.com" )
+ if _content_type( response.headers ) not in (
+ "application/json",
+ "application/vnd.github+json",
+ ):
+ raise BaselineFetchError(
+ "GitHub JSON response has an unexpected Content-Type"
+ )
+ payload = _read_bounded( response, JSON_RESPONSE_LIMIT )
+ except urllib.error.HTTPError as error:
+ raise BaselineFetchError(
+ f"GitHub JSON request returned HTTP {error.code}"
+ ) from error
+ except BaselineFetchError:
+ raise
+ except (
+ OSError,
+ urllib.error.URLError,
+ http.client.HTTPException,
+ ) as error:
+ raise BaselineFetchError(
+ f"GitHub JSON response failed: {error}"
+ ) from error
+
+ try:
+ document = json.loads( payload.decode( "utf-8" ) )
+ except ( UnicodeDecodeError, ValueError, RecursionError ) as error:
+ raise BaselineFetchError( "GitHub returned malformed JSON" ) from error
+ if not isinstance( document, dict ):
+ raise BaselineFetchError( "GitHub JSON response must be an object" )
+ return document
+
+ def download_artifact( self, repository_path: str, artifact_id: int ) -> bytes:
+ path = f"{repository_path}/actions/artifacts/{artifact_id}/zip"
+ try:
+ response = self._request( path )
+ with response:
+ status = getattr( response, "status", response.getcode() )
+ if status in ( 404, 410 ):
+ raise BaselineMissing(
+ "artifact_download_missing",
+ "the exact-base coverage artifact disappeared or expired",
+ )
+ if status != 200:
+ raise BaselineFetchError(
+ f"artifact download returned HTTP {status}"
+ )
+ _https_url_parts( response.geturl(), "artifact download URL" )
+ if _content_type( response.headers ) not in ALLOWED_ZIP_CONTENT_TYPES:
+ raise BaselineFetchError(
+ "artifact download has an unexpected Content-Type"
+ )
+ return _read_bounded( response, DOWNLOAD_LIMIT )
+ except urllib.error.HTTPError as error:
+ if error.code in ( 404, 410 ):
+ raise BaselineMissing(
+ "artifact_download_missing",
+ "the exact-base coverage artifact disappeared or expired",
+ ) from error
+ raise BaselineFetchError(
+ f"artifact download returned HTTP {error.code}"
+ ) from error
+ except BaselineFetchError:
+ raise
+ except (
+ OSError,
+ urllib.error.URLError,
+ http.client.HTTPException,
+ ) as error:
+ raise BaselineFetchError(
+ f"artifact download response failed: {error}"
+ ) from error
+
+
+def validate_repository( repository: str ) -> tuple[str, str]:
+ parts = repository.split( "/" )
+ if (
+ len( parts ) != 2
+ or not parts[0]
+ or not parts[1]
+ or any( part in ( ".", ".." ) for part in parts )
+ or not all( REPOSITORY_PART_RE.fullmatch( part ) for part in parts )
+ ):
+ raise BaselineFetchError( "repository must have the form owner/repo" )
+ return parts[0], parts[1]
+
+
+def validate_workflow( workflow: str ) -> str:
+ if workflow in ( ".", ".." ) or not WORKFLOW_RE.fullmatch( workflow ):
+ raise BaselineFetchError( "workflow must be a workflow filename or numeric ID" )
+ return workflow
+
+
+def validate_sha( revision: str, description: str = "base SHA" ) -> str:
+ if not isinstance( revision, str ) or not SHA_RE.fullmatch( revision ):
+ raise BaselineFetchError( f"{description} must be exactly 40 hexadecimal digits" )
+ return revision.lower()
+
+
+def validate_artifact_prefix( prefix: str ) -> str:
+ if (
+ not prefix
+ or len( prefix ) > 160
+ or any( not character.isprintable() for character in prefix )
+ or "/" in prefix
+ or "\\" in prefix
+ ):
+ raise BaselineFetchError( "artifact name prefix is invalid" )
+ return prefix
+
+
+def repository_api_path( repository: str ) -> str:
+ owner, name = validate_repository( repository )
+ return "/repos/{}/{}".format(
+ urllib.parse.quote( owner, safe="" ), urllib.parse.quote( name, safe="" )
+ )
+
+
+def paginated_items(
+ client: GitHubClient,
+ path: str,
+ list_key: str,
+ query: dict,
+):
+ expected_total = None
+ yielded = 0
+ for page in range( 1, MAX_PAGES + 1 ):
+ page_query = dict( query )
+ page_query.update( { "page": page, "per_page": PER_PAGE } )
+ document = client.get_json( path, page_query )
+ total = document.get( "total_count" )
+ if isinstance( total, bool ) or not isinstance( total, int ) or total < 0:
+ raise BaselineFetchError( "GitHub paginated response has invalid total_count" )
+ if expected_total is None:
+ expected_total = total
+ elif total != expected_total:
+ raise BaselineFetchError( "GitHub paginated total_count changed between pages" )
+ items = document.get( list_key )
+ if not isinstance( items, list ):
+ raise BaselineFetchError(
+ f"GitHub paginated response is missing the {list_key} array"
+ )
+ if len( items ) > PER_PAGE:
+ raise BaselineFetchError(
+ "GitHub paginated response exceeds the requested page size"
+ )
+ yielded += len( items )
+ if yielded > expected_total:
+ raise BaselineFetchError(
+ "GitHub paginated response contains more entries than total_count"
+ )
+ for item in items:
+ yield item
+ if yielded == expected_total:
+ return
+ if len( items ) < PER_PAGE:
+ raise BaselineFetchError(
+ "GitHub paginated response ended before total_count"
+ )
+ raise BaselineFetchError( "GitHub paginated response exceeds the page limit" )
+
+
+def find_exact_run(
+ client: GitHubClient, repository_path: str, workflow: str, base_sha: str
+) -> dict:
+ workflow_component = urllib.parse.quote( validate_workflow( workflow ), safe="" )
+ path = f"{repository_path}/actions/workflows/{workflow_component}/runs"
+ query = {
+ "branch": "develop",
+ "event": "push",
+ "head_sha": base_sha,
+ # The exact baseline artifact is uploaded only after the coverage job
+ # succeeds. The monolithic workflow may still fail for an unrelated
+ # CUDA/integrated job, so workflow-level conclusion is not the trust
+ # boundary for this artifact.
+ "status": "completed",
+ }
+ matches = []
+ for run in paginated_items( client, path, "workflow_runs", query ):
+ if not isinstance( run, dict ):
+ raise BaselineFetchError( "GitHub workflow run entry must be an object" )
+ head_sha = run.get( "head_sha" )
+ if not isinstance( head_sha, str ) or not SHA_RE.fullmatch( head_sha ):
+ raise BaselineFetchError( "GitHub workflow run has an invalid head_sha" )
+ if head_sha.lower() != base_sha:
+ continue
+ if (
+ run.get( "event" ) != "push"
+ or run.get( "head_branch" ) != "develop"
+ or run.get( "status" ) != "completed"
+ ):
+ continue
+ run_id = run.get( "id" )
+ attempt = run.get( "run_attempt", 1 )
+ if (
+ isinstance( run_id, bool )
+ or not isinstance( run_id, int )
+ or run_id < 1
+ or isinstance( attempt, bool )
+ or not isinstance( attempt, int )
+ or attempt < 1
+ ):
+ raise BaselineFetchError( "GitHub workflow run has an invalid identity" )
+ matches.append( ( attempt, run_id, run ) )
+
+ if not matches:
+ raise BaselineMissing(
+ "workflow_run_missing",
+ f"no completed develop push run exists for exact base {base_sha}",
+ )
+ matches.sort( key=lambda candidate: ( candidate[1], candidate[0] ), reverse=True )
+ return matches[0][2]
+
+
+def find_exact_artifact(
+ client: GitHubClient,
+ repository_path: str,
+ run_id: int,
+ expected_name: str,
+) -> dict:
+ path = f"{repository_path}/actions/runs/{run_id}/artifacts"
+ exact = []
+ for artifact in paginated_items( client, path, "artifacts", {} ):
+ if not isinstance( artifact, dict ):
+ raise BaselineFetchError( "GitHub artifact entry must be an object" )
+ if artifact.get( "name" ) != expected_name:
+ continue
+ expired = artifact.get( "expired" )
+ if not isinstance( expired, bool ):
+ raise BaselineFetchError( "GitHub artifact has an invalid expired value" )
+ artifact_id = artifact.get( "id" )
+ if (
+ isinstance( artifact_id, bool )
+ or not isinstance( artifact_id, int )
+ or artifact_id < 1
+ ):
+ raise BaselineFetchError( "GitHub artifact has an invalid ID" )
+ exact.append( artifact )
+
+ available = [artifact for artifact in exact if not artifact["expired"]]
+ if len( available ) > 1:
+ raise BaselineFetchError( "multiple unexpired exact-base artifacts were found" )
+ if available:
+ return available[0]
+ if exact:
+ raise BaselineMissing(
+ "artifact_expired", f"exact-base artifact {expected_name} has expired"
+ )
+ raise BaselineMissing(
+ "artifact_missing", f"exact-base artifact {expected_name} was not found"
+ )
+
+
+def _is_regular_zip_entry( entry: zipfile.ZipInfo ) -> bool:
+ if entry.is_dir():
+ return False
+ if entry.create_system == 3:
+ mode = entry.external_attr >> 16
+ file_type = stat.S_IFMT( mode )
+ return file_type in ( 0, stat.S_IFREG )
+ if entry.create_system == 0:
+ dos_attributes = entry.external_attr & 0xFF
+ return not dos_attributes & 0x18 # Neither a volume label nor a directory.
+ return False
+
+
+def validate_summary_revision( payload: bytes, base_sha: str ) -> None:
+ try:
+ document = json.loads( payload.decode( "utf-8" ) )
+ except ( UnicodeDecodeError, ValueError, RecursionError ) as error:
+ raise BaselineFetchError( "coverage-summary.json is not valid UTF-8 JSON" ) from error
+ if not isinstance( document, dict ):
+ raise BaselineFetchError( "coverage-summary.json must contain an object" )
+ schema_version = document.get( "schema_version" )
+ if (
+ isinstance( schema_version, bool )
+ or not isinstance( schema_version, int )
+ or schema_version < 1
+ ):
+ raise BaselineFetchError( "coverage-summary.json has an invalid schema_version" )
+
+ if schema_version == coverage_contract.COVERAGE_SUMMARY_SCHEMA_VERSION:
+ try:
+ document = coverage_contract.validate_summary( document )
+ except ValueError as error:
+ raise BaselineFetchError(
+ f"coverage-summary.json schema 3 contract is invalid: {error}"
+ ) from error
+ elif schema_version > coverage_contract.COVERAGE_SUMMARY_SCHEMA_VERSION:
+ raise BaselineFetchError(
+ f"coverage-summary.json schema {schema_version} is unsupported"
+ )
+
+ revisions = []
+ measurement = document.get( "measurement" )
+ if schema_version >= 3:
+ if not isinstance( measurement, dict ) or "commit_sha" not in measurement:
+ raise BaselineFetchError(
+ "coverage-summary.json schema 3+ requires measurement.commit_sha"
+ )
+ if measurement is not None:
+ if not isinstance( measurement, dict ):
+ raise BaselineFetchError(
+ "coverage-summary.json measurement must be an object"
+ )
+ if "commit_sha" in measurement:
+ revisions.append( ( "measurement.commit_sha", measurement["commit_sha"] ) )
+
+ source = document.get( "source" )
+ if source is not None:
+ if not isinstance( source, dict ):
+ raise BaselineFetchError( "coverage-summary.json source must be an object" )
+ for key in ( "commit", "revision" ):
+ if key in source:
+ revisions.append( ( f"source.{key}", source[key] ) )
+ for key in ( "source_revision", "revision" ):
+ if key in document:
+ revisions.append( ( key, document[key] ) )
+
+ for description, revision in revisions:
+ validated = validate_sha( revision, f"coverage summary {description}" )
+ if validated != base_sha:
+ raise BaselineFetchError(
+ f"coverage summary {description} does not match the requested base SHA"
+ )
+
+
+def validate_optional_json( filename: str, payload: bytes ) -> None:
+ try:
+ document = json.loads( payload.decode( "utf-8" ) )
+ except ( UnicodeDecodeError, ValueError, RecursionError ) as error:
+ raise BaselineFetchError( f"{filename} is not valid UTF-8 JSON" ) from error
+ if not isinstance( document, dict ):
+ raise BaselineFetchError( f"{filename} must contain a JSON object" )
+
+
+def read_safe_archive( archive: bytes, base_sha: str ) -> dict[str, bytes]:
+ if len( archive ) > DOWNLOAD_LIMIT:
+ raise BaselineFetchError( "coverage artifact archive exceeds the download limit" )
+ try:
+ with zipfile.ZipFile( io.BytesIO( archive ) ) as bundle:
+ entries = bundle.infolist()
+ if not entries:
+ raise BaselineFetchError( "coverage artifact ZIP is empty" )
+ if len( entries ) > len( EXPECTED_FILES ):
+ raise BaselineFetchError(
+ "coverage artifact ZIP contains too many entries"
+ )
+ result = {}
+ total_size = 0
+ for entry in entries:
+ name = entry.filename
+ if (
+ not isinstance( name, str )
+ or entry.orig_filename != name
+ or name not in EXPECTED_FILES
+ or pathlib.PurePosixPath( name ).parts != ( name, )
+ or "\\" in name
+ ):
+ raise BaselineFetchError(
+ f"coverage artifact contains unexpected entry {name!r}"
+ )
+ if name in result:
+ raise BaselineFetchError(
+ f"coverage artifact contains duplicate entry {name}"
+ )
+ if not _is_regular_zip_entry( entry ):
+ raise BaselineFetchError(
+ f"coverage artifact entry {name} is not a regular file"
+ )
+ if entry.flag_bits & 0x1:
+ raise BaselineFetchError(
+ f"coverage artifact entry {name} is encrypted"
+ )
+ if entry.compress_type not in ( zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED ):
+ raise BaselineFetchError(
+ f"coverage artifact entry {name} uses an unsupported compression"
+ )
+ if entry.file_size < 0 or entry.file_size > DOWNLOAD_LIMIT:
+ raise BaselineFetchError(
+ f"coverage artifact entry {name} exceeds the extraction limit"
+ )
+ total_size += entry.file_size
+ if total_size > DOWNLOAD_LIMIT:
+ raise BaselineFetchError(
+ "coverage artifact uncompressed content exceeds the extraction limit"
+ )
+ with bundle.open( entry, "r" ) as stream:
+ payload = stream.read( entry.file_size + 1 )
+ if len( payload ) != entry.file_size:
+ raise BaselineFetchError(
+ f"coverage artifact entry {name} has an inconsistent size"
+ )
+ result[name] = payload
+ except BaselineFetchError:
+ raise
+ except (
+ OSError,
+ EOFError,
+ RuntimeError,
+ ValueError,
+ zipfile.BadZipFile,
+ zipfile.LargeZipFile,
+ zlib.error,
+ ) as error:
+ raise BaselineFetchError( f"coverage artifact is not a safe ZIP: {error}" ) from error
+
+ missing = REQUIRED_FILES - result.keys()
+ if missing:
+ raise BaselineFetchError(
+ "coverage artifact is missing required file coverage-summary.json"
+ )
+ validate_summary_revision( result["coverage-summary.json"], base_sha )
+ if "llvm-summary.json" in result:
+ validate_optional_json( "llvm-summary.json", result["llvm-summary.json"] )
+ return result
+
+
+def prepare_output_directory( output_dir: pathlib.Path ) -> pathlib.Path:
+ try:
+ if output_dir.is_symlink():
+ raise BaselineFetchError( "output directory cannot be a symbolic link" )
+ output_dir.mkdir( parents=True, exist_ok=True )
+ if output_dir.is_symlink() or not output_dir.is_dir():
+ raise BaselineFetchError( "output directory is not a safe directory" )
+ output_dir = output_dir.resolve( strict=True )
+ for filename in ( *EXPECTED_FILES, STATUS_FILE ):
+ path = output_dir / filename
+ if os.path.lexists( path ):
+ if path.is_dir() and not path.is_symlink():
+ raise BaselineFetchError(
+ f"cannot replace directory at output path {filename}"
+ )
+ path.unlink()
+ except BaselineFetchError:
+ raise
+ except OSError as error:
+ raise BaselineFetchError( f"cannot prepare output directory: {error}" ) from error
+ return output_dir
+
+
+def atomic_write( path: pathlib.Path, payload: bytes ) -> None:
+ temporary_path = None
+ try:
+ descriptor, temporary_name = tempfile.mkstemp(
+ prefix=f".{path.name}.", dir=path.parent
+ )
+ temporary_path = pathlib.Path( temporary_name )
+ with os.fdopen( descriptor, "wb" ) as stream:
+ stream.write( payload )
+ stream.flush()
+ os.fsync( stream.fileno() )
+ os.chmod( temporary_path, 0o644 )
+ os.replace( temporary_path, path )
+ temporary_path = None
+ except OSError as error:
+ raise BaselineFetchError( f"cannot write {path}: {error}" ) from error
+ finally:
+ if temporary_path is not None:
+ try:
+ temporary_path.unlink( missing_ok=True )
+ except OSError:
+ pass
+
+
+def write_status( output_dir: pathlib.Path, document: dict ) -> None:
+ payload = ( json.dumps( document, indent=2, sort_keys=True ) + "\n" ).encode(
+ "utf-8"
+ )
+ atomic_write( output_dir / STATUS_FILE, payload )
+
+
+def fetch_baseline(
+ client: GitHubClient,
+ repository: str,
+ workflow: str,
+ base_sha: str,
+ artifact_prefix: str,
+) -> tuple[dict[str, bytes], dict]:
+ repository_path = repository_api_path( repository )
+ workflow = validate_workflow( workflow )
+ base_sha = validate_sha( base_sha )
+ artifact_prefix = validate_artifact_prefix( artifact_prefix )
+ artifact_name = f"{artifact_prefix}{base_sha}"
+ if len( artifact_name ) > 255:
+ raise BaselineFetchError( "exact artifact name is too long" )
+
+ run = find_exact_run( client, repository_path, workflow, base_sha )
+ artifact = find_exact_artifact(
+ client, repository_path, run["id"], artifact_name
+ )
+ archive = client.download_artifact( repository_path, artifact["id"] )
+ files = read_safe_archive( archive, base_sha )
+ metadata = {
+ "artifact_id": artifact["id"],
+ "artifact_name": artifact_name,
+ "base_sha": base_sha,
+ "repository": repository,
+ "run_id": run["id"],
+ "schema_version": 1,
+ "status": "found",
+ "workflow": workflow,
+ }
+ return files, metadata
+
+
+def build_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(
+ description="Fetch the exact develop-base LLVM coverage artifact"
+ )
+ parser.add_argument( "--repository", required=True )
+ parser.add_argument( "--workflow", required=True )
+ parser.add_argument( "--base-sha", required=True )
+ parser.add_argument( "--artifact-name-prefix", required=True )
+ parser.add_argument( "--token-env", default="GITHUB_TOKEN" )
+ parser.add_argument( "--output-dir", required=True, type=pathlib.Path )
+ return parser
+
+
+def main( argv=None, environ=None, opener=None ) -> int:
+ args = build_parser().parse_args( argv )
+ environment = os.environ if environ is None else environ
+ output_dir = None
+ try:
+ validate_repository( args.repository )
+ validate_workflow( args.workflow )
+ base_sha = validate_sha( args.base_sha )
+ artifact_prefix = validate_artifact_prefix( args.artifact_name_prefix )
+ if not ENVIRONMENT_NAME_RE.fullmatch( args.token_env ):
+ raise BaselineFetchError( "token environment variable name is invalid" )
+ token = environment.get( args.token_env, "" )
+ if not isinstance( token, str ) or not TOKEN_RE.fullmatch( token ):
+ raise BaselineFetchError(
+ f"GitHub token environment variable {args.token_env} "
+ "is missing or empty, or contains unsafe characters"
+ )
+ output_dir = prepare_output_directory( args.output_dir )
+ client = GitHubClient( token, opener=opener )
+ files, status = fetch_baseline(
+ client,
+ args.repository,
+ args.workflow,
+ base_sha,
+ artifact_prefix,
+ )
+ for filename in sorted( files ):
+ atomic_write( output_dir / filename, files[filename] )
+ write_status( output_dir, status )
+ print(
+ f"Fetched {status['artifact_name']} from workflow run {status['run_id']}."
+ )
+ return 0
+ except BaselineMissing as error:
+ if output_dir is not None:
+ try:
+ write_status(
+ output_dir,
+ {
+ "artifact_name": f"{args.artifact_name_prefix}{base_sha}",
+ "base_sha": base_sha,
+ "reason": str( error ),
+ "reason_code": error.reason_code,
+ "repository": args.repository,
+ "schema_version": 1,
+ "status": "missing",
+ "workflow": args.workflow,
+ },
+ )
+ except BaselineFetchError as status_error:
+ print( f"coverage baseline error: {status_error}", file=sys.stderr )
+ return 2
+ print( f"coverage baseline unavailable: {error}", file=sys.stderr )
+ return 3
+ except BaselineFetchError as error:
+ print( f"coverage baseline error: {error}", file=sys.stderr )
+ return 2
+
+
+if __name__ == "__main__":
+ sys.exit( main() )
diff --git a/scripts/llvm_source_branch_coverage.sh b/scripts/llvm_source_branch_coverage.sh
new file mode 100755
index 00000000000..433ed62cfee
--- /dev/null
+++ b/scripts/llvm_source_branch_coverage.sh
@@ -0,0 +1,770 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+# Merge Clang source-coverage profiles and report two production branch metrics:
+#
+# canonical LLVM's CoverageReport summary for the compiled configuration.
+# native Every LCOV source-branch outcome emitted by llvm-cov. Template
+# and inline-function instantiations can appear more than once.
+#
+# All metrics include source files below src/coreComponents, excluding
+# test/support directory names listed in production_exclude_regex below.
+
+set -euo pipefail
+
+# Coverage mapping never needs distribution debug packages. Disabling the
+# host's optional debuginfod setting keeps reports local and reproducible.
+export DEBUGINFOD_URLS=
+
+usage()
+{
+ cat <<'EOF'
+Usage: scripts/llvm_source_branch_coverage.sh BUILD_DIR PROFILE_DIR [OUTPUT_DIR]
+
+BUILD_DIR A CPU shared-library build configured with
+ GEOS_ENABLE_LLVM_SOURCE_COVERAGE=ON.
+PROFILE_DIR Directory containing the *.profraw files from the test run.
+OUTPUT_DIR Report directory (default: BUILD_DIR/llvm-source-coverage).
+
+Environment overrides:
+ LLVM_COV llvm-cov executable (default: compiler-matched llvm-cov)
+ LLVM_PROFDATA llvm-profdata executable (default: compiler-matched llvm-profdata)
+ LLVM_READELF llvm-readelf executable (default: compiler-matched llvm-readelf)
+ GEOS_SOURCE_DIR
+ coreComponents source directory
+ (default: PROJECT_ROOT/src/coreComponents)
+
+CI provenance (required):
+ GEOS_CI_CONTAINER_IMAGE
+ GEOS_CI_CONTAINER_IMAGE_ID
+ GEOS_CI_CONTAINER_IMAGE_DIGESTS
+ GEOS_CI_LLVM_PACKAGE_VERSIONS
+ Container label, immutable image ID, and JSON array of
+ immutable repository digests plus the exact runtime-installed
+ LLVM package revisions supplied by the CI launcher.
+
+Typical profile collection:
+ mkdir -p BUILD_DIR/profiles
+ LLVM_PROFILE_FILE='BUILD_DIR/profiles/%p-%m.profraw' \
+ ctest --test-dir BUILD_DIR --parallel 12 --output-on-failure
+
+The script writes merged.profdata, llvm-summary.json, coverage-summary.json,
+native.info, export logs, and branch-summary.txt. The JSON and .info files make
+every numerator and denominator auditable with LLVM and LCOV tooling.
+coverage-summary.json is the stable contract consumed by CI.
+EOF
+}
+
+if (( $# < 2 || $# > 3 )); then
+ usage >&2
+ exit 2
+fi
+
+script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
+project_root="$(dirname -- "${script_dir}")"
+build_dir="$(cd -- "$1" && pwd -P)"
+profile_dir="$(cd -- "$2" && pwd -P)"
+output_arg="${3:-${build_dir}/llvm-source-coverage}"
+mkdir -p -- "${output_arg}"
+output_dir="$(cd -- "${output_arg}" && pwd -P)"
+source_dir="${GEOS_SOURCE_DIR:-${project_root}/src/coreComponents}"
+source_dir="$(cd -- "${source_dir}" && pwd -P)"
+# A reused report directory must never mix fresh summaries with stale evidence
+# from an earlier invocation that failed before exporting every format.
+rm -f -- \
+ "${output_dir}/branch-summary.txt" \
+ "${output_dir}/branch-summary.txt.tmp" \
+ "${output_dir}/coverage-objects.txt" \
+ "${output_dir}/coverage-summary.json" \
+ "${output_dir}/coverage-summary.json.tmp" \
+ "${output_dir}/definition-export.log" \
+ "${output_dir}/definition.info" \
+ "${output_dir}/llvm-summary.json" \
+ "${output_dir}/mapping-integrity.log" \
+ "${output_dir}/merged.profdata" \
+ "${output_dir}/native-export.log" \
+ "${output_dir}/native.info" \
+ "${output_dir}/profraw-inputs.txt" \
+ "${output_dir}/summary-export.log"
+
+cache_file="${build_dir}/CMakeCache.txt"
+if [[ ! -f "${cache_file}" ]]; then
+ echo "CMake cache not found: ${cache_file}" >&2
+ exit 1
+fi
+if ! grep -q '^GEOS_ENABLE_LLVM_SOURCE_COVERAGE:BOOL=ON$' "${cache_file}"; then
+ echo "Build does not have GEOS_ENABLE_LLVM_SOURCE_COVERAGE=ON" >&2
+ exit 1
+fi
+if ! grep -q '^ENABLE_COVERAGE:BOOL=OFF$' "${cache_file}"; then
+ echo "Build must have BLT ENABLE_COVERAGE=OFF" >&2
+ exit 1
+fi
+
+c_compiler="$(sed -n 's/^CMAKE_C_COMPILER:[^=]*=//p' "${cache_file}" | head -n 1)"
+cxx_compiler="$(sed -n 's/^CMAKE_CXX_COMPILER:[^=]*=//p' "${cache_file}" | head -n 1)"
+for compiler in "${c_compiler}" "${cxx_compiler}"; do
+ if [[ ! -x "${compiler}" ]]; then
+ echo "Compiler from CMake cache is not executable: ${compiler}" >&2
+ exit 1
+ fi
+done
+
+c_compiler_major="$("${c_compiler}" --version | sed -nE '1s/.*version[[:space:]]+([0-9]+).*/\1/p')"
+cxx_compiler_major="$("${cxx_compiler}" --version | sed -nE '1s/.*version[[:space:]]+([0-9]+).*/\1/p')"
+if [[ -z "${c_compiler_major}" || -z "${cxx_compiler_major}" ||
+ "${c_compiler_major}" != "${cxx_compiler_major}" ]]; then
+ echo "Clang C/C++ compiler major versions are missing or inconsistent." >&2
+ exit 1
+fi
+compiler_major="${cxx_compiler_major}"
+
+resolve_llvm_tool()
+{
+ local environment_name="$1"
+ local base_name="$2"
+ local override="${!environment_name:-}"
+
+ if [[ -n "${override}" ]]; then
+ command -v -- "${override}"
+ elif command -v -- "${base_name}-${compiler_major}" >/dev/null 2>&1; then
+ command -v -- "${base_name}-${compiler_major}"
+ else
+ command -v -- "${base_name}"
+ fi
+}
+
+llvm_cov="$(resolve_llvm_tool LLVM_COV llvm-cov || true)"
+llvm_profdata="$(resolve_llvm_tool LLVM_PROFDATA llvm-profdata || true)"
+llvm_readelf="$(resolve_llvm_tool LLVM_READELF llvm-readelf || true)"
+python="${PYTHON:-python3}"
+
+for tool in "${llvm_cov}" "${llvm_profdata}" "${llvm_readelf}" "${python}"; do
+ if ! command -v -- "${tool}" >/dev/null 2>&1; then
+ echo "Required tool not found: ${tool}" >&2
+ exit 2
+ fi
+done
+
+for tool in "${llvm_cov}" "${llvm_profdata}" "${llvm_readelf}"; do
+ tool_major="$("${tool}" --version | sed -nE '1s/.*version[[:space:]]+([0-9]+).*/\1/p')"
+ if [[ "${tool_major}" != "${compiler_major}" ]]; then
+ echo "LLVM tool/compiler major-version mismatch: ${tool} is ${tool_major}, compiler is ${compiler_major}" >&2
+ exit 2
+ fi
+done
+
+profile_list="${output_dir}/profraw-inputs.txt"
+find "${profile_dir}" -type f -name '*.profraw' -print | LC_ALL=C sort > "${profile_list}"
+if [[ ! -s "${profile_list}" ]]; then
+ echo "No .profraw files found below ${profile_dir}" >&2
+ exit 1
+fi
+
+profdata="${output_dir}/merged.profdata"
+"${llvm_profdata}" merge --input-files="${profile_list}" --output="${profdata}"
+
+# The denominator is the compiled CPU product: geosx and its component shared
+# libraries. Test executables are profile producers, not shipped artifacts;
+# including their private header instantiations would make the production
+# denominator depend on the shape of the test suite.
+declare -a candidate_objects=( "${build_dir}/bin/geosx" )
+if [[ ! -x "${candidate_objects[0]}" || ! -d "${build_dir}/lib" ]]; then
+ echo "geosx executable or build library directory not found below ${build_dir}" >&2
+ exit 1
+fi
+while IFS= read -r -d '' candidate; do
+ case "$(basename -- "${candidate}")" in
+ libgtest*|libtestingUtilities.*) continue ;;
+ esac
+ candidate_objects+=( "${candidate}" )
+done < <(
+ find "${build_dir}/lib" -maxdepth 1 -type f \
+ \( -name '*.so' -o -name '*.so.*' -o -name '*.dylib' -o -name '*.dll' \) \
+ -print0 | LC_ALL=C sort -z
+)
+
+object_list="${output_dir}/coverage-objects.txt"
+: > "${object_list}"
+for candidate in "${candidate_objects[@]}"; do
+ if "${llvm_readelf}" --sections "${candidate}" 2>/dev/null | grep '__llvm_covmap' >/dev/null; then
+ printf '%s\n' "${candidate}" >> "${object_list}"
+ fi
+done
+
+if [[ ! -s "${object_list}" ]]; then
+ echo "No objects with Clang coverage mappings found below ${build_dir}" >&2
+ exit 1
+fi
+if ! grep -Eq '/geosx(\.exe)?$' "${object_list}"; then
+ echo "Instrumented geosx executable not found below ${build_dir}" >&2
+ exit 1
+fi
+if ! grep -Eq '/libmainInterface\.(so(\..*)?|dylib|dll)$' "${object_list}"; then
+ echo "Instrumented mainInterface shared library not found below ${build_dir}" >&2
+ exit 1
+fi
+
+mapfile -t coverage_objects < "${object_list}"
+oldest_profile=""
+while IFS= read -r profile; do
+ if [[ -z "${oldest_profile}" || "${oldest_profile}" -nt "${profile}" ]]; then
+ oldest_profile="${profile}"
+ fi
+done < "${profile_list}"
+for object in "${coverage_objects[@]}"; do
+ if [[ "${object}" -nt "${oldest_profile}" ]]; then
+ echo "Coverage object is newer than at least one input profile: ${object}" >&2
+ exit 1
+ fi
+done
+
+primary_object="${coverage_objects[0]}"
+declare -a object_args=()
+for object in "${coverage_objects[@]:1}"; do
+ object_args+=( -object "${object}" )
+done
+
+production_exclude_regex='/(unitTests|unitTestUtilities|integrationTests|tests|examples|benchmarks|testingUtilities)/'
+production_exclude_regex+='|/codingUtilities/UnitTestUtilities[.]hpp$'
+
+native_info="${output_dir}/native.info"
+llvm_summary="${output_dir}/llvm-summary.json"
+common_export_args=(
+ -instr-profile "${profdata}"
+ --sources "${source_dir}"
+ -ignore-filename-regex "${production_exclude_regex}"
+ -num-threads=1
+)
+
+integrity_log="${output_dir}/mapping-integrity.log"
+if ! "${llvm_cov}" report "${primary_object}" "${object_args[@]}" \
+ "${common_export_args[@]}" -dump > /dev/null 2> "${integrity_log}"; then
+ cat "${integrity_log}" >&2
+ exit 1
+fi
+
+mismatch_count="$(awk \
+ '/warning: [0-9]+ functions? have mismatched data/ { total += $2 } END { print total + 0 }' \
+ "${integrity_log}")"
+mismatch_details="$(grep -c '^hash-mismatch:' "${integrity_log}" || true)"
+nonzero_mismatches="$(grep '^hash-mismatch:' "${integrity_log}" | grep -vc 'hash = 0x0$' || true)"
+if (( mismatch_count != mismatch_details || nonzero_mismatches != 0 )); then
+ cat "${integrity_log}" >&2
+ echo "Refusing to report coverage after a nonzero or unaudited function-hash mismatch." >&2
+ exit 1
+fi
+
+integrity_warning_regex='hash mismatch|profile data.*out of date|object.*newer than profile'
+integrity_warning_regex+='|no coverage data found|failed to load coverage'
+if grep -Eiq "${integrity_warning_regex}" "${integrity_log}"; then
+ cat "${integrity_log}" >&2
+ echo "Refusing to report coverage after an LLVM profile-integrity warning." >&2
+ exit 1
+fi
+
+run_export()
+{
+ local output_file="$1"
+ local log_file="$2"
+ shift 2
+
+ if ! "${llvm_cov}" export "${primary_object}" "${object_args[@]}" \
+ "${common_export_args[@]}" "$@" > "${output_file}" 2> "${log_file}"; then
+ cat "${log_file}" >&2
+ return 1
+ fi
+
+ local export_mismatch_count
+ export_mismatch_count="$(awk \
+ '/warning: [0-9]+ functions? have mismatched data/ { total += $2 } END { print total + 0 }' \
+ "${log_file}")"
+ if (( export_mismatch_count != mismatch_count )); then
+ cat "${log_file}" >&2
+ echo "LLVM export mismatch count differs from the audited mapping count." >&2
+ return 1
+ fi
+
+ if grep -Eiq "${integrity_warning_regex}" "${log_file}"; then
+ cat "${log_file}" >&2
+ echo "Refusing to report coverage after an LLVM profile-integrity warning." >&2
+ return 1
+ fi
+}
+
+native_instantiation_args=()
+llvm_cov_export_help="$("${llvm_cov}" export --help 2>&1)"
+if grep -q -- '--unify-instantiations' <<< "${llvm_cov_export_help}"; then
+ native_instantiation_args=( -unify-instantiations=false )
+fi
+run_export "${native_info}" "${output_dir}/native-export.log" \
+ -format=lcov "${native_instantiation_args[@]}"
+run_export "${llvm_summary}" "${output_dir}/summary-export.log" \
+ -format=text -summary-only
+
+summarize_branches()
+{
+ # Count the BRDA outcome records directly. This deliberately does not trust
+ # BRF/BRH summaries because older llvm-cov versions can summarize template
+ # instances differently from the records emitted in the same LCOV file.
+ awk -F, '
+ /^BRDA:/ {
+ total += 1
+ if ($4 != "-" && ($4 + 0) > 0) {
+ covered += 1
+ }
+ }
+ END {
+ percent = total ? 100.0 * covered / total : 0.0
+ printf "%d %d %.6f\n", covered, total, percent
+ }
+ ' "$1"
+}
+
+read -r native_covered native_total native_percent < <(summarize_branches "${native_info}")
+read -r canonical_covered canonical_total canonical_percent < <(
+ "${python}" - "${llvm_summary}" <<'PY'
+import json
+import sys
+
+with open( sys.argv[1], encoding="utf-8" ) as stream:
+ document = json.load( stream )
+
+branches = document["data"][0]["totals"]["branches"]
+covered = int( branches["covered"] )
+total = int( branches["count"] )
+percent = 100.0 * covered / total if total else 0.0
+print( covered, total, f"{percent:.6f}" )
+PY
+)
+profile_count="$(wc -l < "${profile_list}")"
+object_count="${#coverage_objects[@]}"
+zero_hash_mappings="${mismatch_details}"
+
+if (( canonical_total == 0 || native_total == 0 )); then
+ echo "LLVM reported a zero production branch denominator." >&2
+ exit 1
+fi
+
+summary="${output_dir}/branch-summary.txt"
+summary_tmp="${summary}.tmp"
+{
+ printf 'scope: %s\n' "${source_dir}"
+ printf 'excluded directory regex: %s\n' "${production_exclude_regex}"
+ printf 'profiles: %d\n' "${profile_count}"
+ printf 'coverage objects: %d\n' "${object_count}"
+ printf 'zero-hash mappings without profile counters: %d\n' "${zero_hash_mappings}"
+ printf 'canonical LLVM branches: %d/%d (%.6f%%)\n' \
+ "${canonical_covered}" "${canonical_total}" "${canonical_percent}"
+ printf 'native LCOV branch outcomes: %d/%d (%.6f%%)\n' \
+ "${native_covered}" "${native_total}" "${native_percent}"
+} | tee "${summary_tmp}"
+mv -- "${summary_tmp}" "${summary}"
+
+coverage_summary="${output_dir}/coverage-summary.json"
+coverage_summary_tmp="${coverage_summary}.tmp"
+"${python}" - "${llvm_summary}" "${coverage_summary_tmp}" \
+ "${project_root}" "${source_dir}" "${production_exclude_regex}" \
+ "${compiler_major}" "${profile_count}" "${object_count}" \
+ "${zero_hash_mappings}" \
+ "${native_covered}" "${native_total}" \
+ "${cache_file}" "${c_compiler}" "${cxx_compiler}" "${llvm_cov}" <<'PY'
+import hashlib
+import json
+import os
+import re
+import subprocess
+import sys
+
+(
+ llvm_summary_path,
+ output_path,
+ project_root,
+ source_dir,
+ excluded_regex,
+ compiler_major,
+ profile_count,
+ object_count,
+ zero_hash_mappings,
+ native_covered,
+ native_total,
+ cache_file,
+ c_compiler,
+ cxx_compiler,
+ llvm_cov,
+) = sys.argv[1:]
+
+COVERAGE_CONTRACT_ID = "geos-llvm-source-coverage-v1"
+METRIC_SEMANTICS = {
+ "canonical_metrics": "llvm-cov-summary-instantiation-groups-v1",
+ "native_branch_outcomes": "llvm-cov-lcov-emitted-brda-records-v1",
+ "source_selection": "llvm-cov-sources-and-ignore-regex-v1",
+}
+OBJECT_SELECTION = {
+ "contract_version": 1,
+ "primary_object": "bin/geosx",
+ "additional_object_directory": "lib",
+ "additional_object_globs": [ "*.dll", "*.dylib", "*.so", "*.so.*" ],
+ "excluded_library_basenames": [ "libgtest*", "libtestingUtilities.*" ],
+ "required_object_basenames": [ "geosx", "libmainInterface" ],
+ "coverage_mapping_section": "__llvm_covmap",
+ "test_executables": "excluded",
+}
+
+
+def command_output( description, command ):
+ try:
+ result = subprocess.run(
+ command,
+ check=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ )
+ except ( OSError, subprocess.CalledProcessError ) as error:
+ output = getattr( error, "stdout", "" ) or ""
+ raise ValueError(
+ f"cannot determine {description}: {output.strip() or error}"
+ ) from error
+ output = result.stdout.rstrip( "\r\n" )
+ if not output:
+ raise ValueError( f"cannot determine {description}: empty output" )
+ return output
+
+
+def normalize_cmake_bool( value ):
+ value = value.strip()
+ upper = value.upper()
+ if upper in ( "", "0", "FALSE", "IGNORE", "N", "NO", "NOTFOUND", "OFF" ):
+ return False
+ if upper.endswith( "-NOTFOUND" ):
+ return False
+ return True
+
+
+def normalized_build_config( path ):
+ entries = {}
+ cache_entry = re.compile( r"^([^#/:=]+):([^=]+)=(.*)$" )
+ with open( path, encoding="utf-8" ) as stream:
+ for raw_line in stream:
+ match = cache_entry.match( raw_line.rstrip( "\r\n" ) )
+ if match is None:
+ continue
+ name, entry_type, value = match.groups()
+ if entry_type == "INTERNAL":
+ continue
+ if name in entries:
+ raise ValueError( f"duplicate CMake cache entry: {name}" )
+ entries[name] = ( entry_type, value.strip() )
+
+ if "CMAKE_BUILD_TYPE" not in entries:
+ raise ValueError( "CMake cache does not define CMAKE_BUILD_TYPE" )
+ build_type = entries["CMAKE_BUILD_TYPE"][1]
+ build_type_suffix = re.sub( r"[^A-Za-z0-9]", "_", build_type ).upper()
+ active_flag_names = {
+ "CMAKE_C_FLAGS",
+ "CMAKE_CXX_FLAGS",
+ "CMAKE_EXE_LINKER_FLAGS",
+ "CMAKE_MODULE_LINKER_FLAGS",
+ "CMAKE_SHARED_LINKER_FLAGS",
+ f"CMAKE_C_FLAGS_{build_type_suffix}",
+ f"CMAKE_CXX_FLAGS_{build_type_suffix}",
+ f"CMAKE_EXE_LINKER_FLAGS_{build_type_suffix}",
+ f"CMAKE_MODULE_LINKER_FLAGS_{build_type_suffix}",
+ f"CMAKE_SHARED_LINKER_FLAGS_{build_type_suffix}",
+ }
+ fixed_names = {
+ "BLT_CXX_STD",
+ "BUILD_SHARED_LIBS",
+ "CMAKE_BUILD_TYPE",
+ "CMAKE_CUDA_ARCHITECTURES",
+ "CMAKE_C_EXTENSIONS",
+ "CMAKE_C_STANDARD",
+ "CMAKE_C_STANDARD_REQUIRED",
+ "CMAKE_CXX_EXTENSIONS",
+ "CMAKE_CXX_STANDARD",
+ "CMAKE_CXX_STANDARD_REQUIRED",
+ "CMAKE_INTERPROCEDURAL_OPTIMIZATION",
+ "CMAKE_POSITION_INDEPENDENT_CODE",
+ "CMAKE_UNITY_BUILD",
+ "GEOS_GLOBALINDEX_TYPE",
+ "GEOS_GLOBALINDEX_TYPE_FLAG",
+ "GEOS_LA_INTERFACE",
+ "GEOS_LA_INTERFACE_HYPRE",
+ "GEOS_LOCALINDEX_TYPE",
+ "GEOS_LOCALINDEX_TYPE_FLAG",
+ }
+
+ config = {}
+ for name, ( entry_type, value ) in entries.items():
+ relevant = (
+ name in fixed_names
+ or name in active_flag_names
+ or name.startswith( "ENABLE_" )
+ or name.startswith( "GEOS_BUILD_" )
+ or name.startswith( "GEOS_ENABLE_" )
+ or name.startswith( "LVARRAY_" )
+ or name.startswith( "RAJA_ENABLE_" )
+ )
+ if not relevant:
+ continue
+ config[name] = normalize_cmake_bool( value ) if entry_type == "BOOL" else value
+
+ required = {
+ "BLT_CXX_STD",
+ "BUILD_SHARED_LIBS",
+ "CMAKE_BUILD_TYPE",
+ "ENABLE_COVERAGE",
+ "ENABLE_CUDA",
+ "ENABLE_HIP",
+ "ENABLE_HYPRE",
+ "ENABLE_MPI",
+ "ENABLE_OPENMP",
+ "ENABLE_TRILINOS",
+ "GEOS_BUILD_SHARED_LIBS",
+ "GEOS_ENABLE_BOUNDS_CHECK",
+ "GEOS_ENABLE_LLVM_SOURCE_COVERAGE",
+ "GEOS_GLOBALINDEX_TYPE",
+ "GEOS_LA_INTERFACE",
+ "GEOS_LOCALINDEX_TYPE",
+ "LVARRAY_BOUNDS_CHECK",
+ "RAJA_ENABLE_CUDA",
+ "RAJA_ENABLE_HIP",
+ "RAJA_ENABLE_OPENMP",
+ }
+ missing = sorted( required - config.keys() )
+ if missing:
+ raise ValueError(
+ "CMake coverage configuration is incomplete: " + ", ".join( missing )
+ )
+ if config["ENABLE_COVERAGE"] is not False:
+ raise ValueError( "ENABLE_COVERAGE must be OFF in normalized build config" )
+ if config["GEOS_ENABLE_LLVM_SOURCE_COVERAGE"] is not True:
+ raise ValueError(
+ "GEOS_ENABLE_LLVM_SOURCE_COVERAGE must be ON in normalized build config"
+ )
+ if config["BUILD_SHARED_LIBS"] is not True or \
+ config["GEOS_BUILD_SHARED_LIBS"] is not True:
+ raise ValueError( "coverage object selection requires shared libraries" )
+ return dict( sorted( config.items() ) )
+
+
+def container_provenance():
+ environment_names = (
+ "GEOS_CI_CONTAINER_IMAGE",
+ "GEOS_CI_CONTAINER_IMAGE_ID",
+ "GEOS_CI_CONTAINER_IMAGE_DIGESTS",
+ )
+ missing = [ name for name in environment_names if not os.environ.get( name ) ]
+ if missing:
+ raise ValueError(
+ "missing CI container provenance: " + ", ".join( missing )
+ )
+
+ image = os.environ["GEOS_CI_CONTAINER_IMAGE"]
+ image_id = os.environ["GEOS_CI_CONTAINER_IMAGE_ID"]
+ try:
+ image_digests = json.loads( os.environ["GEOS_CI_CONTAINER_IMAGE_DIGESTS"] )
+ except json.JSONDecodeError as error:
+ raise ValueError( "GEOS_CI_CONTAINER_IMAGE_DIGESTS is not valid JSON" ) from error
+ if (
+ not isinstance( image_digests, list )
+ or not image_digests
+ or any( not isinstance( digest, str ) or not digest for digest in image_digests )
+ ):
+ raise ValueError(
+ "GEOS_CI_CONTAINER_IMAGE_DIGESTS must be a nonempty JSON string array"
+ )
+ image_digests = sorted( set( image_digests ) )
+ if re.fullmatch( r"sha256:[0-9a-f]{64}", image_id ) is None:
+ raise ValueError( "GEOS_CI_CONTAINER_IMAGE_ID must be an immutable SHA256 ID" )
+ for digest in image_digests:
+ if re.fullmatch( r"[^\s@]+@sha256:[0-9a-f]{64}", digest ) is None:
+ raise ValueError( "container repository digests must be immutable SHA256 digests" )
+ if any( character in image for character in "\r\n\0" ):
+ raise ValueError( "GEOS_CI_CONTAINER_IMAGE contains a control character" )
+ return {
+ "image": image,
+ "image_id": image_id,
+ "image_digests": image_digests,
+ }
+
+
+git_command = [
+ "git",
+ "-c",
+ f"safe.directory={project_root}",
+ "-C",
+ project_root,
+ "rev-parse",
+ "--verify",
+]
+commit_sha = command_output( "Git HEAD commit", git_command + [ "HEAD^{commit}" ] )
+tree_sha = command_output( "Git HEAD tree", git_command + [ "HEAD^{tree}" ] )
+git_object_id = re.compile( r"(?:[0-9a-f]{40}|[0-9a-f]{64})" )
+if git_object_id.fullmatch( commit_sha ) is None or \
+ git_object_id.fullmatch( tree_sha ) is None:
+ raise ValueError( "Git commit/tree provenance is not a canonical object ID" )
+
+c_compiler_version = command_output( "C compiler version", [ c_compiler, "--version" ] )
+cxx_compiler_version = command_output(
+ "C++ compiler version", [ cxx_compiler, "--version" ]
+)
+llvm_cov_version = command_output( "llvm-cov version", [ llvm_cov, "--version" ] )
+c_compiler_target = command_output( "C compiler target", [ c_compiler, "-dumpmachine" ] )
+cxx_compiler_target = command_output(
+ "C++ compiler target", [ cxx_compiler, "-dumpmachine" ]
+)
+if c_compiler_target != cxx_compiler_target:
+ raise ValueError( "C and C++ compiler targets differ" )
+
+container = container_provenance()
+toolchain = {
+ "c_compiler_version": c_compiler_version,
+ "cxx_compiler_version": cxx_compiler_version,
+ "llvm_cov_version": llvm_cov_version,
+ "compiler_target": cxx_compiler_target,
+}
+package_versions = os.environ.get( "GEOS_CI_LLVM_PACKAGE_VERSIONS", "" ).splitlines()
+if (
+ not package_versions
+ or package_versions != sorted( set( package_versions ) )
+ or any( re.fullmatch( r"[A-Za-z0-9.+-]+=[^\s=]+", entry ) is None
+ for entry in package_versions )
+):
+ raise ValueError(
+ "GEOS_CI_LLVM_PACKAGE_VERSIONS must be a sorted package=version list"
+ )
+toolchain["llvm_package_versions"] = package_versions
+build_config = normalized_build_config( cache_file )
+
+with open( llvm_summary_path, encoding="utf-8" ) as stream:
+ llvm_document = json.load( stream )
+
+totals = llvm_document["data"][0]["totals"]
+
+def metric( covered, total ):
+ covered = int( covered )
+ total = int( total )
+ if covered < 0 or total < 0 or covered > total:
+ raise ValueError( "LLVM emitted invalid coverage counts" )
+ return {
+ "covered": covered,
+ "total": total,
+ "not_covered": total - covered,
+ "percent": round( 100.0 * covered / total if total else 0.0, 6 ),
+ }
+
+canonical = {}
+for name in ( "regions", "functions", "lines", "branches" ):
+ canonical[name] = metric( totals[name]["covered"], totals[name]["count"] )
+
+source_root = os.path.realpath( source_dir )
+branch_gaps = []
+per_file_metrics = []
+file_aggregates = {
+ name: { "covered": 0, "total": 0 }
+ for name in ( "regions", "functions", "lines", "branches" )
+}
+seen_paths = set()
+for source in llvm_document["data"][0]["files"]:
+ raw_filename = source["filename"]
+ if not os.path.isabs( raw_filename ):
+ raw_filename = os.path.join( project_root, raw_filename )
+ filename = os.path.realpath( raw_filename )
+ try:
+ in_scope = os.path.commonpath( ( source_root, filename ) ) == source_root
+ except ValueError:
+ in_scope = False
+ if not in_scope:
+ continue
+ relative_path = os.path.relpath( filename, project_root ).replace( os.sep, "/" )
+ if relative_path in seen_paths:
+ raise ValueError( f"duplicate LLVM per-file coverage path: {relative_path}" )
+ seen_paths.add( relative_path )
+ file_metrics = {}
+ for name in ( "regions", "functions", "lines", "branches" ):
+ llvm_metric = source["summary"][name]
+ file_metrics[name] = metric( llvm_metric["covered"], llvm_metric["count"] )
+ file_aggregates[name]["covered"] += file_metrics[name]["covered"]
+ file_aggregates[name]["total"] += file_metrics[name]["total"]
+ per_file_metrics.append( { "path": relative_path, "metrics": file_metrics } )
+
+ branch_metric = file_metrics["branches"]
+ if branch_metric["not_covered"] == 0:
+ continue
+ branch_gaps.append(
+ {
+ "path": relative_path,
+ **branch_metric,
+ }
+ )
+per_file_metrics.sort( key=lambda source: source["path"] )
+branch_gaps.sort( key=lambda gap: ( -gap["not_covered"], gap["path"] ) )
+for name, aggregate in file_aggregates.items():
+ if ( aggregate["covered"], aggregate["total"] ) != (
+ canonical[name]["covered"], canonical[name]["total"]
+ ):
+ raise ValueError( f"per-file {name} counts do not match canonical totals" )
+
+scope = os.path.relpath( source_dir, project_root ).replace( os.sep, "/" )
+contract_payload = {
+ "summary_schema_version": 3,
+ "contract_id": COVERAGE_CONTRACT_ID,
+ "scope": scope,
+ "excluded_regex": excluded_regex,
+ "metric_semantics": METRIC_SEMANTICS,
+ "container": {
+ "image_id": container["image_id"],
+ "image_digests": container["image_digests"],
+ },
+ "toolchain": toolchain,
+ "build_config": build_config,
+ "object_selection": OBJECT_SELECTION,
+}
+contract_bytes = json.dumps(
+ contract_payload,
+ allow_nan=False,
+ ensure_ascii=True,
+ separators=( ",", ":" ),
+ sort_keys=True,
+).encode( "utf-8" )
+contract_fingerprint = hashlib.sha256( contract_bytes ).hexdigest()
+
+document = {
+ "schema_version": 3,
+ "scope": scope,
+ "excluded_regex": excluded_regex,
+ "measurement": {
+ "commit_sha": commit_sha,
+ "tree_sha": tree_sha,
+ "contract_id": COVERAGE_CONTRACT_ID,
+ "contract_fingerprint": contract_fingerprint,
+ "container": container,
+ "toolchain": toolchain,
+ "build_config": build_config,
+ "metric_semantics": METRIC_SEMANTICS,
+ "object_selection": OBJECT_SELECTION,
+ },
+ "tool": {
+ "name": "llvm-cov",
+ "major": int( compiler_major ),
+ },
+ "inputs": {
+ "profiles": int( profile_count ),
+ "coverage_objects": int( object_count ),
+ "zero_hash_mappings": int( zero_hash_mappings ),
+ },
+ "metrics": canonical,
+ "per_file_metrics": per_file_metrics,
+ "top_branch_gaps": branch_gaps[:5],
+ "supplemental": {
+ "native_branch_outcomes": metric( native_covered, native_total ),
+ },
+}
+
+with open( output_path, "w", encoding="utf-8" ) as stream:
+ json.dump( document, stream, indent=2, sort_keys=True )
+ stream.write( "\n" )
+PY
+mv -- "${coverage_summary_tmp}" "${coverage_summary}"
diff --git a/scripts/render_coverage_report_html.py b/scripts/render_coverage_report_html.py
new file mode 100644
index 00000000000..3d418ca1f6a
--- /dev/null
+++ b/scripts/render_coverage_report_html.py
@@ -0,0 +1,644 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-only
+
+"""Render the LLVM source-coverage artifact as one self-contained HTML page."""
+
+from __future__ import annotations
+
+import argparse
+import html
+import json
+import os
+import pathlib
+import re
+import sys
+import tempfile
+
+
+SCRIPT_DIRECTORY = pathlib.Path( __file__ ).resolve().parent
+if str( SCRIPT_DIRECTORY ) not in sys.path:
+ sys.path.insert( 0, str( SCRIPT_DIRECTORY ) )
+import check_coverage_thresholds as coverage_contract
+
+
+METRIC_NAMES = ( "lines", "functions", "branches", "regions" )
+METRIC_LABELS = {
+ "branches": "Canonical branches",
+ "functions": "Functions",
+ "lines": "Lines",
+ "regions": "Regions",
+}
+PATCH_METRIC_LABELS = {
+ "executable_lines": "Executable changed lines",
+ "function_start_sites": "Changed function start sites",
+ "native_branch_outcomes": "Native changed branch outcomes",
+}
+DETAIL_FILES = (
+ ( "CTest output", "ctest.log" ),
+ ( "LLVM mapping-integrity log", "mapping-integrity.log" ),
+ ( "Native export log", "native-export.log" ),
+ ( "Summary export log", "summary-export.log" ),
+ ( "Branch summary", "branch-summary.txt" ),
+ ( "Coverage objects", "coverage-objects.txt" ),
+ ( "Toolchain", "toolchain.txt" ),
+ ( "CMake configuration", "CMakeCache.txt" ),
+ ( "Coverage status source", "coverage-status.md" ),
+ ( "Coverage policy source", "coverage-summary.md" ),
+ ( "Pull-request coverage source", "pr-coverage.md" ),
+ ( "Baseline status", "coverage-baseline-status.json" ),
+ ( "Coverage summary JSON", "coverage-summary.json" ),
+ ( "Pull-request coverage JSON", "pr-coverage.json" ),
+ ( "LLVM summary JSON", "llvm-summary.json" ),
+)
+
+
+def escape( value: object ) -> str:
+ return html.escape( str( value ), quote=True )
+
+
+def read_text( path: pathlib.Path ) -> str | None:
+ if not path.is_file() or path.is_symlink():
+ return None
+ try:
+ return path.read_text( encoding="utf-8", errors="replace" )
+ except OSError:
+ return None
+
+
+def read_json( path: pathlib.Path ) -> dict | None:
+ text = read_text( path )
+ if text is None:
+ return None
+ try:
+ document = json.loads( text )
+ except json.JSONDecodeError:
+ return None
+ return document if isinstance( document, dict ) else None
+
+
+def write_atomic( path: pathlib.Path, contents: str ) -> None:
+ """Replace a report even when a container created the old file as root."""
+ path.parent.mkdir( parents=True, exist_ok=True )
+ if path.is_symlink():
+ raise ValueError( f"refusing to replace symbolic link: {path}" )
+ descriptor, temporary_name = tempfile.mkstemp(
+ prefix=f".{path.name}.", dir=path.parent
+ )
+ temporary_path = pathlib.Path( temporary_name )
+ try:
+ os.fchmod( descriptor, 0o644 )
+ with os.fdopen( descriptor, "w", encoding="utf-8" ) as stream:
+ stream.write( contents )
+ stream.flush()
+ os.fsync( stream.fileno() )
+ os.replace( temporary_path, path )
+ temporary_path = None
+ except BaseException:
+ temporary_path.unlink( missing_ok=True )
+ raise
+
+
+def validated_summary( document: dict | None ) -> dict | None:
+ if document is None:
+ return None
+ try:
+ return coverage_contract.validate_summary( document )
+ except ValueError:
+ return document
+
+
+def validated_policy( document: dict | None, scope: str | None ) -> dict:
+ if document is None or scope is None:
+ return {}
+ try:
+ return coverage_contract.validate_policy( document, scope )
+ except ValueError:
+ raw_thresholds = document.get( "minimum_basis_points", {} )
+ if not isinstance( raw_thresholds, dict ):
+ return {}
+ return {
+ name: value
+ for name, value in raw_thresholds.items()
+ if name in METRIC_NAMES and isinstance( value, int )
+ }
+
+
+def number( value: object ) -> str:
+ if isinstance( value, int ) and not isinstance( value, bool ):
+ return f"{value:,}"
+ return escape( value )
+
+
+def metric_percent( metric: dict | None ) -> str:
+ if not isinstance( metric, dict ):
+ return "N/A"
+ total = metric.get( "total", 0 )
+ covered = metric.get( "covered", 0 )
+ if not isinstance( total, int ) or total == 0:
+ return "N/A"
+ return f"{100.0 * covered / total:.2f}%"
+
+
+def metric_counts( metric: dict | None ) -> str:
+ if not isinstance( metric, dict ):
+ return "N/A"
+ return f"{number( metric.get( 'covered', 0 ) )}/{number( metric.get( 'total', 0 ) )}"
+
+
+def policy_result( metric: dict | None, threshold: int | None ) -> tuple[str, str]:
+ if not isinstance( metric, dict ) or threshold is None:
+ return "info", "Measured"
+ total = metric.get( "total", 0 )
+ covered = metric.get( "covered", 0 )
+ if not isinstance( total, int ) or not isinstance( covered, int ) or total == 0:
+ return "info", "N/A"
+ passed = covered * 10000 >= threshold * total
+ return ( "pass", "Pass" ) if passed else ( "fail", "Fail" )
+
+
+def badge( state: str, label: str ) -> str:
+ icons = { "pass": "✓", "fail": "✕", "warn": "!", "info": "i" }
+ return (
+ f''
+ f'{icons.get( state, "i" )} '
+ f"{escape( label )} "
+ )
+
+
+def table( headers: list[str], rows: list[list[str]], class_name="" ) -> str:
+ header_html = "".join( f"
{header} " for header in headers )
+ body = []
+ for row in rows:
+ body.append( "" + "".join( f"{cell} " for cell in row ) + " " )
+ return (
+ f''
+ f"{header_html} {''.join( body )} "
+ "
"
+ )
+
+
+def card( title: str, result: str, detail: str, state: str ) -> str:
+ return (
+ f''
+ f"{escape( title )}
"
+ f'{badge( state, result )}
'
+ f'{detail}
'
+ " "
+ )
+
+
+def markdown_table_value( document: str, label: str ) -> tuple[str, str] | None:
+ for line in document.splitlines():
+ cells = [ cell.strip() for cell in line.strip().strip( "|" ).split( "|" ) ]
+ if len( cells ) >= 3 and cells[0] == label:
+ return cells[1], cells[2]
+ return None
+
+
+def status_details( status_markdown: str | None ) -> dict:
+ status = "info"
+ result = "Unavailable"
+ if status_markdown:
+ if re.search( r"^### ✅ LLVM source coverage passed", status_markdown, re.MULTILINE ):
+ status, result = "pass", "Passed"
+ elif re.search( r"^### ❌ LLVM source coverage failed", status_markdown, re.MULTILINE ):
+ status, result = "fail", "Failed"
+ tests = markdown_table_value( status_markdown or "", "Coverage smoke suite" )
+ if tests is None:
+ tests = markdown_table_value( status_markdown or "", "Coverage smoke CTest" )
+ configuration = {}
+ for label in ( "Runner", "Container", "Immutable image ID (short)", "Toolchain", "Build", "Test scheduling" ):
+ value = markdown_table_value( status_markdown or "", label )
+ if value is not None:
+ configuration[label] = value[1].strip( "`" )
+ return {
+ "state": status,
+ "result": result,
+ "tests": tests,
+ "configuration": configuration,
+ }
+
+
+def metric_rows( summary: dict | None, thresholds: dict ) -> list[list[str]]:
+ if summary is None:
+ return []
+ rows = []
+ metrics = summary.get( "metrics", {} )
+ for name in METRIC_NAMES:
+ metric = metrics.get( name )
+ threshold = thresholds.get( name )
+ state, result = policy_result( metric, threshold )
+ requirement = "—" if threshold is None else f"≥ {threshold / 100:.2f}%"
+ percent = metric_percent( metric )
+ width = 0.0
+ if isinstance( metric, dict ) and isinstance( metric.get( "percent" ), ( int, float ) ):
+ width = max( 0.0, min( 100.0, float( metric["percent"] ) ) )
+ progress = (
+ f'
'
+ )
+ rows.append(
+ [
+ escape( METRIC_LABELS[name] ),
+ f"{escape( percent )} {progress}",
+ number( metric.get( "covered", 0 ) ) if isinstance( metric, dict ) else "N/A",
+ number( metric.get( "not_covered", 0 ) ) if isinstance( metric, dict ) else "N/A",
+ number( metric.get( "total", 0 ) ) if isinstance( metric, dict ) else "N/A",
+ escape( requirement ),
+ badge( state, result ),
+ ]
+ )
+ return rows
+
+
+def policy_card( summary: dict | None, thresholds: dict ) -> tuple[str, str, str]:
+ if summary is None:
+ return "info", "Unavailable", "No validated coverage summary was produced."
+ results = [ policy_result( summary.get( "metrics", {} ).get( name ), threshold )[0]
+ for name, threshold in thresholds.items() ]
+ if not results:
+ return "info", "Measured", "No repository thresholds are configured."
+ if "fail" in results:
+ return "fail", "Failed", "At least one enforced coverage floor is below the required value."
+ if "info" in results:
+ return "info", "Unavailable", "One or more configured coverage metrics are unavailable."
+ return "pass", "Passed", "All configured lines, functions, and branch thresholds are satisfied."
+
+
+def baseline_card( report: dict | None ) -> tuple[str, str, str]:
+ if report is None:
+ return "info", "Not applicable", "This artifact was not produced for a pull request."
+ aggregate = report.get( "aggregate_comparison", {} )
+ if not aggregate.get( "comparable", False ):
+ return "info", "Unavailable", aggregate.get(
+ "reason", "No exact-base coverage summary was available."
+ )
+ deltas = [
+ comparison.get( "percentage_point_delta", 0.0 )
+ for comparison in aggregate.get( "metrics", {} ).values()
+ ]
+ if deltas and all( delta >= 0 for delta in deltas ):
+ return "pass", "Maintained / improved", "Every comparable repository metric stayed level or increased."
+ if deltas and all( delta <= 0 for delta in deltas ):
+ return "warn", "Decreased", "At least one comparable repository metric decreased."
+ if not deltas:
+ return "info", "Unavailable", "No comparable repository metrics were provided."
+ return "warn", "Mixed", "Some comparable repository metrics increased while others decreased."
+
+
+def changed_lines_card( report: dict | None ) -> tuple[str, str, str]:
+ if report is None:
+ return "info", "Not applicable", "This artifact was not produced for a pull request."
+ metric = report.get( "patch", {} ).get( "metrics", {} ).get( "executable_lines", {} )
+ total = metric.get( "total", 0 )
+ missed = metric.get( "not_covered", 0 )
+ if total == 0:
+ return "info", "No executable changes", "The production diff contains no executable changed lines."
+ if missed:
+ return "warn", "Review needed", f"{number( metric.get( 'covered', 0 ) )}/{number( total )} executable changed lines are covered."
+ return "pass", "Covered", f"All {number( total )} executable changed lines are covered."
+
+
+def coverage_section( summary: dict | None, thresholds: dict ) -> str:
+ if summary is None:
+ return 'Repository coverage Coverage metrics are unavailable.
'
+ rows = metric_rows( summary, thresholds )
+ scope = escape( summary.get( "scope", "unknown" ) )
+ reporter = f"{summary.get( 'tool', {} ).get( 'name', 'unknown' )} {summary.get( 'tool', {} ).get( 'major', '?' )}"
+ result = (
+ f'Repository coverage '
+ f'Production scope {scope} · Reporter {escape( reporter )}. '
+ "Thresholds are evaluated using exact covered and total counts.
"
+ + table(
+ [ "Metric", "Coverage", "Covered", "Missed", "Total", "Requirement", "Result" ],
+ rows,
+ "metrics",
+ )
+ )
+ gaps = summary.get( "top_branch_gaps", [] )
+ if gaps:
+ gap_rows = []
+ scope_prefix = summary.get( "scope", "" ).rstrip( "/" ) + "/"
+ for gap in gaps:
+ path = gap.get( "path", "" )
+ if path.startswith( scope_prefix ):
+ path = path[len( scope_prefix ):]
+ gap_rows.append(
+ [
+ f"{escape( path )}",
+ number( gap.get( "covered", 0 ) ),
+ number( gap.get( "not_covered", 0 ) ),
+ number( gap.get( "total", 0 ) ),
+ escape( metric_percent( gap ) ),
+ ]
+ )
+ result += (
+ "Largest branch gaps "
+ + table( [ "Source file", "Covered", "Missed", "Total", "Coverage" ], gap_rows )
+ + 'Ranked by missed canonical branch outcomes within the production scope.
'
+ )
+ supplemental = summary.get( "supplemental", {} ).get( "native_branch_outcomes" )
+ inputs = summary.get( "inputs", {} )
+ collection_rows = [
+ [ "Profile files merged", number( inputs.get( "profiles", "N/A" ) ) ],
+ [ "Instrumented product objects", number( inputs.get( "coverage_objects", "N/A" ) ) ],
+ [ "Audited zero-hash mappings", number( inputs.get( "zero_hash_mappings", "N/A" ) ) ],
+ ]
+ result += (
+ "Coverage collection "
+ + table( [ "Input", "Value" ], collection_rows )
+ + 'Zero-hash mappings have no profile counters and are audited during report generation; any nonzero or unaudited mismatch fails the job.
'
+ )
+ if supplemental is not None:
+ result += (
+ 'Diagnostic only: native branch outcomes '
+ f"{escape( metric_counts( supplemental ) )} ({escape( metric_percent( supplemental ))}). "
+ "They are instantiation-weighted and are not the canonical branch gate.
"
+ )
+ return result + " "
+
+
+def comparison_table( report: dict ) -> str:
+ aggregate = report.get( "aggregate_comparison", {} )
+ if not aggregate.get( "comparable", False ):
+ reason = escape( aggregate.get( "reason", "No exact-base coverage summary was available." ) )
+ return table( [ "Comparison", "Result", "Reason" ], [ [ "Exact base → candidate", badge( "info", "Unavailable" ), reason ] ] )
+ rows = []
+ for name in METRIC_NAMES:
+ comparison = aggregate.get( "metrics", {} ).get( name )
+ if comparison is None:
+ continue
+ delta = comparison.get( "percentage_point_delta", 0.0 )
+ state = "pass" if delta >= 0 else "warn"
+ rows.append(
+ [
+ escape( METRIC_LABELS[name] ),
+ escape( metric_counts( comparison.get( "baseline" ) ) + " (" + metric_percent( comparison.get( "baseline" ) ) + ")" ),
+ escape( metric_counts( comparison.get( "candidate" ) ) + " (" + metric_percent( comparison.get( "candidate" ) ) + ")" ),
+ f'{escape( f"{delta:+.2f} pp" )} ',
+ escape( f"{comparison.get( 'covered_delta', 0):+,}" ),
+ escape( f"{comparison.get( 'not_covered_delta', 0):+,}" ),
+ escape( f"{comparison.get( 'total_delta', 0):+,}" ),
+ ]
+ )
+ return table(
+ [ "Metric", "Exact base", "Candidate", "Coverage change", "Covered Δ", "Missed Δ", "Total Δ" ],
+ rows,
+ )
+
+
+def pull_request_section( report: dict | None ) -> str:
+ if report is None:
+ return 'Pull-request coverage No pull-request coverage report was produced.
'
+ aggregate = report.get( "aggregate_comparison", {} )
+ patch = report.get( "patch", {} )
+ base = escape( str( report.get( "base_sha", "unknown" ) )[:12] )
+ head = escape( str( report.get( "head_sha", "unknown" ) )[:12] )
+ scope = escape( report.get( "scope", "unknown" ) )
+ result = (
+ f'Pull-request coverage '
+ f'Effective source diff {base} → {head} · Production scope {scope}.
'
+ "Project coverage change "
+ + comparison_table( report )
+ )
+ if aggregate.get( "comparable", False ):
+ result += 'Comparison is exact because commit, tree, and measurement-contract provenance match.
'
+ else:
+ result += 'Baseline comparison is advisory and is unavailable when no exact-base artifact is present.
'
+
+ patch_metrics = patch.get( "metrics", {} )
+ patch_rows = []
+ interpretations = {
+ "executable_lines": "Exact LLVM line mapping on added/modified head lines",
+ "function_start_sites": "Advisory; C++ instantiations may share a source definition",
+ "native_branch_outcomes": "Advisory and instantiation-weighted; not the canonical branch gate",
+ }
+ for name, label in PATCH_METRIC_LABELS.items():
+ metric = patch_metrics.get( name, {} )
+ state = "pass" if metric.get( "total", 0 ) == 0 or metric.get( "not_covered", 0 ) == 0 else "warn"
+ patch_rows.append(
+ [
+ escape( label ),
+ number( metric.get( "covered", 0 ) ),
+ number( metric.get( "not_covered", 0 ) ),
+ number( metric.get( "total", 0 ) ),
+ escape( metric_percent( metric ) ),
+ badge( state, "Covered" if state == "pass" else "Review" ),
+ escape( interpretations[name] ),
+ ]
+ )
+ result += "Changed-code coverage " + table(
+ [ "Signal", "Covered", "Missed", "Total", "Coverage", "Review", "Interpretation" ],
+ patch_rows,
+ )
+ result += (
+ f'Git identified {number( patch.get( "changed_lines", 0 ) )} '
+ f"added/modified head lines across {number( patch.get( 'changed_files', 0 ) )} production files. "
+ f"{number( patch.get( 'non_instrumented_changed_lines', 0 ) )} changed lines have no LLVM line mapping."
+ "
"
+ )
+
+ impacted_files = patch.get( "top_impacted_files", [] )
+ if impacted_files:
+ file_rows = []
+ for source in impacted_files:
+ metrics = source.get( "metrics", {} )
+ file_rows.append(
+ [
+ f'{escape( source.get( "path", "" ) )}',
+ escape( source.get( "status", "" ) ),
+ number( source.get( "changed_lines", 0 ) ),
+ escape( metric_counts( metrics.get( "executable_lines" ) ) ),
+ escape( metric_counts( metrics.get( "function_start_sites" ) ) ),
+ escape( metric_counts( metrics.get( "native_branch_outcomes" ) ) ),
+ ]
+ )
+ result += "Changed files with the largest coverage gaps " + table(
+ [ "Source file", "Change", "Changed lines", "Executable lines", "Function starts", "Native branches" ],
+ file_rows,
+ )
+
+ locations = patch.get( "top_impacted_locations", [] )
+ result += "Highest-impact changed locations "
+ if locations:
+ location_rows = []
+ for location in locations:
+ gaps = []
+ if location.get( "executable_line" ) and not location.get( "line_covered" ):
+ gaps.append( "executable line not hit" )
+ missed_branches = location.get( "native_branch_outcomes", {} ).get( "not_covered", 0 )
+ if missed_branches:
+ gaps.append( f"{number( missed_branches )} native branch outcome(s) missed" )
+ if location.get( "function_start_covered" ) is False:
+ gaps.append( "function start not entered" )
+ location_rows.append(
+ [
+ f'{escape( location.get( "path", "" ) )}:{number( location.get( "line", "" ) )}',
+ escape( "; ".join( gaps ) ),
+ escape( location.get( "hint", "" ) ),
+ ]
+ )
+ result += table( [ "Location", "Coverage gap", "Suggested test" ], location_rows )
+ else:
+ result += 'No uncovered instrumented changed locations were found.
'
+ return result + (
+ 'Changed-line coverage is exact. Function-start and native branch details are diagnostics because LLVM 20 retains C++ template and inline-function instantiations.
'
+ )
+
+
+def details_section( artifact_dir: pathlib.Path ) -> str:
+ sections = []
+ for label, filename in DETAIL_FILES:
+ path = artifact_dir / filename
+ text = read_text( path )
+ if text is None:
+ continue
+ if filename.endswith( ".json" ):
+ document = read_json( path )
+ if document is not None:
+ text = json.dumps( document, indent=2, sort_keys=True, ensure_ascii=False ) + "\n"
+ sections.append(
+ f'{escape( label )} {escape( filename )} '
+ f'{number( len( text.encode( "utf-8" ) ) )} bytes '
+ f'{escape( text )} '
+ )
+ if not sections:
+ return 'Diagnostics and raw data No diagnostic files were produced.
'
+ return 'Diagnostics and raw data All intermediate reports are retained below in collapsible sections, so this single HTML file remains the complete downloadable artifact.
' + "".join( sections ) + " "
+
+
+def render_html( artifact_dir: pathlib.Path ) -> str:
+ status_markdown = read_text( artifact_dir / "coverage-status.md" )
+ status = status_details( status_markdown )
+ summary = validated_summary( read_json( artifact_dir / "coverage-summary.json" ) )
+ policy_document = read_json( pathlib.Path( __file__ ).parents[1] / ".github" / "coverage-thresholds.json" )
+ thresholds = validated_policy( policy_document, summary.get( "scope" ) if summary else None )
+ report = read_json( artifact_dir / "pr-coverage.json" )
+ policy_state, policy_result_text, policy_detail = policy_card( summary, thresholds )
+ baseline_state, baseline_result, baseline_detail = baseline_card( report )
+ changed_state, changed_result, changed_detail = changed_lines_card( report )
+ test_result, test_detail = status.get( "tests" ) or ( "Unavailable", "No CTest summary was recorded." )
+ overall_state = status["state"]
+ overall_result = status["result"]
+ if overall_state == "info":
+ overall_state = "fail" if policy_state == "fail" else policy_state
+ overall_result = "Failed" if overall_state == "fail" else "Available"
+ summary_metrics = summary or {}
+ scope = escape( summary_metrics.get( "scope", "src/coreComponents" ) )
+ measurement = summary_metrics.get( "measurement", {} )
+ commit = escape( str( measurement.get( "commit_sha", "unknown" ) )[:12] )
+ configuration = status.get( "configuration", {} )
+ config_rows = [ [ escape( label ), escape( value ) ] for label, value in configuration.items() ]
+ if not config_rows and measurement:
+ toolchain = measurement.get( "toolchain", {} )
+ config_rows = [
+ [ "Compiler target", escape( toolchain.get( "compiler_target", "unknown" ) ) ],
+ [ "LLVM", escape( summary_metrics.get( "tool", {} ).get( "major", "unknown" ) ) ],
+ ]
+ nav = ''.join(
+ f'{escape( label )} '
+ for anchor, label in (
+ ( "coverage", "Coverage" ),
+ ( "pull-request", "Pull request" ),
+ ( "details", "Diagnostics" ),
+ )
+ )
+ return f"""
+
+
+
+
+GEOS LLVM source coverage
+
+
+
+
+
+{nav}
+
+{card( "Coverage smoke tests", test_result, escape( test_detail ), "pass" if "pass" in test_result.lower() else "fail" if "fail" in test_result.lower() else "info" )}
+{card( "Enforced policy", policy_result_text, escape( policy_detail ), policy_state )}
+{card( "Baseline comparison", baseline_result, escape( baseline_detail ), baseline_state )}
+{card( "Changed production lines", changed_result, escape( changed_detail ), changed_state )}
+
+{coverage_section( summary, thresholds )}
+{pull_request_section( report )}
+Run configuration {table( [ "Item", "Value" ], config_rows, "config" ) if config_rows else 'Run configuration is unavailable.
'}
+{details_section( artifact_dir )}
+
+
+
+"""
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser( description=__doc__ )
+ parser.add_argument( "--artifact-dir", required=True, type=pathlib.Path )
+ parser.add_argument( "--output", required=True, type=pathlib.Path )
+ arguments = parser.parse_args()
+ try:
+ write_atomic( arguments.output, render_html( arguments.artifact_dir ) )
+ except ( OSError, ValueError ) as error:
+ print( f"Coverage HTML report error: {error}", file=sys.stderr )
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit( main() )
diff --git a/scripts/render_coverage_review_summary.py b/scripts/render_coverage_review_summary.py
new file mode 100644
index 00000000000..c53fbbd4f24
--- /dev/null
+++ b/scripts/render_coverage_review_summary.py
@@ -0,0 +1,250 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-only
+
+"""Render the concise reviewer-facing portion of the LLVM coverage summary."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import pathlib
+import sys
+
+
+SCRIPT_DIRECTORY = pathlib.Path( __file__ ).resolve().parent
+if str( SCRIPT_DIRECTORY ) not in sys.path:
+ sys.path.insert( 0, str( SCRIPT_DIRECTORY ) )
+import check_coverage_thresholds as coverage_contract
+
+
+DISPLAY_NAMES = {
+ "regions": "Regions",
+ "functions": "Functions",
+ "lines": "Lines",
+ "branches": "Canonical branches",
+}
+CONSOLE_METRICS = ( "lines", "functions", "branches" )
+
+
+def render_console_summary( summary: dict, thresholds: dict ) -> str:
+ """Render a plain-text table for the GitHub Actions log."""
+ rows = []
+ all_passed = True
+ for name in CONSOLE_METRICS:
+ metric = summary["metrics"][name]
+ threshold = thresholds[name]
+ passed = metric["covered"] * 10000 >= threshold * metric["total"]
+ all_passed = all_passed and passed
+ rows.append(
+ (
+ DISPLAY_NAMES[name],
+ f"{metric['percent']:.2f}%",
+ f"{metric['covered']:,} / {metric['total']:,}",
+ f"{metric['not_covered']:,}",
+ f">= {threshold / 100:.2f}%",
+ "✅ Pass" if passed else "❌ Fail",
+ )
+ )
+
+ headers = ( "Metric", "Coverage", "Covered / total", "Missed", "Requirement", "Result" )
+ widths = [
+ max( len( headers[index] ), *( len( row[index] ) for row in rows ) )
+ for index in range( len( headers ) )
+ ]
+
+ def border() -> str:
+ return "+" + "+".join( "-" * ( width + 2 ) for width in widths ) + "+"
+
+ def row( values ) -> str:
+ return "|" + "|".join(
+ f" {value:<{width}} "
+ for value, width in zip( values, widths )
+ ) + "|"
+
+ status = "✅ PASS" if all_passed else "❌ FAIL"
+ output = [
+ "",
+ "LLVM source coverage policy",
+ f"Overall result: {status}",
+ border(),
+ row( headers ),
+ border(),
+ *( row( values ) for values in rows ),
+ border(),
+ "",
+ ]
+ return "\n".join( output )
+
+
+def read_json( path: pathlib.Path, description: str ) -> dict:
+ try:
+ with path.open( encoding="utf-8" ) as stream:
+ value = json.load( stream )
+ except ( OSError, UnicodeError, json.JSONDecodeError ) as error:
+ raise ValueError( f"cannot read {description} {path}: {error}" ) from error
+ if not isinstance( value, dict ):
+ raise ValueError( f"{description} must contain an object" )
+ return value
+
+
+def metric_text( metric: dict ) -> str:
+ return (
+ f"{metric['percent']:.2f}% "
+ f"({metric['covered']:,}/{metric['total']:,})"
+ )
+
+
+def policy_row( summary: dict, thresholds: dict ) -> str:
+ details = []
+ all_passed = True
+ for name in coverage_contract.DISPLAY_METRICS:
+ if name not in thresholds:
+ continue
+ metric = summary["metrics"][name]
+ threshold = thresholds[name]
+ passed = metric["covered"] * 10000 >= threshold * metric["total"]
+ all_passed = all_passed and passed
+ result = "✅" if passed else "❌"
+ details.append(
+ f"{result} {DISPLAY_NAMES[name]} {metric_text( metric )} "
+ f"≥ {threshold / 100:.2f}%"
+ )
+ status = "✅ Pass" if all_passed else "❌ Fail"
+ return (
+ f"| Enforced coverage policy | {status} | "
+ f"{' '.join( details )} |"
+ )
+
+
+def comparison_row( report: dict | None ) -> str:
+ if report is None:
+ return (
+ "| Baseline comparison | ⏭️ Not applicable | "
+ "This is not a pull-request coverage run. |"
+ )
+
+ aggregate = report.get( "aggregate_comparison", {} )
+ if not aggregate.get( "comparable", False ):
+ reason = aggregate.get( "reason", "No exact-base coverage summary was provided." )
+ if reason == "No exact-base coverage summary was provided.":
+ reason = "No exact-base baseline artifact was available."
+ return f"| Baseline comparison | ℹ️ Unavailable | {reason} |"
+
+ deltas = []
+ for name in coverage_contract.DISPLAY_METRICS:
+ comparison = aggregate["metrics"].get( name )
+ if comparison is not None:
+ deltas.append( comparison["percentage_point_delta"] )
+ if all( delta >= 0 for delta in deltas ):
+ result = "✅ Maintained/improved"
+ elif all( delta <= 0 for delta in deltas ):
+ result = "⚠️ Decreased"
+ else:
+ result = "⚠️ Mixed"
+ details = "; ".join(
+ f"{DISPLAY_NAMES[name]} {aggregate['metrics'][name]['percentage_point_delta']:+.2f} pp"
+ for name in coverage_contract.DISPLAY_METRICS
+ if name in aggregate["metrics"]
+ )
+ return f"| Baseline comparison | {result} | {details} |"
+
+
+def changed_lines_row( report: dict | None ) -> str:
+ if report is None:
+ return (
+ "| Changed production lines | ⏭️ Not applicable | "
+ "This is not a pull-request coverage run. |"
+ )
+
+ metric = report["patch"]["metrics"]["executable_lines"]
+ if metric["total"] == 0:
+ return (
+ "| Changed production lines | ℹ️ None | "
+ "The production diff contains no executable changed lines. |"
+ )
+
+ locations = []
+ for location in report["patch"].get( "top_impacted_locations", [] ):
+ if location.get( "executable_line" ) and not location.get( "line_covered" ):
+ locations.append( f"{location['path']}:{location['line']}" )
+ if len( locations ) == 3:
+ break
+ details = (
+ f"{metric['covered']:,}/{metric['total']:,} executable changed lines covered"
+ )
+ if locations:
+ details += "; review " + ", ".join( f"{path}" for path in locations )
+ if metric["not_covered"]:
+ return f"| Changed production lines | ⚠️ Review | {details} |"
+ return f"| Changed production lines | ✅ Covered | {details} |"
+
+
+def render_summary(
+ summary: dict,
+ thresholds: dict,
+ tests_result: str,
+ tests_detail: str,
+ report: dict | None,
+) -> str:
+ rows = [
+ "### Reviewer summary",
+ "",
+ "| Signal | Result | Details |",
+ "|---|:---:|---|",
+ f"| Coverage smoke tests | {tests_result} | {tests_detail} |",
+ policy_row( summary, thresholds ),
+ comparison_row( report ),
+ changed_lines_row( report ),
+ "",
+ "The policy row is the enforced gate. Baseline and changed-line signals "
+ "are reviewer guidance; detailed diagnostics appear below.",
+ "",
+ ]
+ return "\n".join( rows )
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(
+ description="Render the concise LLVM coverage review summary."
+ )
+ parser.add_argument( "--coverage-summary", required=True, type=pathlib.Path )
+ parser.add_argument( "--thresholds", required=True, type=pathlib.Path )
+ parser.add_argument( "--pr-report", type=pathlib.Path )
+ parser.add_argument( "--console", action="store_true" )
+ parser.add_argument( "--tests-result" )
+ parser.add_argument( "--tests-detail" )
+ arguments = parser.parse_args()
+
+ try:
+ raw_summary = read_json( arguments.coverage_summary, "coverage summary" )
+ raw_policy = read_json( arguments.thresholds, "coverage policy" )
+ summary = coverage_contract.validate_summary( raw_summary )
+ thresholds = coverage_contract.validate_policy( raw_policy, summary["scope"] )
+ if arguments.console:
+ print( render_console_summary( summary, thresholds ), end="" )
+ return 0
+ if arguments.tests_result is None or arguments.tests_detail is None:
+ raise ValueError( "--tests-result and --tests-detail are required" )
+ report = (
+ read_json( arguments.pr_report, "pull-request coverage report" )
+ if arguments.pr_report is not None
+ else None
+ )
+ print(
+ render_summary(
+ summary,
+ thresholds,
+ arguments.tests_result,
+ arguments.tests_detail,
+ report,
+ ),
+ end="",
+ )
+ return 0
+ except ( OSError, ValueError ) as error:
+ print( f"Coverage review summary error: {error}", file=sys.stderr )
+ return 1
+
+
+if __name__ == "__main__":
+ raise SystemExit( main() )
diff --git a/scripts/tests/test_check_coverage_thresholds.py b/scripts/tests/test_check_coverage_thresholds.py
new file mode 100755
index 00000000000..0b77b3c7cc3
--- /dev/null
+++ b/scripts/tests/test_check_coverage_thresholds.py
@@ -0,0 +1,537 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-only
+
+import importlib.util
+import json
+import pathlib
+import unittest
+
+
+SCRIPT = pathlib.Path( __file__ ).parents[1] / "check_coverage_thresholds.py"
+REPOSITORY_ROOT = pathlib.Path( __file__ ).parents[2]
+SPEC = importlib.util.spec_from_file_location( "coverage_gate", SCRIPT )
+coverage_gate = importlib.util.module_from_spec( SPEC )
+SPEC.loader.exec_module( coverage_gate )
+
+
+def metric( covered, total ):
+ return {
+ "covered": covered,
+ "total": total,
+ "not_covered": total - covered,
+ "percent": round( 100.0 * covered / total if total else 0.0, 6 ),
+ }
+
+
+def measurement( scope, excluded_regex ):
+ value = {
+ "commit_sha": "1" * 40,
+ "tree_sha": "2" * 40,
+ "contract_id": coverage_gate.COVERAGE_CONTRACT_ID,
+ "contract_fingerprint": "0" * 64,
+ "container": {
+ "image": "geosx/ubuntu24.04-clang20:349-1014",
+ "image_id": f"sha256:{'3' * 64}",
+ "image_digests": [
+ f"geosx/ubuntu24.04-clang20@sha256:{'4' * 64}"
+ ],
+ },
+ "toolchain": {
+ "c_compiler_version": (
+ "Ubuntu clang version 20.1.2 (0ubuntu1~24.04.3)\n"
+ "Target: x86_64-pc-linux-gnu"
+ ),
+ "cxx_compiler_version": (
+ "Ubuntu clang version 20.1.2 (0ubuntu1~24.04.3)\n"
+ "Target: x86_64-pc-linux-gnu"
+ ),
+ "llvm_cov_version": (
+ "Ubuntu LLVM version 20.1.2\n Optimized build."
+ ),
+ "llvm_package_versions": [
+ "libclang-rt-20-dev=1:20.1.2-0ubuntu1~24.04.3",
+ "llvm-20=1:20.1.2-0ubuntu1~24.04.3",
+ ],
+ "compiler_target": "x86_64-pc-linux-gnu",
+ },
+ "build_config": {
+ "BLT_CXX_STD": "c++17",
+ "BUILD_SHARED_LIBS": True,
+ "CMAKE_BUILD_TYPE": "RelWithDebInfo",
+ "CMAKE_C_FLAGS": "-pthread",
+ "CMAKE_C_FLAGS_RELWITHDEBINFO": "-O2 -g -DNDEBUG",
+ "CMAKE_CXX_FLAGS": "-pthread",
+ "CMAKE_CXX_FLAGS_RELWITHDEBINFO": "-O2 -g -DNDEBUG",
+ "ENABLE_COVERAGE": False,
+ "ENABLE_CUDA": False,
+ "ENABLE_HIP": False,
+ "ENABLE_HYPRE": True,
+ "ENABLE_MPI": True,
+ "ENABLE_OPENMP": True,
+ "ENABLE_HYPRE_DEVICE": "CPU",
+ "ENABLE_TRILINOS": False,
+ "GEOS_BUILD_SHARED_LIBS": True,
+ "GEOS_ENABLE_BOUNDS_CHECK": False,
+ "GEOS_ENABLE_LLVM_SOURCE_COVERAGE": True,
+ "GEOS_GLOBALINDEX_TYPE": "long long int",
+ "GEOS_LA_INTERFACE": "Hypre",
+ "GEOS_LOCALINDEX_TYPE": "int",
+ "LVARRAY_BOUNDS_CHECK": False,
+ "RAJA_ENABLE_CUDA": False,
+ "RAJA_ENABLE_HIP": False,
+ "RAJA_ENABLE_OPENMP": False,
+ },
+ "metric_semantics": json.loads(
+ json.dumps( coverage_gate.METRIC_SEMANTICS )
+ ),
+ "object_selection": json.loads(
+ json.dumps( coverage_gate.OBJECT_SELECTION )
+ ),
+ }
+ value["contract_fingerprint"] = coverage_gate.compute_contract_fingerprint(
+ scope, excluded_regex, value
+ )
+ return value
+
+
+def summary( branch_covered=1, branch_total=2 ):
+ scope = "src/coreComponents"
+ excluded_regex = "/unitTests/"
+ canonical = {
+ "regions": metric( 1, 2 ),
+ "functions": metric( 1, 2 ),
+ "lines": metric( 1, 2 ),
+ "branches": metric( branch_covered, branch_total ),
+ }
+ document = {
+ "schema_version": 3,
+ "scope": scope,
+ "excluded_regex": excluded_regex,
+ "measurement": measurement( scope, excluded_regex ),
+ "tool": { "name": "llvm-cov", "major": 20 },
+ "inputs": {
+ "profiles": 10,
+ "coverage_objects": 2,
+ "zero_hash_mappings": 3,
+ },
+ "metrics": canonical,
+ "per_file_metrics": [
+ {
+ "path": "src/coreComponents/example.cpp",
+ "metrics": json.loads( json.dumps( canonical ) ),
+ }
+ ],
+ "top_branch_gaps": [],
+ "supplemental": {
+ "native_branch_outcomes": metric( 1, 4 ),
+ },
+ }
+ if branch_covered < branch_total:
+ document["top_branch_gaps"] = [
+ {
+ "path": "src/coreComponents/example.cpp",
+ **metric( branch_covered, branch_total ),
+ }
+ ]
+ return document
+
+
+class CoverageThresholdTests( unittest.TestCase ):
+ def test_exact_threshold_passes( self ):
+ validated = coverage_gate.validate_summary( summary() )
+ markdown, passed = coverage_gate.build_markdown(
+ validated, { "branches": 5000 }
+ )
+ self.assertTrue( passed )
+ self.assertIn( "50.00%", markdown )
+ self.assertIn( "✅ Coverage policy passed", markdown )
+
+ def test_one_count_below_threshold_fails( self ):
+ validated = coverage_gate.validate_summary( summary( 49, 100 ) )
+ markdown, passed = coverage_gate.build_markdown(
+ validated, { "branches": 5000 }
+ )
+ self.assertFalse( passed )
+ self.assertIn( "❌ Coverage policy failed", markdown )
+ self.assertIn( "❌ Fail (-1.00 pp)", markdown )
+
+ def test_integer_comparison_avoids_rounding_up( self ):
+ validated = coverage_gate.validate_summary( summary( 1, 3 ) )
+ markdown, passed = coverage_gate.build_markdown(
+ validated, { "branches": 3334 }
+ )
+ self.assertFalse( passed )
+ self.assertIn( "❌ Fail (<0.01 pp below)", markdown )
+
+ def test_checked_in_threshold_boundary_is_exact( self ):
+ passing = coverage_gate.validate_summary( summary( 5001, 10000 ) )
+ failing = coverage_gate.validate_summary( summary( 5000, 10000 ) )
+ self.assertTrue(
+ coverage_gate.build_markdown( passing, { "branches": 5001 } )[1]
+ )
+ self.assertFalse(
+ coverage_gate.build_markdown( failing, { "branches": 5001 } )[1]
+ )
+
+ def test_inconsistent_counts_are_rejected( self ):
+ document = summary()
+ document["metrics"]["branches"]["not_covered"] = 99
+ with self.assertRaisesRegex( ValueError, "inconsistent" ):
+ coverage_gate.validate_summary( document )
+
+ def test_boolean_schema_version_is_rejected( self ):
+ document = summary()
+ document["schema_version"] = True
+ with self.assertRaisesRegex( ValueError, "integer 3" ):
+ coverage_gate.validate_summary( document )
+
+ def test_invalid_metadata_is_rejected( self ):
+ document = summary()
+ document["inputs"]["profiles"] = 0
+ with self.assertRaisesRegex( ValueError, "inputs.profiles" ):
+ coverage_gate.validate_summary( document )
+
+ def test_contract_fingerprint_is_stable_and_excludes_revision_inputs( self ):
+ document = summary()
+ self.assertEqual(
+ document["measurement"]["contract_fingerprint"],
+ "392f58cf380615f8214dc716273efbadcabefb0f25d6bfdd83560077f1cd4672",
+ )
+
+ document["measurement"]["commit_sha"] = "a" * 40
+ document["measurement"]["tree_sha"] = "b" * 40
+ document["measurement"]["container"]["image"] = "local/relabel:latest"
+ document["inputs"]["profiles"] = 999
+ document["inputs"]["coverage_objects"] = 77
+ validated = coverage_gate.validate_summary( document )
+ self.assertEqual( validated["measurement"]["commit_sha"], "a" * 40 )
+
+ def test_hard_contract_changes_require_a_new_fingerprint( self ):
+ mutations = (
+ lambda document: document["measurement"]["build_config"].__setitem__(
+ "ENABLE_HYPRE", False
+ ),
+ lambda document: document["measurement"]["toolchain"].__setitem__(
+ "compiler_target", "aarch64-unknown-linux-gnu"
+ ),
+ lambda document: document["measurement"]["toolchain"].__setitem__(
+ "llvm_package_versions", [ "llvm-20=20.1.3" ]
+ ),
+ lambda document: document["measurement"]["container"].__setitem__(
+ "image_id", f"sha256:{'5' * 64}"
+ ),
+ )
+ for mutate in mutations:
+ with self.subTest( mutation=mutate ):
+ document = summary()
+ mutate( document )
+ with self.assertRaisesRegex( ValueError, "fingerprint" ):
+ coverage_gate.validate_summary( document )
+
+ def test_measurement_contract_fields_are_strictly_validated( self ):
+ document = summary()
+ document["measurement"]["contract_id"] = "other-contract"
+ with self.assertRaisesRegex( ValueError, "contract_id" ):
+ coverage_gate.validate_summary( document )
+
+ document = summary()
+ document["measurement"]["toolchain"]["llvm_cov_version"] = (
+ "Ubuntu LLVM version 19.1.0"
+ )
+ with self.assertRaisesRegex( ValueError, "does not match tool.major" ):
+ coverage_gate.validate_summary( document )
+
+ document = summary()
+ package = document["measurement"]["toolchain"]["llvm_package_versions"][0]
+ document["measurement"]["toolchain"]["llvm_package_versions"] = [
+ package,
+ package,
+ ]
+ with self.assertRaisesRegex( ValueError, "sorted and unique" ):
+ coverage_gate.validate_summary( document )
+
+ document = summary()
+ digest = document["measurement"]["container"]["image_digests"][0]
+ document["measurement"]["container"]["image_digests"] = [ digest, digest ]
+ with self.assertRaisesRegex( ValueError, "sorted and unique" ):
+ coverage_gate.validate_summary( document )
+
+ def test_build_config_must_be_normalized_and_relevant( self ):
+ document = summary()
+ document["measurement"]["build_config"]["ENABLE_COVERAGE"] = "OFF"
+ with self.assertRaisesRegex( ValueError, "ENABLE_COVERAGE" ):
+ coverage_gate.validate_summary( document )
+
+ document = summary()
+ document["measurement"]["build_config"]["CMAKE_INSTALL_PREFIX"] = "/tmp"
+ with self.assertRaisesRegex( ValueError, "irrelevant key" ):
+ coverage_gate.validate_summary( document )
+
+ def test_inconsistent_percent_is_rejected( self ):
+ document = summary()
+ document["metrics"]["branches"]["percent"] = 99.0
+ with self.assertRaisesRegex( ValueError, "percent is inconsistent" ):
+ coverage_gate.validate_summary( document )
+
+ def test_per_file_metrics_are_sorted_unique_and_in_scope( self ):
+ document = summary()
+ document["per_file_metrics"] = [
+ {
+ "path": "src/coreComponents/z.cpp",
+ "metrics": {
+ name: metric( 0, 1 ) for name in coverage_gate.CANONICAL_METRICS
+ },
+ },
+ {
+ "path": "src/coreComponents/a.cpp",
+ "metrics": {
+ name: metric( 1, 1 ) for name in coverage_gate.CANONICAL_METRICS
+ },
+ },
+ ]
+ document["top_branch_gaps"] = [
+ { "path": "src/coreComponents/z.cpp", **metric( 0, 1 ) }
+ ]
+ with self.assertRaisesRegex( ValueError, "sorted by path" ):
+ coverage_gate.validate_summary( document )
+
+ document = summary()
+ document["per_file_metrics"][0]["path"] = "outside.cpp"
+ with self.assertRaisesRegex( ValueError, "outside the scope" ):
+ coverage_gate.validate_summary( document )
+
+ document = summary()
+ duplicate = json.loads( json.dumps( document["per_file_metrics"][0] ) )
+ document["per_file_metrics"].append( duplicate )
+ with self.assertRaisesRegex( ValueError, "paths must be unique" ):
+ coverage_gate.validate_summary( document )
+
+ def test_per_file_metrics_must_reconcile_every_canonical_metric( self ):
+ for name in coverage_gate.CANONICAL_METRICS:
+ with self.subTest( metric=name ):
+ document = summary()
+ document["per_file_metrics"][0]["metrics"][name] = metric( 0, 2 )
+ with self.assertRaisesRegex( ValueError, f"{name} covered" ):
+ coverage_gate.validate_summary( document )
+
+ def test_per_file_zero_denominators_are_allowed( self ):
+ document = summary()
+ document["per_file_metrics"].insert(
+ 0,
+ {
+ "path": "src/coreComponents/empty.cpp",
+ "metrics": {
+ name: metric( 0, 0 ) for name in coverage_gate.CANONICAL_METRICS
+ },
+ },
+ )
+ validated = coverage_gate.validate_summary( document )
+ self.assertEqual(
+ validated["per_file_metrics"][0]["metrics"]["branches"]["total"],
+ 0,
+ )
+
+ def test_top_branch_gaps_must_match_per_file_metrics( self ):
+ document = summary()
+ document["top_branch_gaps"][0] = {
+ "path": "src/coreComponents/example.cpp",
+ **metric( 0, 2 ),
+ }
+ with self.assertRaisesRegex( ValueError, "does not match canonical per-file" ):
+ coverage_gate.validate_summary( document )
+
+ def test_policy_scope_must_match( self ):
+ policy = {
+ "schema_version": 1,
+ "scope": "wrong/scope",
+ "minimum_basis_points": { "branches": 5000 },
+ }
+ with self.assertRaisesRegex( ValueError, "does not match" ):
+ coverage_gate.validate_policy( policy, "src/coreComponents" )
+
+ def test_non_gated_metrics_are_report_only( self ):
+ validated = coverage_gate.validate_summary( summary() )
+ markdown, passed = coverage_gate.build_markdown(
+ validated, { "branches": 5000 }
+ )
+ self.assertTrue( passed )
+ self.assertIn( "Functions", markdown )
+ self.assertIn( "ℹ️ Measured", markdown )
+
+ def test_lines_and_functions_thresholds_are_enforced( self ):
+ validated = coverage_gate.validate_summary( summary() )
+ markdown, passed = coverage_gate.build_markdown(
+ validated,
+ { "lines": 5001, "functions": 5001, "branches": 5000 },
+ )
+
+ self.assertFalse( passed )
+ self.assertIn( "| Lines | 1 | 1 | 2 | 50.00% | ≥ 50.01% | ❌ Fail", markdown )
+ self.assertIn(
+ "| Functions | 1 | 1 | 2 | 50.00% | ≥ 50.01% | ❌ Fail", markdown
+ )
+
+ def test_markdown_golden_output( self ):
+ validated = coverage_gate.validate_summary( summary() )
+ markdown, passed = coverage_gate.build_markdown(
+ validated, { "branches": 5000 }
+ )
+ expected = """### ✅ Coverage policy passed
+
+Production scope: `src/coreComponents` · Reporter: `llvm-cov 20`
+
+| Metric | Covered | Missed | Total | Coverage | Requirement | Result |
+|---|---:|---:|---:|---:|---:|:---:|
+| Lines | 1 | 1 | 2 | 50.00% | — | ℹ️ Measured |
+| Functions | 1 | 1 | 2 | 50.00% | — | ℹ️ Measured |
+| Canonical branches | 1 | 1 | 2 | 50.00% | ≥ 50.00% | ✅ Pass (+0.00 pp) |
+| Regions | 1 | 1 | 2 | 50.00% | — | ℹ️ Measured |
+
+Percentages are rounded for display; policy thresholds use exact covered/total counts.
+
+### Largest branch gaps
+
+| Source file | Covered | Missed | Total | Coverage |
+|---|---:|---:|---:|---:|
+| example.cpp | 1 | 1 | 2 | 50.00% |
+
+Ranked by missed canonical branch outcomes within the production scope.
+
+### Coverage collection
+
+| Input | Value |
+|---|---:|
+| Profile files merged | 10 |
+| Instrumented product objects | 2 |
+| Audited zero-hash mappings | 3 |
+
+Zero-hash mappings have no profile counters and are audited during report generation; any nonzero or unaudited mismatch fails the job.
+
+
+Branch diagnostic (not gated)
+
+| Diagnostic | Covered | Total | Coverage |
+|---|---:|---:|---:|
+| Native branch outcomes | 1 | 4 | 25.00% |
+
+Native outcomes are instantiation-weighted: C++ template and inline-function instances may appear more than once. This diagnostic is not the policy metric.
+
+
+"""
+ self.assertTrue( passed )
+ self.assertEqual( markdown, expected )
+
+ def test_markdown_presents_counts_inputs_and_diagnostics( self ):
+ document = summary( 12, 23 )
+ document["inputs"] = {
+ "profiles": 3451,
+ "coverage_objects": 29,
+ "zero_hash_mappings": 29966,
+ }
+ document["metrics"]["functions"] = metric( 10887, 14276 )
+ document["supplemental"]["native_branch_outcomes"] = metric(
+ 48401, 684500
+ )
+ document["top_branch_gaps"] = [
+ {
+ "path": "src/coreComponents/a.cpp",
+ **metric( 10, 20 ),
+ },
+ {
+ "path": "src/coreComponents/nested/b.cpp",
+ **metric( 2, 3 ),
+ },
+ ]
+ document["per_file_metrics"] = [
+ {
+ "path": "src/coreComponents/a.cpp",
+ "metrics": {
+ "regions": metric( 1, 2 ),
+ "functions": metric( 10887, 14276 ),
+ "lines": metric( 1, 2 ),
+ "branches": metric( 10, 20 ),
+ },
+ },
+ {
+ "path": "src/coreComponents/nested/b.cpp",
+ "metrics": {
+ "regions": metric( 0, 0 ),
+ "functions": metric( 0, 0 ),
+ "lines": metric( 0, 0 ),
+ "branches": metric( 2, 3 ),
+ },
+ },
+ ]
+ validated = coverage_gate.validate_summary( document )
+ markdown, passed = coverage_gate.build_markdown(
+ validated, { "branches": 5001 }
+ )
+
+ self.assertTrue( passed )
+ self.assertIn(
+ "| Functions | 10,887 | 3,389 | 14,276 | 76.26% | — | ℹ️ Measured |",
+ markdown,
+ )
+ self.assertIn( "### Largest branch gaps", markdown )
+ self.assertLess( markdown.index( "a.cpp" ), markdown.index( "nested/b.cpp" ) )
+ self.assertIn( "| a.cpp | 10 | 10 | 20 | 50.00% |", markdown )
+ self.assertIn( "| Profile files merged | 3,451 |", markdown )
+ self.assertIn( "| Instrumented product objects | 29 |", markdown )
+ self.assertIn( "| Audited zero-hash mappings | 29,966 |", markdown )
+ self.assertIn( "| Native branch outcomes | 48,401 | 684,500 | 7.07% |", markdown )
+ primary_table, diagnostic = markdown.split( "", maxsplit=1 )
+ self.assertNotIn( "Native branch outcomes", primary_table )
+ self.assertIn( "instantiation-weighted", diagnostic )
+
+ def test_branch_gap_paths_must_be_unique_and_in_scope( self ):
+ document = summary()
+ document["top_branch_gaps"][0]["path"] = "src/tests/not-production.cpp"
+ with self.assertRaisesRegex( ValueError, "outside the scope" ):
+ coverage_gate.validate_summary( document )
+
+ document = summary()
+ document["top_branch_gaps"].append( dict( document["top_branch_gaps"][0] ) )
+ with self.assertRaisesRegex( ValueError, "normalized and unique" ):
+ coverage_gate.validate_summary( document )
+
+ for invalid_path in (
+ "src/coreComponents/",
+ "src/coreComponents//file.cpp",
+ "src/coreComponents/./file.cpp",
+ ):
+ with self.subTest( path=invalid_path ):
+ document = summary()
+ document["top_branch_gaps"][0]["path"] = invalid_path
+ with self.assertRaisesRegex( ValueError, "normalized" ):
+ coverage_gate.validate_summary( document )
+
+ def test_branch_gaps_must_be_sorted_by_missed_count( self ):
+ document = summary()
+ document["top_branch_gaps"] = [
+ { "path": "src/coreComponents/small.cpp", **metric( 2, 3 ) },
+ { "path": "src/coreComponents/large.cpp", **metric( 1, 4 ) },
+ ]
+ with self.assertRaisesRegex( ValueError, "sorted" ):
+ coverage_gate.validate_summary( document )
+
+ def test_branch_gap_totals_cannot_exceed_canonical_branches( self ):
+ document = summary()
+ document["top_branch_gaps"] = [
+ { "path": "src/coreComponents/too-large.cpp", **metric( 0, 3 ) }
+ ]
+ with self.assertRaisesRegex( ValueError, "exceed canonical" ):
+ coverage_gate.validate_summary( document )
+
+ def test_checked_in_policy_is_valid( self ):
+ policy_path = REPOSITORY_ROOT / ".github" / "coverage-thresholds.json"
+ with policy_path.open( encoding="utf-8" ) as stream:
+ policy = json.load( stream )
+ thresholds = coverage_gate.validate_policy( policy, "src/coreComponents" )
+ self.assertEqual(
+ thresholds,
+ { "lines": 7001, "functions": 7501, "branches": 5001 },
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/scripts/tests/test_compare_pr_coverage.py b/scripts/tests/test_compare_pr_coverage.py
new file mode 100755
index 00000000000..15e541ee404
--- /dev/null
+++ b/scripts/tests/test_compare_pr_coverage.py
@@ -0,0 +1,540 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-only
+
+from __future__ import annotations
+
+import importlib.util
+import json
+import pathlib
+import stat
+import subprocess
+import sys
+import tempfile
+import unittest
+
+
+SCRIPT = pathlib.Path( __file__ ).parents[1] / "compare_pr_coverage.py"
+SPEC = importlib.util.spec_from_file_location( "compare_pr_coverage", SCRIPT )
+compare_pr_coverage = importlib.util.module_from_spec( SPEC )
+SPEC.loader.exec_module( compare_pr_coverage )
+
+
+def run_command( repository: pathlib.Path, *arguments: str ) -> str:
+ result = subprocess.run(
+ [ *arguments ],
+ cwd=repository,
+ check=True,
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+ return result.stdout.strip()
+
+
+def metric( covered: int, total: int ) -> dict:
+ return {
+ "covered": covered,
+ "not_covered": total - covered,
+ "total": total,
+ "percent": round( 100.0 * covered / total, 6 ),
+ }
+
+
+class GitFixture:
+ def __init__( self ) -> None:
+ self.temporary = tempfile.TemporaryDirectory()
+ self.repository = pathlib.Path( self.temporary.name ) / "repository"
+ self.repository.mkdir()
+ run_command( self.repository, "git", "init", "-q", "-b", "develop" )
+ run_command(
+ self.repository, "git", "config", "user.email", "coverage@example.com"
+ )
+ run_command(
+ self.repository, "git", "config", "user.name", "Coverage Tests"
+ )
+
+ def close( self ) -> None:
+ self.temporary.cleanup()
+
+ def write( self, path: str, contents: str ) -> pathlib.Path:
+ target = self.repository / path
+ target.parent.mkdir( parents=True, exist_ok=True )
+ target.write_text( contents, encoding="utf-8" )
+ return target
+
+ def commit( self, message: str ) -> str:
+ run_command( self.repository, "git", "add", "--all" )
+ run_command( self.repository, "git", "commit", "-q", "-m", message )
+ return run_command( self.repository, "git", "rev-parse", "HEAD" )
+
+ def tree( self, commit_sha: str ) -> str:
+ return run_command(
+ self.repository, "git", "rev-parse", f"{commit_sha}^{{tree}}"
+ )
+
+
+def summary_document(
+ commit_sha: str,
+ tree_sha: str,
+ *,
+ schema_version: int = 3,
+ image_digit: str = "3",
+ scope: str = "src/coreComponents",
+ covered_offset: int = 0,
+) -> dict:
+ document = {
+ "schema_version": schema_version,
+ "scope": scope,
+ "excluded_regex": "/(unitTests|tests)/",
+ "tool": { "name": "llvm-cov", "major": 20 },
+ "inputs": {
+ "profiles": 4,
+ "coverage_objects": 2,
+ "zero_hash_mappings": 0,
+ },
+ "metrics": {
+ "regions": metric( 50 + covered_offset, 100 ),
+ "functions": metric( 60 + covered_offset, 100 ),
+ "lines": metric( 70 + covered_offset, 100 ),
+ "branches": metric( 40 + covered_offset, 100 ),
+ },
+ "top_branch_gaps": [],
+ "supplemental": {
+ "native_branch_outcomes": metric( 5 + covered_offset, 100 )
+ },
+ }
+ if schema_version >= 3:
+ measurement = {
+ "commit_sha": commit_sha,
+ "tree_sha": tree_sha,
+ "contract_id": compare_pr_coverage.coverage_contract.COVERAGE_CONTRACT_ID,
+ "contract_fingerprint": "0" * 64,
+ "container": {
+ "image": "geosx/ubuntu24.04-clang20:test",
+ "image_id": f"sha256:{image_digit * 64}",
+ "image_digests": [
+ f"geosx/ubuntu24.04-clang20@sha256:{'4' * 64}"
+ ],
+ },
+ "toolchain": {
+ "c_compiler_version": "Ubuntu clang version 20.1.2",
+ "cxx_compiler_version": "Ubuntu clang version 20.1.2",
+ "llvm_cov_version": "Ubuntu LLVM version 20.1.2",
+ "llvm_package_versions": [
+ "libclang-rt-20-dev=1:20.1.2",
+ "llvm-20=1:20.1.2",
+ ],
+ "compiler_target": "x86_64-pc-linux-gnu",
+ },
+ "build_config": {
+ "BLT_CXX_STD": "c++17",
+ "BUILD_SHARED_LIBS": True,
+ "CMAKE_BUILD_TYPE": "RelWithDebInfo",
+ "CMAKE_C_FLAGS": "-pthread",
+ "CMAKE_C_FLAGS_RELWITHDEBINFO": "-O2 -g -DNDEBUG",
+ "CMAKE_CXX_FLAGS": "-pthread",
+ "CMAKE_CXX_FLAGS_RELWITHDEBINFO": "-O2 -g -DNDEBUG",
+ "ENABLE_COVERAGE": False,
+ "ENABLE_CUDA": False,
+ "ENABLE_HIP": False,
+ "ENABLE_HYPRE": True,
+ "ENABLE_MPI": True,
+ "ENABLE_OPENMP": True,
+ "ENABLE_TRILINOS": False,
+ "GEOS_BUILD_SHARED_LIBS": True,
+ "GEOS_ENABLE_BOUNDS_CHECK": False,
+ "GEOS_ENABLE_LLVM_SOURCE_COVERAGE": True,
+ "GEOS_GLOBALINDEX_TYPE": "long long int",
+ "GEOS_LA_INTERFACE": "Hypre",
+ "GEOS_LOCALINDEX_TYPE": "int",
+ "LVARRAY_BOUNDS_CHECK": False,
+ "RAJA_ENABLE_CUDA": False,
+ "RAJA_ENABLE_HIP": False,
+ "RAJA_ENABLE_OPENMP": False,
+ },
+ "metric_semantics": json.loads(
+ json.dumps( compare_pr_coverage.coverage_contract.METRIC_SEMANTICS )
+ ),
+ "object_selection": json.loads(
+ json.dumps( compare_pr_coverage.coverage_contract.OBJECT_SELECTION )
+ ),
+ }
+ measurement["contract_fingerprint"] = (
+ compare_pr_coverage.coverage_contract.compute_contract_fingerprint(
+ scope, document["excluded_regex"], measurement
+ )
+ )
+ document["measurement"] = measurement
+ document["per_file_metrics"] = [
+ {
+ "path": "src/coreComponents/New.cpp",
+ "metrics": json.loads( json.dumps( document["metrics"] ) ),
+ }
+ ]
+ document["top_branch_gaps"] = [
+ {
+ "path": "src/coreComponents/New.cpp",
+ **document["metrics"]["branches"],
+ }
+ ]
+ return document
+
+
+class ComparePrCoverageTest( unittest.TestCase ):
+ def setUp( self ) -> None:
+ self.fixture = GitFixture()
+
+ def tearDown( self ) -> None:
+ self.fixture.close()
+
+ def write_json( self, name: str, document: dict ) -> pathlib.Path:
+ path = pathlib.Path( self.fixture.temporary.name ) / name
+ path.write_text( json.dumps( document ), encoding="utf-8" )
+ return path
+
+ def write_lcov( self, contents: str ) -> pathlib.Path:
+ path = pathlib.Path( self.fixture.temporary.name ) / "native.info"
+ path.write_text( contents, encoding="utf-8" )
+ return path
+
+ def make_added_source( self, path: str = "src/coreComponents/New.cpp" ) -> tuple:
+ self.fixture.write( "README", "base\n" )
+ base_sha = self.fixture.commit( "base" )
+ source = self.fixture.write(
+ path,
+ "int uncovered() {\n"
+ " if ( flag ) {\n"
+ " return 1;\n"
+ " }\n"
+ " return 0;\n"
+ "}\n",
+ )
+ head_sha = self.fixture.commit( "add source" )
+ return base_sha, head_sha, source
+
+ def candidate_summary(
+ self, head_sha: str, *, schema_version: int = 3, offset: int = 0
+ ) -> pathlib.Path:
+ return self.write_json(
+ "candidate.json",
+ summary_document(
+ head_sha,
+ self.fixture.tree( head_sha ),
+ schema_version=schema_version,
+ covered_offset=offset,
+ ),
+ )
+
+ def test_patch_metrics_and_actionable_locations( self ) -> None:
+ base_sha, head_sha, source = self.make_added_source()
+ lcov = self.write_lcov(
+ f"SF:{source}\n"
+ "FN:1,uncovered\n"
+ "FNDA:0,uncovered\n"
+ "DA:1,0\n"
+ "DA:2,1\n"
+ "DA:3,0\n"
+ "DA:5,1\n"
+ "BRDA:2,0,0,1\n"
+ "BRDA:2,0,1,0\n"
+ "end_of_record\n"
+ )
+ report, markdown = compare_pr_coverage.analyze(
+ self.fixture.repository,
+ base_sha,
+ head_sha,
+ lcov,
+ self.candidate_summary( head_sha ),
+ )
+
+ patch = report["patch"]
+ self.assertEqual( patch["changed_files"], 1 )
+ self.assertEqual( patch["changed_lines"], 6 )
+ self.assertEqual(
+ patch["metrics"]["executable_lines"], metric( 2, 4 )
+ )
+ self.assertEqual(
+ patch["metrics"]["function_start_sites"], metric( 0, 1 )
+ )
+ self.assertEqual(
+ patch["metrics"]["native_branch_outcomes"], metric( 1, 2 )
+ )
+ self.assertEqual( patch["non_instrumented_changed_lines"], 2 )
+ self.assertIn( "Add cases that exercise the remaining", markdown )
+ self.assertIn( "Advisory and instantiation-weighted", markdown )
+ self.assertFalse( report["aggregate_comparison"]["comparable"] )
+ self.assertIn(
+ "No exact-base", report["aggregate_comparison"]["reason"]
+ )
+
+ def test_exact_baseline_comparison( self ) -> None:
+ base_sha, head_sha, source = self.make_added_source()
+ lcov = self.write_lcov(
+ f"SF:{source}\nDA:1,1\nend_of_record\n"
+ )
+ baseline = self.write_json(
+ "baseline.json",
+ summary_document( base_sha, self.fixture.tree( base_sha ) ),
+ )
+ candidate = self.candidate_summary( head_sha, offset=5 )
+ report, markdown = compare_pr_coverage.analyze(
+ self.fixture.repository,
+ base_sha,
+ head_sha,
+ lcov,
+ candidate,
+ baseline,
+ )
+
+ comparison = report["aggregate_comparison"]
+ self.assertTrue( comparison["comparable"] )
+ self.assertEqual( comparison["metrics"]["branches"]["covered_delta"], 5 )
+ self.assertEqual(
+ comparison["metrics"]["branches"]["not_covered_delta"], -5
+ )
+ self.assertEqual(
+ comparison["metrics"]["branches"]["percentage_point_delta"], 5.0
+ )
+ self.assertIn( "| Canonical branches |", markdown )
+ self.assertIn( "+5.00 pp", markdown )
+ self.assertIn( "##### Canonical movement in changed files", markdown )
+ self.assertIn( "| src/coreComponents/New.cpp", markdown )
+
+ def test_incompatible_baseline_is_explicitly_not_comparable( self ) -> None:
+ base_sha, head_sha, source = self.make_added_source()
+ lcov = self.write_lcov(
+ f"SF:{source}\nDA:1,1\nend_of_record\n"
+ )
+ baseline = self.write_json(
+ "baseline.json",
+ summary_document(
+ base_sha,
+ self.fixture.tree( base_sha ),
+ image_digit="5",
+ ),
+ )
+ report, markdown = compare_pr_coverage.analyze(
+ self.fixture.repository,
+ base_sha,
+ head_sha,
+ lcov,
+ self.candidate_summary( head_sha ),
+ baseline,
+ )
+
+ comparison = report["aggregate_comparison"]
+ self.assertFalse( comparison["comparable"] )
+ self.assertIn( "fingerprints differ", comparison["reason"] )
+ self.assertIn( "ℹ️ N/A", markdown )
+
+ def test_tampered_v3_baseline_is_rejected_by_contract_validator( self ) -> None:
+ base_sha, head_sha, source = self.make_added_source()
+ lcov = self.write_lcov( f"SF:{source}\nDA:1,1\nend_of_record\n" )
+ baseline_document = summary_document(
+ base_sha, self.fixture.tree( base_sha )
+ )
+ baseline_document["measurement"]["container"]["image_id"] = (
+ f"sha256:{'9' * 64}"
+ )
+ baseline = self.write_json( "baseline.json", baseline_document )
+ report, _ = compare_pr_coverage.analyze(
+ self.fixture.repository,
+ base_sha,
+ head_sha,
+ lcov,
+ self.candidate_summary( head_sha ),
+ baseline,
+ )
+
+ comparison = report["aggregate_comparison"]
+ self.assertFalse( comparison["comparable"] )
+ self.assertIn( "baseline summary is invalid", comparison["reason"] )
+ self.assertIn( "fingerprint", comparison["reason"] )
+
+ def test_legacy_v2_summaries_make_exact_delta_unavailable( self ) -> None:
+ base_sha, head_sha, source = self.make_added_source()
+ lcov = self.write_lcov(
+ f"SF:{source}\nDA:1,1\nend_of_record\n"
+ )
+ baseline = self.write_json(
+ "baseline.json",
+ summary_document(
+ base_sha, self.fixture.tree( base_sha ), schema_version=2
+ ),
+ )
+ report, _ = compare_pr_coverage.analyze(
+ self.fixture.repository,
+ base_sha,
+ head_sha,
+ lcov,
+ self.candidate_summary( head_sha, schema_version=2 ),
+ baseline,
+ )
+
+ comparison = report["aggregate_comparison"]
+ self.assertFalse( comparison["comparable"] )
+ self.assertIn( "schema-3 measurement provenance", comparison["reason"] )
+
+ def test_rename_uses_only_modified_head_lines( self ) -> None:
+ original = "\n".join( f"int value_{index} = {index};" for index in range( 10 ) )
+ self.fixture.write( "src/coreComponents/Old.cpp", original + "\n" )
+ base_sha = self.fixture.commit( "base" )
+ run_command(
+ self.fixture.repository,
+ "git",
+ "mv",
+ "src/coreComponents/Old.cpp",
+ "src/coreComponents/New.cpp",
+ )
+ updated = original.replace( "int value_4 = 4;", "int value_4 = 40;" )
+ self.fixture.write( "src/coreComponents/New.cpp", updated + "\n" )
+ head_sha = self.fixture.commit( "rename and edit" )
+
+ records = compare_pr_coverage.changed_line_ranges(
+ self.fixture.repository, base_sha, head_sha
+ )
+ self.assertEqual( len( records ), 1 )
+ self.assertEqual( records[0]["status"], "R" )
+ self.assertEqual( records[0]["old_path"], "src/coreComponents/Old.cpp" )
+ self.assertEqual( records[0]["path"], "src/coreComponents/New.cpp" )
+ self.assertEqual( records[0]["lines"], frozenset( ( 5, ) ) )
+
+ def test_noninstrumented_only_patch_has_empty_executable_metric( self ) -> None:
+ source = self.fixture.write(
+ "src/coreComponents/Comment.cpp",
+ "// old comment\nint stable() { return 1; }\n",
+ )
+ base_sha = self.fixture.commit( "base" )
+ self.fixture.write(
+ "src/coreComponents/Comment.cpp",
+ "// new comment\nint stable() { return 1; }\n",
+ )
+ head_sha = self.fixture.commit( "comment only" )
+ lcov = self.write_lcov(
+ f"SF:{source}\n"
+ "FN:2,stable\nFNDA:1,stable\nDA:2,1\nend_of_record\n"
+ )
+ report, markdown = compare_pr_coverage.analyze(
+ self.fixture.repository,
+ base_sha,
+ head_sha,
+ lcov,
+ self.candidate_summary( head_sha ),
+ )
+
+ lines = report["patch"]["metrics"]["executable_lines"]
+ self.assertEqual( lines["total"], 0 )
+ self.assertIsNone( lines["percent"] )
+ self.assertEqual( report["patch"]["non_instrumented_changed_lines"], 1 )
+ self.assertIn( "| Executable changed lines | 0 | 0 | 0 | N/A |", markdown )
+
+ def test_test_sources_are_excluded_from_production_patch_totals( self ) -> None:
+ self.fixture.write( "README", "base\n" )
+ base_sha = self.fixture.commit( "base" )
+ self.fixture.write(
+ "src/coreComponents/unitTests/testFeature.cpp",
+ "TEST( Feature, Covered ) {}\n",
+ )
+ head_sha = self.fixture.commit( "add unit test" )
+ lcov = self.write_lcov( "" )
+ report, markdown = compare_pr_coverage.analyze(
+ self.fixture.repository,
+ base_sha,
+ head_sha,
+ lcov,
+ self.candidate_summary( head_sha ),
+ )
+
+ patch = report["patch"]
+ self.assertEqual( patch["changed_files"], 0 )
+ self.assertEqual( patch["changed_lines"], 0 )
+ self.assertEqual( patch["excluded_changed_files"], 1 )
+ self.assertEqual( patch["excluded_changed_lines"], 1 )
+ self.assertIn( "test/example/support files", markdown )
+
+ def test_markdown_escapes_paths_and_function_names( self ) -> None:
+ path = "src/coreComponents/evil|.cpp"
+ base_sha, head_sha, source = self.make_added_source( path )
+ lcov = self.write_lcov(
+ f"SF:{source}\n"
+ "FN:1,bad|\nFNDA:0,bad|\nDA:1,0\nend_of_record\n"
+ )
+ _, markdown = compare_pr_coverage.analyze(
+ self.fixture.repository,
+ base_sha,
+ head_sha,
+ lcov,
+ self.candidate_summary( head_sha ),
+ )
+
+ self.assertIn( "evil|<x>.cpp", markdown )
+ self.assertIn( "bad|<T>", markdown )
+ self.assertNotIn( "evil|.cpp", markdown )
+ self.assertNotIn( "bad|", markdown )
+
+ def test_cli_writes_compact_outputs_and_rejects_short_sha( self ) -> None:
+ base_sha, head_sha, source = self.make_added_source()
+ lcov = self.write_lcov(
+ f"SF:{source}\nDA:1,1\nend_of_record\n"
+ )
+ candidate = self.candidate_summary( head_sha )
+ output_json = pathlib.Path( self.fixture.temporary.name ) / "diff.json"
+ output_markdown = pathlib.Path( self.fixture.temporary.name ) / "diff.md"
+ result = subprocess.run(
+ [
+ sys.executable,
+ str( SCRIPT ),
+ "--repository",
+ str( self.fixture.repository ),
+ "--base-sha",
+ base_sha,
+ "--head-sha",
+ head_sha,
+ "--native-info",
+ str( lcov ),
+ "--candidate-summary",
+ str( candidate ),
+ "--output-json",
+ str( output_json ),
+ "--output-markdown",
+ str( output_markdown ),
+ ],
+ check=False,
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+ self.assertEqual( result.returncode, 0, result.stderr )
+ self.assertEqual( result.stdout, "" )
+ self.assertEqual( json.loads( output_json.read_text() )["schema_version"], 1 )
+ self.assertIn( "Pull-request coverage", output_markdown.read_text() )
+ self.assertLess( output_json.stat().st_size, 20_000 )
+ self.assertEqual( stat.S_IMODE( output_json.stat().st_mode ), 0o644 )
+ self.assertEqual( stat.S_IMODE( output_markdown.stat().st_mode ), 0o644 )
+
+ invalid = subprocess.run(
+ [
+ sys.executable,
+ str( SCRIPT ),
+ "--repository",
+ str( self.fixture.repository ),
+ "--base-sha",
+ base_sha[:12],
+ "--head-sha",
+ head_sha,
+ "--native-info",
+ str( lcov ),
+ "--candidate-summary",
+ str( candidate ),
+ ],
+ check=False,
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+ self.assertEqual( invalid.returncode, 2 )
+ self.assertIn( "full 40-character Git SHA", invalid.stderr )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/scripts/tests/test_fetch_coverage_baseline.py b/scripts/tests/test_fetch_coverage_baseline.py
new file mode 100755
index 00000000000..b441e7e8958
--- /dev/null
+++ b/scripts/tests/test_fetch_coverage_baseline.py
@@ -0,0 +1,739 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-only
+
+import contextlib
+import importlib.util
+import io
+import json
+import pathlib
+import stat
+import tempfile
+import unittest
+import urllib.error
+import urllib.parse
+import urllib.request
+import warnings
+import zipfile
+from unittest import mock
+
+
+SCRIPT = pathlib.Path( __file__ ).parents[1] / "fetch_coverage_baseline.py"
+SPEC = importlib.util.spec_from_file_location( "coverage_baseline", SCRIPT )
+coverage_baseline = importlib.util.module_from_spec( SPEC )
+SPEC.loader.exec_module( coverage_baseline )
+CHECKER_TEST_SCRIPT = pathlib.Path( __file__ ).with_name(
+ "test_check_coverage_thresholds.py"
+)
+CHECKER_TEST_SPEC = importlib.util.spec_from_file_location(
+ "coverage_threshold_fixtures", CHECKER_TEST_SCRIPT
+)
+coverage_threshold_fixtures = importlib.util.module_from_spec( CHECKER_TEST_SPEC )
+CHECKER_TEST_SPEC.loader.exec_module( coverage_threshold_fixtures )
+
+BASE_SHA = "0123456789abcdef0123456789abcdef01234567"
+OTHER_SHA = "fedcba9876543210fedcba9876543210fedcba98"
+PREFIX = "llvm-source-coverage-baseline-"
+
+
+class FakeResponse:
+ def __init__(
+ self,
+ payload,
+ *,
+ status=200,
+ content_type="application/json",
+ content_length=True,
+ final_url=None,
+ max_read_size=None,
+ read_error=None,
+ ):
+ self._stream = io.BytesIO( payload )
+ self.status = status
+ self.headers = { "Content-Type": content_type }
+ if content_length:
+ self.headers["Content-Length"] = str( len( payload ) )
+ self.final_url = final_url
+ self.max_read_size = max_read_size
+ self.read_error = read_error
+
+ def __enter__( self ):
+ return self
+
+ def __exit__( self, exception_type, exception, traceback ):
+ return False
+
+ def getcode( self ):
+ return self.status
+
+ def geturl( self ):
+ return self.final_url
+
+ def read( self, size=-1 ):
+ if self.read_error is not None:
+ raise self.read_error
+ if self.max_read_size is not None and (
+ size < 0 or size > self.max_read_size
+ ):
+ size = self.max_read_size
+ return self._stream.read( size )
+
+
+class QueueOpener:
+ def __init__( self, responses ):
+ self.responses = list( responses )
+ self.requests = []
+
+ def open( self, request, timeout ):
+ self.requests.append( ( request, timeout ) )
+ if not self.responses:
+ raise AssertionError( f"unexpected request: {request.full_url}" )
+ response = self.responses.pop( 0 )
+ if isinstance( response, BaseException ):
+ raise response
+ if callable( response ):
+ response = response( request )
+ if response.final_url is None:
+ response.final_url = request.full_url
+ return response
+
+
+def json_response( document, **kwargs ):
+ return FakeResponse( json.dumps( document ).encode( "utf-8" ), **kwargs )
+
+
+def workflow_run( run_id=101, sha=BASE_SHA, **overrides ):
+ document = {
+ "conclusion": "success",
+ "event": "push",
+ "head_branch": "develop",
+ "head_sha": sha,
+ "id": run_id,
+ "run_attempt": 1,
+ "status": "completed",
+ }
+ document.update( overrides )
+ return document
+
+
+def artifact( artifact_id=202, name=None, expired=False ):
+ return {
+ "expired": expired,
+ "id": artifact_id,
+ "name": name if name is not None else f"{PREFIX}{BASE_SHA}",
+ }
+
+
+def coverage_summary( revision=BASE_SHA, include_revision=True ):
+ document = (
+ coverage_threshold_fixtures.summary()
+ if include_revision
+ else { "schema_version": 2 }
+ )
+ if include_revision:
+ document["measurement"]["commit_sha"] = revision
+ return json.dumps( document, sort_keys=True ).encode( "utf-8" )
+
+
+def make_zip( entries ):
+ output = io.BytesIO()
+ with warnings.catch_warnings():
+ warnings.simplefilter( "ignore", UserWarning )
+ with zipfile.ZipFile( output, "w" ) as bundle:
+ for entry in entries:
+ if len( entry ) == 2:
+ name, payload = entry
+ bundle.writestr( name, payload, compress_type=zipfile.ZIP_DEFLATED )
+ else:
+ name, payload, mode = entry
+ information = zipfile.ZipInfo( name )
+ information.create_system = 3
+ information.external_attr = mode << 16
+ information.compress_type = zipfile.ZIP_DEFLATED
+ bundle.writestr( information, payload )
+ return output.getvalue()
+
+
+def successful_responses( archive, *, artifact_document=None ):
+ artifacts = (
+ [artifact()] if artifact_document is None else artifact_document
+ )
+ return [
+ json_response( { "total_count": 1, "workflow_runs": [workflow_run()] } ),
+ json_response( { "artifacts": artifacts, "total_count": len( artifacts ) } ),
+ FakeResponse( archive, content_type="application/zip" ),
+ ]
+
+
+def cli_arguments( output_dir ):
+ return [
+ "--repository",
+ "LLNL/GEOS",
+ "--workflow",
+ "ci_tests.yml",
+ "--base-sha",
+ BASE_SHA,
+ "--artifact-name-prefix",
+ PREFIX,
+ "--token-env",
+ "TEST_TOKEN",
+ "--output-dir",
+ str( output_dir ),
+ ]
+
+
+def run_main( opener, output_dir, environment=None ):
+ standard_output = io.StringIO()
+ standard_error = io.StringIO()
+ if environment is None:
+ environment = { "TEST_TOKEN": "secret-token" }
+ with contextlib.redirect_stdout( standard_output ), contextlib.redirect_stderr(
+ standard_error
+ ):
+ status = coverage_baseline.main(
+ cli_arguments( output_dir ), environ=environment, opener=opener
+ )
+ return status, standard_output.getvalue(), standard_error.getvalue()
+
+
+class CoverageBaselineFetchTests( unittest.TestCase ):
+ def test_success_fetches_only_exact_flat_files( self ):
+ archive = make_zip(
+ [
+ ( "coverage-summary.json", coverage_summary() ),
+ ( "llvm-summary.json", b'{"data": []}\n' ),
+ ( "toolchain.txt", b"LLVM 20.1.2\n" ),
+ ]
+ )
+ wrong_name = artifact(
+ 200, name=f"{PREFIX}{BASE_SHA}-extra", expired=False
+ )
+ opener = QueueOpener(
+ successful_responses( archive, artifact_document=[wrong_name, artifact()] )
+ )
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ output_dir = pathlib.Path( temporary_directory )
+ ( output_dir / "toolchain.txt" ).write_text( "stale", encoding="utf-8" )
+ status, standard_output, standard_error = run_main( opener, output_dir )
+
+ self.assertEqual( status, 0 )
+ self.assertEqual( standard_error, "" )
+ self.assertIn( f"{PREFIX}{BASE_SHA}", standard_output )
+ self.assertEqual(
+ json.loads(
+ ( output_dir / "coverage-summary.json" ).read_text(
+ encoding="utf-8"
+ )
+ )["measurement"]["commit_sha"],
+ BASE_SHA,
+ )
+ self.assertEqual(
+ ( output_dir / "toolchain.txt" ).read_text( encoding="utf-8" ),
+ "LLVM 20.1.2\n",
+ )
+ status_document = json.loads(
+ ( output_dir / coverage_baseline.STATUS_FILE ).read_text(
+ encoding="utf-8"
+ )
+ )
+ self.assertEqual( status_document["status"], "found" )
+ self.assertEqual( status_document["run_id"], 101 )
+ self.assertEqual( status_document["artifact_id"], 202 )
+
+ self.assertEqual( len( opener.requests ), 3 )
+ runs_url = urllib.parse.urlsplit( opener.requests[0][0].full_url )
+ query = urllib.parse.parse_qs( runs_url.query )
+ self.assertEqual( query["head_sha"], [BASE_SHA] )
+ self.assertEqual( query["branch"], ["develop"] )
+ self.assertEqual( query["event"], ["push"] )
+ self.assertEqual( query["status"], ["completed"] )
+ self.assertEqual(
+ opener.requests[2][0].full_url,
+ "https://api.github.com/repos/LLNL/GEOS/actions/artifacts/202/zip",
+ )
+ self.assertEqual(
+ opener.requests[0][0].get_header( "Authorization" ),
+ "Bearer secret-token",
+ )
+
+ def test_schema_without_embedded_revision_is_accepted( self ):
+ archive = make_zip(
+ [
+ (
+ "coverage-summary.json",
+ coverage_summary( include_revision=False ),
+ )
+ ]
+ )
+ files = coverage_baseline.read_safe_archive( archive, BASE_SHA )
+ self.assertEqual( set( files ), { "coverage-summary.json" } )
+
+ def test_schema_three_requires_embedded_revision( self ):
+ document = coverage_threshold_fixtures.summary()
+ del document["measurement"]
+ archive = make_zip(
+ [( "coverage-summary.json", json.dumps( document ).encode( "utf-8" ) )]
+ )
+ with self.assertRaisesRegex(
+ coverage_baseline.BaselineFetchError, "measurement"
+ ):
+ coverage_baseline.read_safe_archive( archive, BASE_SHA )
+
+ def test_missing_run_is_exit_three_and_clears_stale_outputs( self ):
+ opener = QueueOpener(
+ [json_response( { "total_count": 0, "workflow_runs": [] } )]
+ )
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ output_dir = pathlib.Path( temporary_directory )
+ ( output_dir / "coverage-summary.json" ).write_text(
+ "stale", encoding="utf-8"
+ )
+ status, _, standard_error = run_main( opener, output_dir )
+ self.assertEqual( status, 3 )
+ self.assertIn( "unavailable", standard_error )
+ self.assertFalse( ( output_dir / "coverage-summary.json" ).exists() )
+ reason = json.loads(
+ ( output_dir / coverage_baseline.STATUS_FILE ).read_text(
+ encoding="utf-8"
+ )
+ )
+ self.assertEqual( reason["status"], "missing" )
+ self.assertEqual( reason["reason_code"], "workflow_run_missing" )
+ self.assertEqual( reason["base_sha"], BASE_SHA )
+
+ def test_expired_or_inexact_artifact_is_exit_three( self ):
+ cases = (
+ (
+ [artifact( expired=True )],
+ "artifact_expired",
+ ),
+ (
+ [artifact( name=f"{PREFIX}{BASE_SHA}-not-exact" )],
+ "artifact_missing",
+ ),
+ )
+ for artifacts, reason_code in cases:
+ with self.subTest( reason_code=reason_code ):
+ opener = QueueOpener(
+ [
+ json_response(
+ { "total_count": 1, "workflow_runs": [workflow_run()] }
+ ),
+ json_response(
+ { "artifacts": artifacts, "total_count": len( artifacts ) }
+ ),
+ ]
+ )
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ output_dir = pathlib.Path( temporary_directory )
+ status, _, _ = run_main( opener, output_dir )
+ self.assertEqual( status, 3 )
+ reason = json.loads(
+ ( output_dir / coverage_baseline.STATUS_FILE ).read_text(
+ encoding="utf-8"
+ )
+ )
+ self.assertEqual( reason["reason_code"], reason_code )
+
+ def test_download_404_is_a_missing_baseline( self ):
+ download_url = (
+ "https://api.github.com/repos/LLNL/GEOS/actions/artifacts/202/zip"
+ )
+ missing_responses = (
+ urllib.error.HTTPError( download_url, 404, "missing", {}, None ),
+ FakeResponse( b"", status=410, content_type="application/zip" ),
+ )
+ for missing_response in missing_responses:
+ with self.subTest( response=missing_response ):
+ opener = QueueOpener(
+ [
+ json_response(
+ {
+ "total_count": 1,
+ "workflow_runs": [workflow_run()],
+ }
+ ),
+ json_response(
+ { "artifacts": [artifact()], "total_count": 1 }
+ ),
+ missing_response,
+ ]
+ )
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ output_dir = pathlib.Path( temporary_directory )
+ status, _, _ = run_main( opener, output_dir )
+ self.assertEqual( status, 3 )
+ reason = json.loads(
+ ( output_dir / coverage_baseline.STATUS_FILE ).read_text(
+ encoding="utf-8"
+ )
+ )
+ self.assertEqual(
+ reason["reason_code"], "artifact_download_missing"
+ )
+
+ def test_http_and_json_failures_are_exit_two( self ):
+ runs_url = (
+ "https://api.github.com/repos/LLNL/GEOS/actions/workflows/"
+ "ci_tests.yml/runs"
+ )
+ cases = (
+ QueueOpener(
+ [urllib.error.HTTPError( runs_url, 403, "forbidden", {}, None )]
+ ),
+ QueueOpener(
+ [FakeResponse( b"not json", content_type="application/json" )]
+ ),
+ QueueOpener(
+ [json_response( { "workflow_runs": [], "total_count": "zero" } )]
+ ),
+ )
+ for opener in cases:
+ with self.subTest( opener=opener ):
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ status, _, standard_error = run_main(
+ opener, pathlib.Path( temporary_directory )
+ )
+ self.assertEqual( status, 2 )
+ self.assertIn( "coverage baseline error", standard_error )
+
+ def test_response_content_type_final_url_and_size_are_strict( self ):
+ document = { "total_count": 0, "workflow_runs": [] }
+ cases = (
+ json_response( document, content_type="text/plain" ),
+ json_response( document, final_url="https://example.com/runs" ),
+ json_response(
+ document,
+ final_url="https://api.github.com:invalid/runs",
+ ),
+ FakeResponse(
+ b"{}",
+ content_type="application/json",
+ content_length=False,
+ ),
+ )
+ cases[3].headers["Content-Length"] = str(
+ coverage_baseline.JSON_RESPONSE_LIMIT + 1
+ )
+ for response in cases:
+ with self.subTest( response=response ):
+ client = coverage_baseline.GitHubClient(
+ "token", opener=QueueOpener( [response] )
+ )
+ with self.assertRaises( coverage_baseline.BaselineFetchError ):
+ client.get_json( "/repos/LLNL/GEOS/actions/runs" )
+
+ def test_partial_reads_are_completed_and_read_failures_are_wrapped( self ):
+ document = { "total_count": 0, "workflow_runs": [] }
+ client = coverage_baseline.GitHubClient(
+ "token",
+ opener=QueueOpener(
+ [json_response( document, max_read_size=3 )]
+ ),
+ )
+ self.assertEqual(
+ client.get_json( "/repos/LLNL/GEOS/actions/runs" ), document
+ )
+
+ client = coverage_baseline.GitHubClient(
+ "token",
+ opener=QueueOpener(
+ [FakeResponse( b"", read_error=OSError( "read failed" ) )]
+ ),
+ )
+ with self.assertRaisesRegex(
+ coverage_baseline.BaselineFetchError, "response failed"
+ ):
+ client.get_json( "/repos/LLNL/GEOS/actions/runs" )
+
+ def test_latest_exact_completed_develop_push_is_selected( self ):
+ runs = [
+ workflow_run( 100, conclusion="failure" ),
+ workflow_run( 101, head_branch="other" ),
+ workflow_run( 102, sha=OTHER_SHA ),
+ workflow_run( 103, run_attempt=2 ),
+ workflow_run( 104, run_attempt=1 ),
+ ]
+ client = coverage_baseline.GitHubClient(
+ "token",
+ opener=QueueOpener(
+ [json_response( { "total_count": len( runs ), "workflow_runs": runs } )]
+ ),
+ )
+ selected = coverage_baseline.find_exact_run(
+ client, "/repos/LLNL/GEOS", "ci_tests.yml", BASE_SHA
+ )
+ self.assertEqual( selected["id"], 104 )
+
+ def test_pagination_finds_an_exact_run_on_a_later_page( self ):
+ with mock.patch.object( coverage_baseline, "PER_PAGE", 2 ):
+ opener = QueueOpener(
+ [
+ json_response(
+ {
+ "total_count": 3,
+ "workflow_runs": [
+ workflow_run( 1, sha=OTHER_SHA ),
+ workflow_run( 2, sha=OTHER_SHA ),
+ ],
+ }
+ ),
+ json_response(
+ {
+ "total_count": 3,
+ "workflow_runs": [workflow_run( 3 )],
+ }
+ ),
+ ]
+ )
+ client = coverage_baseline.GitHubClient( "token", opener=opener )
+ selected = coverage_baseline.find_exact_run(
+ client, "/repos/LLNL/GEOS", "12345", BASE_SHA
+ )
+ self.assertEqual( selected["id"], 3 )
+ second_query = urllib.parse.parse_qs(
+ urllib.parse.urlsplit( opener.requests[1][0].full_url ).query
+ )
+ self.assertEqual( second_query["page"], ["2"] )
+
+ def test_pagination_rejects_a_short_incomplete_response( self ):
+ client = coverage_baseline.GitHubClient(
+ "token",
+ opener=QueueOpener(
+ [
+ json_response(
+ {
+ "total_count": 2,
+ "workflow_runs": [workflow_run()],
+ }
+ )
+ ]
+ ),
+ )
+ with self.assertRaisesRegex(
+ coverage_baseline.BaselineFetchError, "before total_count"
+ ):
+ coverage_baseline.find_exact_run(
+ client, "/repos/LLNL/GEOS", "ci_tests.yml", BASE_SHA
+ )
+
+ def test_duplicate_exact_artifacts_are_rejected( self ):
+ client = coverage_baseline.GitHubClient(
+ "token",
+ opener=QueueOpener(
+ [
+ json_response(
+ {
+ "artifacts": [artifact( 1 ), artifact( 2 )],
+ "total_count": 2,
+ }
+ )
+ ]
+ ),
+ )
+ with self.assertRaisesRegex(
+ coverage_baseline.BaselineFetchError, "multiple"
+ ):
+ coverage_baseline.find_exact_artifact(
+ client,
+ "/repos/LLNL/GEOS",
+ 10,
+ f"{PREFIX}{BASE_SHA}",
+ )
+
+ def test_zip_rejects_unexpected_traversal_duplicate_and_missing_entries( self ):
+ valid_summary = coverage_summary()
+ archives = (
+ make_zip( [( "../coverage-summary.json", valid_summary )] ),
+ make_zip(
+ [
+ ( "coverage-summary.json", valid_summary ),
+ ( "unexpected.txt", b"unexpected" ),
+ ]
+ ),
+ make_zip(
+ [
+ ( "coverage-summary.json", valid_summary ),
+ ( "coverage-summary.json", valid_summary ),
+ ]
+ ),
+ make_zip( [( "toolchain.txt", b"LLVM" )] ),
+ )
+ for archive in archives:
+ with self.subTest():
+ with self.assertRaises( coverage_baseline.BaselineFetchError ):
+ coverage_baseline.read_safe_archive( archive, BASE_SHA )
+
+ def test_zip_rejects_symlinks_and_special_files( self ):
+ modes = ( stat.S_IFLNK | 0o777, stat.S_IFIFO | 0o600 )
+ for mode in modes:
+ archive = make_zip(
+ [( "coverage-summary.json", b"target", mode )]
+ )
+ with self.subTest( mode=mode ):
+ with self.assertRaisesRegex(
+ coverage_baseline.BaselineFetchError, "regular file"
+ ):
+ coverage_baseline.read_safe_archive( archive, BASE_SHA )
+
+ dos_directory = zipfile.ZipInfo( "coverage-summary.json" )
+ dos_directory.create_system = 0
+ dos_directory.external_attr = 0x10
+ self.assertFalse( coverage_baseline._is_regular_zip_entry( dos_directory ) )
+
+ unknown_system = zipfile.ZipInfo( "coverage-summary.json" )
+ unknown_system.create_system = 7
+ self.assertFalse( coverage_baseline._is_regular_zip_entry( unknown_system ) )
+
+ def test_zip_rejects_invalid_json_revision_mismatch_and_malformed_zip( self ):
+ archives = (
+ make_zip( [( "coverage-summary.json", b"not json" )] ),
+ make_zip(
+ [( "coverage-summary.json", coverage_summary( OTHER_SHA ) )]
+ ),
+ b"not a zip archive",
+ )
+ for archive in archives:
+ with self.subTest():
+ with self.assertRaises( coverage_baseline.BaselineFetchError ):
+ coverage_baseline.read_safe_archive( archive, BASE_SHA )
+
+ def test_zip_bomb_is_rejected_by_uncompressed_limit( self ):
+ oversized = b" " * ( coverage_baseline.DOWNLOAD_LIMIT + 1 )
+ archive = make_zip( [( "coverage-summary.json", oversized )] )
+ self.assertLess( len( archive ), coverage_baseline.DOWNLOAD_LIMIT )
+ with self.assertRaisesRegex(
+ coverage_baseline.BaselineFetchError, "extraction limit"
+ ):
+ coverage_baseline.read_safe_archive( archive, BASE_SHA )
+
+ def test_download_body_is_limited_even_without_content_length( self ):
+ oversized = b"x" * ( coverage_baseline.DOWNLOAD_LIMIT + 1 )
+ response = FakeResponse(
+ oversized,
+ content_type="application/zip",
+ content_length=False,
+ final_url="https://objects.example.test/artifact.zip",
+ )
+ client = coverage_baseline.GitHubClient(
+ "token", opener=QueueOpener( [response] )
+ )
+ with self.assertRaisesRegex(
+ coverage_baseline.BaselineFetchError, "download limit"
+ ):
+ client.download_artifact( "/repos/LLNL/GEOS", 1 )
+
+ def test_download_response_type_and_url_are_strict( self ):
+ responses = (
+ FakeResponse( b"zip", content_type="text/plain" ),
+ FakeResponse(
+ b"zip",
+ content_type="application/zip",
+ final_url="http://objects.example.test/artifact.zip",
+ ),
+ )
+ for response in responses:
+ with self.subTest( response=response ):
+ client = coverage_baseline.GitHubClient(
+ "token", opener=QueueOpener( [response] )
+ )
+ with self.assertRaises( coverage_baseline.BaselineFetchError ):
+ client.download_artifact( "/repos/LLNL/GEOS", 1 )
+
+ def test_cross_origin_redirect_strips_authorization( self ):
+ handler = coverage_baseline.SafeRedirectHandler()
+ request = urllib.request.Request(
+ "https://api.github.com/repos/LLNL/GEOS/actions/artifacts/1/zip",
+ headers={
+ "Authorization": "Bearer secret",
+ "Cookie": "session=secret",
+ "Proxy-Authorization": "Basic secret",
+ "X-GitHub-Api-Version": coverage_baseline.API_VERSION,
+ "User-Agent": "test",
+ },
+ )
+ redirected = handler.redirect_request(
+ request,
+ None,
+ 302,
+ "Found",
+ {},
+ "https://objects.example.test/artifact.zip",
+ )
+ headers = {
+ name.lower(): value for name, value in redirected.header_items()
+ }
+ self.assertNotIn( "authorization", headers )
+ self.assertNotIn( "cookie", headers )
+ self.assertNotIn( "proxy-authorization", headers )
+ self.assertNotIn( "x-github-api-version", headers )
+ self.assertEqual( headers["user-agent"], "test" )
+ unsafe_urls = (
+ "http://example.test/artifact.zip",
+ "https://@objects.example.test/artifact.zip",
+ "https://api.github.com:invalid/artifact.zip",
+ "https://[malformed/artifact.zip",
+ )
+ for unsafe_url in unsafe_urls:
+ with self.subTest( unsafe_url=unsafe_url ):
+ with self.assertRaises( coverage_baseline.BaselineFetchError ):
+ handler.redirect_request(
+ request, None, 302, "Found", {}, unsafe_url
+ )
+
+ def test_unsafe_inputs_and_output_symlink_are_exit_two( self ):
+ invalid_argument_sets = (
+ ( "--repository", "../GEOS" ),
+ ( "--workflow", "../ci_tests.yml" ),
+ ( "--base-sha", "short" ),
+ ( "--artifact-name-prefix", "bad/name" ),
+ ( "--token-env", "BAD-NAME" ),
+ )
+ for option, value in invalid_argument_sets:
+ with self.subTest( option=option ):
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ arguments = cli_arguments( pathlib.Path( temporary_directory ) )
+ arguments[arguments.index( option ) + 1] = value
+ with contextlib.redirect_stderr( io.StringIO() ):
+ status = coverage_baseline.main(
+ arguments,
+ environ={ "TEST_TOKEN": "token", "BAD-NAME": "token" },
+ opener=QueueOpener( [] ),
+ )
+ self.assertEqual( status, 2 )
+
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ root = pathlib.Path( temporary_directory )
+ target = root / "target"
+ target.mkdir()
+ output_link = root / "output"
+ output_link.symlink_to( target, target_is_directory=True )
+ with contextlib.redirect_stderr( io.StringIO() ):
+ status = coverage_baseline.main(
+ cli_arguments( output_link ),
+ environ={ "TEST_TOKEN": "token" },
+ opener=QueueOpener( [] ),
+ )
+ self.assertEqual( status, 2 )
+
+ def test_missing_token_is_exit_two_without_an_http_request( self ):
+ environments = (
+ {},
+ { "TEST_TOKEN": "token\nInjected: value" },
+ { "TEST_TOKEN": "token\0suffix" },
+ )
+ for environment in environments:
+ with self.subTest( environment=environment ):
+ opener = QueueOpener( [] )
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ status, _, standard_error = run_main(
+ opener,
+ pathlib.Path( temporary_directory ),
+ environment=environment,
+ )
+ self.assertEqual( status, 2 )
+ self.assertIn( "missing or empty", standard_error )
+ self.assertEqual( opener.requests, [] )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/scripts/tests/test_render_coverage_report_html.py b/scripts/tests/test_render_coverage_report_html.py
new file mode 100644
index 00000000000..0631c1766ac
--- /dev/null
+++ b/scripts/tests/test_render_coverage_report_html.py
@@ -0,0 +1,135 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-only
+
+import importlib.util
+import json
+import pathlib
+import tempfile
+import unittest
+
+
+SCRIPT = pathlib.Path( __file__ ).parents[1] / "render_coverage_report_html.py"
+SPEC = importlib.util.spec_from_file_location( "coverage_html", SCRIPT )
+coverage_html = importlib.util.module_from_spec( SPEC )
+SPEC.loader.exec_module( coverage_html )
+
+
+def metric( covered: int, total: int ) -> dict:
+ return {
+ "covered": covered,
+ "not_covered": total - covered,
+ "total": total,
+ "percent": round( 100.0 * covered / total, 6 ) if total else 0.0,
+ }
+
+
+def summary() -> dict:
+ return {
+ "schema_version": 3,
+ "scope": "src/coreComponents",
+ "tool": { "name": "llvm-cov", "major": 20 },
+ "metrics": {
+ "lines": metric( 74, 100 ),
+ "functions": metric( 76, 100 ),
+ "branches": metric( 55, 100 ),
+ "regions": metric( 50, 100 ),
+ },
+ "inputs": {
+ "profiles": 106,
+ "coverage_objects": 29,
+ "zero_hash_mappings": 3,
+ },
+ "supplemental": { "native_branch_outcomes": metric( 7, 100 ) },
+ "top_branch_gaps": [
+ {
+ "path": "src/coreComponents/example.cpp",
+ **metric( 1, 2 ),
+ }
+ ],
+ "measurement": { "commit_sha": "a" * 40 },
+ }
+
+
+def pull_request_report() -> dict:
+ return {
+ "base_sha": "b" * 40,
+ "head_sha": "a" * 40,
+ "scope": "src/coreComponents",
+ "aggregate_comparison": {
+ "comparable": False,
+ "reason": "No exact-base coverage summary was provided.",
+ },
+ "patch": {
+ "changed_files": 1,
+ "changed_lines": 2,
+ "non_instrumented_changed_lines": 0,
+ "metrics": {
+ "executable_lines": metric( 0, 1 ),
+ "function_start_sites": metric( 0, 0 ),
+ "native_branch_outcomes": metric( 0, 0 ),
+ },
+ "top_impacted_files": [],
+ "top_impacted_locations": [
+ {
+ "path": "src/coreComponents/example.cpp",
+ "line": 42,
+ "executable_line": True,
+ "line_covered": False,
+ "native_branch_outcomes": metric( 0, 0 ),
+ "function_start_covered": None,
+ "hint": "Add a focused coverage smoke.",
+ }
+ ],
+ },
+ }
+
+
+class CoverageHtmlTests( unittest.TestCase ):
+ def test_report_is_a_single_self_contained_human_readable_page( self ):
+ with tempfile.TemporaryDirectory() as temporary:
+ artifact_dir = pathlib.Path( temporary )
+ ( artifact_dir / "coverage-summary.json" ).write_text(
+ json.dumps( summary() ), encoding="utf-8"
+ )
+ ( artifact_dir / "pr-coverage.json" ).write_text(
+ json.dumps( pull_request_report() ), encoding="utf-8"
+ )
+ ( artifact_dir / "coverage-status.md" ).write_text(
+ """### ✅ LLVM source coverage passed
+
+| Validation | Result | Details |
+|---|:---:|---|
+| Coverage smoke suite | ✅ Pass | 106/106 passed in 10.00 s |
+
+### Run configuration
+
+| Item | Value |
+|---|---|
+| Toolchain | `Clang 20 / LLVM 20` |
+""",
+ encoding="utf-8",
+ )
+ ( artifact_dir / "ctest.log" ).write_text(
+ "100% tests passed, 0 tests failed out of 106\n", encoding="utf-8"
+ )
+ ( artifact_dir / "mapping-integrity.log" ).write_text(
+ "mapping checks passed\n", encoding="utf-8"
+ )
+ output_path = artifact_dir / "index.html"
+ output_path.write_text( "old report\n", encoding="utf-8" )
+ output_path.chmod( 0o400 )
+ output = coverage_html.render_html( artifact_dir )
+ coverage_html.write_atomic( output_path, output )
+ self.assertIn( "", output_path.read_text() )
+
+ self.assertIn( "", output )
+ self.assertIn( "Enforced policy", output )
+ self.assertIn( "74.00%", output )
+ self.assertIn( "No exact-base coverage summary was provided.", output )
+ self.assertIn( "example.cpp:42", output )
+ self.assertIn( "mapping checks passed", output )
+ self.assertIn( "details", output )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/scripts/tests/test_render_coverage_review_summary.py b/scripts/tests/test_render_coverage_review_summary.py
new file mode 100644
index 00000000000..4fa2b591794
--- /dev/null
+++ b/scripts/tests/test_render_coverage_review_summary.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-only
+
+import importlib.util
+import pathlib
+import unittest
+
+
+SCRIPT = pathlib.Path( __file__ ).parents[1] / "render_coverage_review_summary.py"
+SPEC = importlib.util.spec_from_file_location( "coverage_review_summary", SCRIPT )
+coverage_review_summary = importlib.util.module_from_spec( SPEC )
+SPEC.loader.exec_module( coverage_review_summary )
+
+
+def metric( covered: int, total: int ) -> dict:
+ return {
+ "covered": covered,
+ "not_covered": total - covered,
+ "total": total,
+ "percent": round( 100.0 * covered / total, 6 ),
+ }
+
+
+def summary() -> dict:
+ return {
+ "scope": "src/coreComponents",
+ "metrics": {
+ "regions": metric( 50, 100 ),
+ "functions": metric( 76, 100 ),
+ "lines": metric( 74, 100 ),
+ "branches": metric( 55, 100 ),
+ },
+ }
+
+
+def unavailable_report() -> dict:
+ return {
+ "aggregate_comparison": {
+ "comparable": False,
+ "reason": "No exact-base coverage summary was provided.",
+ },
+ "patch": {
+ "metrics": { "executable_lines": metric( 0, 1 ) },
+ "top_impacted_locations": [
+ {
+ "path": "src/coreComponents/example.cpp",
+ "line": 42,
+ "executable_line": True,
+ "line_covered": False,
+ }
+ ],
+ },
+ }
+
+
+class CoverageReviewSummaryTests( unittest.TestCase ):
+ thresholds = { "lines": 7001, "functions": 7501, "branches": 5001 }
+
+ def test_console_summary_is_plain_text_and_contains_enforced_metrics( self ):
+ rendered = coverage_review_summary.render_console_summary(
+ summary(), self.thresholds
+ )
+
+ self.assertIn( "LLVM source coverage policy", rendered )
+ self.assertIn( "Overall result: ✅ PASS", rendered )
+ self.assertIn( "Lines", rendered )
+ self.assertIn( "74.00%", rendered )
+ self.assertIn( "74 / 100", rendered )
+ self.assertIn( ">= 70.01%", rendered )
+ self.assertIn( "Functions", rendered )
+ self.assertIn( "Canonical branches", rendered )
+ self.assertNotIn( "|---", rendered )
+
+ def test_console_summary_marks_a_failed_threshold( self ):
+ failing_summary = summary()
+ failing_summary["metrics"]["lines"]["covered"] = 69
+ failing_summary["metrics"]["lines"]["not_covered"] = 31
+ failing_summary["metrics"]["lines"]["percent"] = 69.0
+
+ rendered = coverage_review_summary.render_console_summary(
+ failing_summary, self.thresholds
+ )
+
+ self.assertIn( "Overall result: ❌ FAIL", rendered )
+ self.assertIn( "❌ Fail", rendered )
+
+ def test_summary_separates_policy_baseline_and_changed_line_signals( self ):
+ rendered = coverage_review_summary.render_summary(
+ summary(),
+ self.thresholds,
+ "✅ Pass",
+ "106/106 passed in 106.37 s",
+ unavailable_report(),
+ )
+
+ self.assertIn( "### Reviewer summary", rendered )
+ self.assertIn( "106/106 passed in 106.37 s", rendered )
+ self.assertIn( "✅ Lines 74.00% (74/100) ≥ 70.01%", rendered )
+ self.assertIn( "✅ Functions 76.00% (76/100) ≥ 75.01%", rendered )
+ self.assertIn( "✅ Canonical branches 55.00% (55/100) ≥ 50.01%", rendered )
+ self.assertIn( "ℹ️ Unavailable", rendered )
+ self.assertIn( "No exact-base baseline artifact was available.", rendered )
+ self.assertIn( "⚠️ Review", rendered )
+ self.assertIn( "example.cpp:42", rendered )
+
+ def test_summary_reports_a_project_coverage_regression( self ):
+ report = unavailable_report()
+ report["aggregate_comparison"] = {
+ "comparable": True,
+ "metrics": {
+ name: { "percentage_point_delta": delta }
+ for name, delta in (
+ ( "lines", -0.10 ),
+ ( "functions", -0.05 ),
+ ( "branches", -0.20 ),
+ ( "regions", -0.01 ),
+ )
+ },
+ }
+
+ rendered = coverage_review_summary.render_summary(
+ summary(), self.thresholds, "✅ Pass", "106/106 passed", report
+ )
+
+ self.assertIn( "⚠️ Decreased", rendered )
+ self.assertIn( "Lines -0.10 pp", rendered )
+ self.assertIn( "Canonical branches -0.20 pp", rendered )
+
+ def test_non_pull_request_summary_has_no_pr_warning( self ):
+ rendered = coverage_review_summary.render_summary(
+ summary(), self.thresholds, "✅ Pass", "106/106 passed", None
+ )
+
+ self.assertIn( "⏭️ Not applicable", rendered )
+ self.assertIn( "not a pull-request coverage run", rendered )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/cmake/GeosxOptions.cmake b/src/cmake/GeosxOptions.cmake
index 3e41db757d2..826495faa36 100644
--- a/src/cmake/GeosxOptions.cmake
+++ b/src/cmake/GeosxOptions.cmake
@@ -6,8 +6,35 @@ message( "CMAKE_HOST_APPLE = ${CMAKE_HOST_APPLE}" )
### OPTIONS ###
option( GEOS_ENABLE_FPE "Enables floating point exceptions" ON )
option( GEOS_ENABLE_TESTS "Enables unit tests" ON )
+option( GEOS_ENABLE_LLVM_SOURCE_COVERAGE
+ "Enables Clang source-based code coverage instrumentation" OFF )
option( ENABLE_CALIPER "Enables Caliper instrumentation" OFF )
+if( GEOS_ENABLE_LLVM_SOURCE_COVERAGE )
+ if( NOT CMAKE_C_COMPILER_ID MATCHES "Clang" OR
+ NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang" )
+ message( FATAL_ERROR
+ "GEOS_ENABLE_LLVM_SOURCE_COVERAGE requires Clang C and C++ compilers." )
+ endif()
+ if( ENABLE_CUDA OR ENABLE_HIP )
+ message( FATAL_ERROR
+ "GEOS_ENABLE_LLVM_SOURCE_COVERAGE currently supports CPU builds only." )
+ endif()
+ if( ENABLE_COVERAGE )
+ message( FATAL_ERROR
+ "GEOS_ENABLE_LLVM_SOURCE_COVERAGE and BLT ENABLE_COVERAGE use "
+ "different instrumentation formats and cannot both be enabled." )
+ endif()
+
+ # Keep this mode separate from BLT's gcov-compatible ENABLE_COVERAGE mode.
+ # Source-based coverage records source conditions rather than compiler CFG
+ # arcs and is consumed by llvm-profdata/llvm-cov.
+ add_compile_options(
+ $<$:-fprofile-instr-generate>
+ $<$:-fcoverage-mapping> )
+ add_link_options( $<$:-fprofile-instr-generate> )
+endif()
+
option( ENABLE_MATHPRESSO "" ON )
option( ENABLE_CHAI "Enables CHAI" ON )
diff --git a/src/coreComponents/dataRepository/unitTests/testPacking.cpp b/src/coreComponents/dataRepository/unitTests/testPacking.cpp
index 32b31f7c1b6..8b24d012986 100644
--- a/src/coreComponents/dataRepository/unitTests/testPacking.cpp
+++ b/src/coreComponents/dataRepository/unitTests/testPacking.cpp
@@ -259,6 +259,210 @@ TEST( testPacking, testPackByIndexDevice )
}
}
+#define CHECK_EXPLICIT_DEVICE_BUFFER_OPS( TYPE, NDIM, PERMUTATION ) \
+ { \
+ using ArrayType = Array< TYPE, NDIM, PERMUTATION >; \
+ localIndex dimensions[NDIM]; \
+ for( localIndex dimension = 0; dimension < NDIM; ++dimension ) \
+ { \
+ dimensions[dimension] = 2; \
+ } \
+ ArrayType source; \
+ source.resize( NDIM, dimensions ); \
+ for( localIndex valueIndex = 0; valueIndex < source.size(); ++valueIndex ) \
+ { \
+ source.data()[valueIndex] = static_cast< double >( valueIndex + 1 ); \
+ } \
+ \
+ buffer_unit_type * nullBuffer = nullptr; \
+ parallelDeviceEvents sizeEvents; \
+ localIndex const fullCalculatedSize = \
+ bufferOps::PackDevice< false >( nullBuffer, source.toViewConst(), sizeEvents ); \
+ EXPECT_EQ( nullBuffer, nullptr ); \
+ EXPECT_GT( fullCalculatedSize, 0 ); \
+ \
+ buffer_type fullBuffer( fullCalculatedSize ); \
+ buffer_unit_type * fullWriteBuffer = fullBuffer.data(); \
+ parallelDeviceEvents fullPackEvents; \
+ localIndex const fullPackedSize = \
+ bufferOps::PackDevice< true >( fullWriteBuffer, source.toViewConst(), fullPackEvents ); \
+ EXPECT_EQ( fullPackedSize, fullCalculatedSize ); \
+ EXPECT_EQ( fullWriteBuffer, fullBuffer.data() + fullPackedSize ); \
+ waitAllDeviceEvents( fullPackEvents ); \
+ \
+ ArrayType fullDestination; \
+ fullDestination.resize( NDIM, dimensions ); \
+ buffer_unit_type const * fullReadBuffer = fullBuffer.data(); \
+ parallelDeviceEvents fullUnpackEvents; \
+ localIndex const fullUnpackedSize = \
+ bufferOps::UnpackDevice( fullReadBuffer, fullDestination.toView(), fullUnpackEvents ); \
+ EXPECT_EQ( fullUnpackedSize, fullPackedSize ); \
+ EXPECT_EQ( fullReadBuffer, fullBuffer.data() + fullUnpackedSize ); \
+ waitAllDeviceEvents( fullUnpackEvents ); \
+ source.move( hostMemorySpace ); \
+ fullDestination.move( hostMemorySpace ); \
+ for( localIndex valueIndex = 0; valueIndex < source.size(); ++valueIndex ) \
+ { \
+ EXPECT_EQ( fullDestination.data()[valueIndex], source.data()[valueIndex] ); \
+ } \
+ \
+ array1d< localIndex > indices( dimensions[0] ); \
+ for( localIndex index = 0; index < indices.size(); ++index ) \
+ { \
+ indices[index] = index; \
+ } \
+ nullBuffer = nullptr; \
+ parallelDeviceEvents indexedSizeEvents; \
+ localIndex const indexedCalculatedSize = bufferOps::PackByIndexDevice< false >( \
+ nullBuffer, source.toViewConst(), indices.toViewConst(), indexedSizeEvents ); \
+ EXPECT_EQ( nullBuffer, nullptr ); \
+ EXPECT_GT( indexedCalculatedSize, 0 ); \
+ \
+ buffer_type indexedBuffer( indexedCalculatedSize ); \
+ buffer_unit_type * indexedWriteBuffer = indexedBuffer.data(); \
+ parallelDeviceEvents indexedPackEvents; \
+ localIndex const indexedPackedSize = bufferOps::PackByIndexDevice< true >( \
+ indexedWriteBuffer, source.toViewConst(), indices.toViewConst(), indexedPackEvents ); \
+ EXPECT_EQ( indexedPackedSize, indexedCalculatedSize ); \
+ localIndex const indexedWrittenSize = indexedWriteBuffer - indexedBuffer.data(); \
+ EXPECT_LE( indexedWrittenSize, indexedPackedSize ); \
+ EXPECT_LT( indexedPackedSize - indexedWrittenSize, static_cast< localIndex >( sizeof( TYPE ) ) ); \
+ waitAllDeviceEvents( indexedPackEvents ); \
+ \
+ ArrayType replaceDestination; \
+ replaceDestination.resize( NDIM, dimensions ); \
+ for( localIndex valueIndex = 0; valueIndex < replaceDestination.size(); ++valueIndex ) \
+ { \
+ replaceDestination.data()[valueIndex] = -1.0; \
+ } \
+ buffer_unit_type const * replaceReadBuffer = indexedBuffer.data(); \
+ parallelDeviceEvents replaceEvents; \
+ localIndex const replaceSize = bufferOps::UnpackByIndexDevice( \
+ replaceReadBuffer, replaceDestination.toView(), indices.toViewConst(), replaceEvents, MPI_REPLACE ); \
+ EXPECT_EQ( replaceSize, indexedPackedSize ); \
+ localIndex const replaceReadSize = replaceReadBuffer - indexedBuffer.data(); \
+ EXPECT_LE( replaceReadSize, replaceSize ); \
+ EXPECT_LT( replaceSize - replaceReadSize, static_cast< localIndex >( sizeof( TYPE ) ) ); \
+ waitAllDeviceEvents( replaceEvents ); \
+ replaceDestination.move( hostMemorySpace ); \
+ source.move( hostMemorySpace ); \
+ for( localIndex valueIndex = 0; valueIndex < source.size(); ++valueIndex ) \
+ { \
+ EXPECT_EQ( replaceDestination.data()[valueIndex], source.data()[valueIndex] ); \
+ } \
+ \
+ ArrayType sumDestination; \
+ sumDestination.resize( NDIM, dimensions ); \
+ for( localIndex valueIndex = 0; valueIndex < sumDestination.size(); ++valueIndex ) \
+ { \
+ sumDestination.data()[valueIndex] = 2.0; \
+ } \
+ buffer_unit_type const * sumReadBuffer = indexedBuffer.data(); \
+ parallelDeviceEvents sumEvents; \
+ localIndex const sumSize = bufferOps::UnpackByIndexDevice( \
+ sumReadBuffer, sumDestination.toView(), indices.toViewConst(), sumEvents, MPI_SUM ); \
+ EXPECT_EQ( sumSize, indexedPackedSize ); \
+ localIndex const sumReadSize = sumReadBuffer - indexedBuffer.data(); \
+ EXPECT_LE( sumReadSize, sumSize ); \
+ EXPECT_LT( sumSize - sumReadSize, static_cast< localIndex >( sizeof( TYPE ) ) ); \
+ waitAllDeviceEvents( sumEvents ); \
+ sumDestination.move( hostMemorySpace ); \
+ source.move( hostMemorySpace ); \
+ for( localIndex valueIndex = 0; valueIndex < source.size(); ++valueIndex ) \
+ { \
+ TYPE expected = source.data()[valueIndex]; \
+ expected += 2.0; \
+ EXPECT_EQ( sumDestination.data()[valueIndex], expected ); \
+ } \
+ \
+ ArrayType maxIncomingDestination; \
+ maxIncomingDestination.resize( NDIM, dimensions ); \
+ for( localIndex valueIndex = 0; valueIndex < maxIncomingDestination.size(); ++valueIndex ) \
+ { \
+ maxIncomingDestination.data()[valueIndex] = 0.0; \
+ } \
+ buffer_unit_type const * maxIncomingReadBuffer = indexedBuffer.data(); \
+ parallelDeviceEvents maxIncomingEvents; \
+ localIndex const maxIncomingSize = bufferOps::UnpackByIndexDevice( \
+ maxIncomingReadBuffer, maxIncomingDestination.toView(), indices.toViewConst(), maxIncomingEvents, MPI_MAX ); \
+ EXPECT_EQ( maxIncomingSize, indexedPackedSize ); \
+ localIndex const maxIncomingReadSize = maxIncomingReadBuffer - indexedBuffer.data(); \
+ EXPECT_LE( maxIncomingReadSize, maxIncomingSize ); \
+ EXPECT_LT( maxIncomingSize - maxIncomingReadSize, static_cast< localIndex >( sizeof( TYPE ) ) ); \
+ waitAllDeviceEvents( maxIncomingEvents ); \
+ maxIncomingDestination.move( hostMemorySpace ); \
+ source.move( hostMemorySpace ); \
+ for( localIndex valueIndex = 0; valueIndex < source.size(); ++valueIndex ) \
+ { \
+ EXPECT_EQ( maxIncomingDestination.data()[valueIndex], source.data()[valueIndex] ); \
+ } \
+ \
+ ArrayType maxExistingDestination; \
+ maxExistingDestination.resize( NDIM, dimensions ); \
+ for( localIndex valueIndex = 0; valueIndex < maxExistingDestination.size(); ++valueIndex ) \
+ { \
+ maxExistingDestination.data()[valueIndex] = 1000.0; \
+ } \
+ buffer_unit_type const * maxExistingReadBuffer = indexedBuffer.data(); \
+ parallelDeviceEvents maxExistingEvents; \
+ localIndex const maxExistingSize = bufferOps::UnpackByIndexDevice( \
+ maxExistingReadBuffer, maxExistingDestination.toView(), indices.toViewConst(), maxExistingEvents, MPI_MAX ); \
+ EXPECT_EQ( maxExistingSize, indexedPackedSize ); \
+ localIndex const maxExistingReadSize = maxExistingReadBuffer - indexedBuffer.data(); \
+ EXPECT_LE( maxExistingReadSize, maxExistingSize ); \
+ EXPECT_LT( maxExistingSize - maxExistingReadSize, static_cast< localIndex >( sizeof( TYPE ) ) ); \
+ waitAllDeviceEvents( maxExistingEvents ); \
+ maxExistingDestination.move( hostMemorySpace ); \
+ for( localIndex valueIndex = 0; valueIndex < maxExistingDestination.size(); ++valueIndex ) \
+ { \
+ TYPE expected; \
+ expected = 1000.0; \
+ EXPECT_EQ( maxExistingDestination.data()[valueIndex], expected ); \
+ } \
+ }
+
+#define CHECK_EXPLICIT_DEVICE_BUFFER_OPS_UP_TO_2D( TYPE ) \
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS( TYPE, 1, RAJA::PERM_I ); \
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS( TYPE, 2, RAJA::PERM_JI ); \
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS( TYPE, 2, RAJA::PERM_IJ )
+
+#define CHECK_EXPLICIT_DEVICE_BUFFER_OPS_UP_TO_3D( TYPE ) \
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS_UP_TO_2D( TYPE ); \
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS( TYPE, 3, RAJA::PERM_JKI ); \
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS( TYPE, 3, RAJA::PERM_IKJ ); \
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS( TYPE, 3, RAJA::PERM_IJK )
+
+#define CHECK_EXPLICIT_DEVICE_BUFFER_OPS_UP_TO_4D( TYPE ) \
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS_UP_TO_3D( TYPE ); \
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS( TYPE, 4, RAJA::PERM_JKLI ); \
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS( TYPE, 4, RAJA::PERM_IKLJ ); \
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS( TYPE, 4, RAJA::PERM_IJLK ); \
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS( TYPE, 4, RAJA::PERM_IJKL )
+
+#define CHECK_EXPLICIT_DEVICE_BUFFER_OPS_UP_TO_5D( TYPE ) \
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS_UP_TO_4D( TYPE ); \
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS( TYPE, 5, RAJA::PERM_JKLMI ); \
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS( TYPE, 5, RAJA::PERM_IKLMJ ); \
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS( TYPE, 5, RAJA::PERM_IJLMK ); \
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS( TYPE, 5, RAJA::PERM_IJKML ); \
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS( TYPE, 5, RAJA::PERM_IJKLM )
+
+TEST( testPacking, testExplicitDeviceInstantiationMatrix )
+{
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS_UP_TO_3D( int );
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS_UP_TO_3D( long int );
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS_UP_TO_3D( long long int );
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS_UP_TO_5D( real32 );
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS_UP_TO_5D( real64 );
+ CHECK_EXPLICIT_DEVICE_BUFFER_OPS_UP_TO_3D( R1Tensor );
+}
+
+#undef CHECK_EXPLICIT_DEVICE_BUFFER_OPS_UP_TO_5D
+#undef CHECK_EXPLICIT_DEVICE_BUFFER_OPS_UP_TO_4D
+#undef CHECK_EXPLICIT_DEVICE_BUFFER_OPS_UP_TO_3D
+#undef CHECK_EXPLICIT_DEVICE_BUFFER_OPS_UP_TO_2D
+#undef CHECK_EXPLICIT_DEVICE_BUFFER_OPS
+
int main( int ac, char * av[] )
{
diff --git a/src/coreComponents/dataRepository/unitTests/testWrapper.cpp b/src/coreComponents/dataRepository/unitTests/testWrapper.cpp
index 4e17ded9543..4208404a00d 100644
--- a/src/coreComponents/dataRepository/unitTests/testWrapper.cpp
+++ b/src/coreComponents/dataRepository/unitTests/testWrapper.cpp
@@ -14,16 +14,372 @@
*/
// Source includes
+#include "common/TypeDispatch.hpp"
#include "dataRepository/Group.hpp"
#include "dataRepository/Wrapper.hpp"
+#include "LvArray/src/system.hpp"
// TPL includes
#include
#include
+// System includes
+#include
+#include
+#include
+
using namespace geos;
using namespace dataRepository;
+namespace
+{
+
+class ScopedThrowingLvArrayErrorHandler
+{
+public:
+ ScopedThrowingLvArrayErrorHandler()
+ {
+ LvArray::system::setErrorHandler( []()
+ {
+ throw std::runtime_error( "Expected WrapperStandardArrays error" );
+ } );
+ }
+
+ ~ScopedThrowingLvArrayErrorHandler() noexcept
+ {
+ LvArray::system::setErrorHandler( []()
+ {
+ std::abort();
+ } );
+ }
+
+ ScopedThrowingLvArrayErrorHandler( ScopedThrowingLvArrayErrorHandler const & ) = delete;
+ ScopedThrowingLvArrayErrorHandler & operator=( ScopedThrowingLvArrayErrorHandler const & ) = delete;
+};
+
+template< typename ... ARRAY_TYPES >
+void testStandardArrayVirtualInterface( camp::list< ARRAY_TYPES ... > )
+{
+ ScopedThrowingLvArrayErrorHandler const scopedErrorHandler;
+
+ // Keep the arrays outside the Group so both the registered wrappers and their clones are
+ // non-owning. This makes it safe to exercise clone() without sharing ownership of the data.
+ std::tuple< ARRAY_TYPES ... > storage;
+ conduit::Node rootNode;
+ Group group( "Problem", rootNode );
+
+ localIndex wrapperIndex = 0;
+ std::apply( [&]( auto & ... value )
+ {
+ ( group.registerWrapper( "standardArray" + std::to_string( wrapperIndex++ ), &value ), ... );
+ }, storage );
+
+ xmlWrapper::xmlDocument emptyDocument;
+ xmlWrapper::xmlNodePos const noPosition( emptyDocument,
+ "",
+ xmlWrapper::xmlDocument::npos,
+ xmlWrapper::xmlDocument::npos,
+ xmlWrapper::xmlDocument::npos );
+ xmlWrapper::xmlNode const emptyNode;
+
+ string_array const dimLabels = { "first", "second" };
+ array1d< localIndex > packIndices( 1 );
+ packIndices[0] = 0;
+ parallelDeviceEvents events;
+ localIndex numWrappersTested = 0;
+
+ group.forWrappers( [&]( WrapperBase & wrapper )
+ {
+ ++numWrappersTested;
+
+ EXPECT_FALSE( wrapper.getName().empty() );
+ EXPECT_FALSE( wrapper.getPath().empty() );
+ EXPECT_EQ( &wrapper.getParent(), &group );
+ EXPECT_FALSE( wrapper.getSuccessfulReadFromInput() );
+ EXPECT_FALSE( wrapper.processInputFile( emptyNode, noPosition ) );
+
+ int const numDims = wrapper.numArrayDims();
+ ASSERT_GT( numDims, 0 );
+ stdVector< localIndex > const dims( numDims, 2 );
+ wrapper.resize( numDims, dims.data() );
+
+ EXPECT_GT( wrapper.size(), 0 );
+ EXPECT_NE( wrapper.voidPointer(), nullptr );
+ EXPECT_GT( wrapper.elementByteSize(), 0 );
+ EXPECT_GT( wrapper.bytesAllocated(), 0 );
+
+ wrapper.resize( 3 );
+ wrapper.resize( numDims, dims.data() );
+ wrapper.reserve( 3 );
+ EXPECT_GT( wrapper.capacity(), 0 );
+
+ // These validation failures occur before Array dimensions or storage are modified.
+ EXPECT_THROW( wrapper.resize( numDims + 1, dims.data() ), std::runtime_error );
+ EXPECT_THROW( wrapper.resize( localIndex( -1 ) ), std::runtime_error );
+ EXPECT_THROW( wrapper.move( static_cast< LvArray::MemorySpace >( -1 ), false ), std::runtime_error );
+
+ for( integer dim = 0; dim < numDims; ++dim )
+ {
+ EXPECT_EQ( &wrapper.setDimLabels( dim, dimLabels ), &wrapper );
+ Span< string const > const labels = wrapper.getDimLabels( dim );
+ ASSERT_EQ( labels.size(), dimLabels.size() );
+ EXPECT_EQ( labels[0], dimLabels[0] );
+ EXPECT_EQ( labels[1], dimLabels[1] );
+ }
+
+ EXPECT_THROW( wrapper.setDimLabels( -1, dimLabels ), std::runtime_error );
+ EXPECT_THROW( wrapper.setDimLabels( numDims, dimLabels ), std::runtime_error );
+ EXPECT_THROW( wrapper.getDimLabels( -1 ), std::runtime_error );
+ EXPECT_THROW( wrapper.getDimLabels( numDims ), std::runtime_error );
+
+ stdVector< string > const invalidComponentNames( wrapper.numArrayComp() + 1, "invalid" );
+ conduit::Node invalidFields;
+ conduit::Node invalidMCArray;
+ EXPECT_THROW( wrapper.addBlueprintField( invalidFields,
+ wrapper.getName(),
+ "topology",
+ invalidComponentNames ),
+ std::runtime_error );
+ EXPECT_THROW( wrapper.populateMCArray( invalidMCArray, invalidComponentNames ), std::runtime_error );
+
+ wrapper.resize( 0 );
+ conduit::Node emptyFields;
+ conduit::Node emptyMCArray;
+ EXPECT_THROW( wrapper.addBlueprintField( emptyFields, wrapper.getName(), "topology" ), std::runtime_error );
+ EXPECT_THROW( wrapper.populateMCArray( emptyMCArray ), std::runtime_error );
+ wrapper.resize( numDims, dims.data() );
+
+ // Restrict dispatch to the production type catalog so the test does not introduce unrelated
+ // Wrapper types solely for coverage.
+ EXPECT_TRUE( types::dispatch( types::ListofTypeList< types::StandardArrays >{}, [&]( auto typeTuple )
+ {
+ using ArrayType = camp::first< decltype( typeTuple ) >;
+ using ValueType = typename ArrayType::ValueType;
+
+ Wrapper< ArrayType > & typedWrapper = Wrapper< ArrayType >::cast( wrapper );
+ WrapperBase const & constBase = wrapper;
+ EXPECT_EQ( &Wrapper< ArrayType >::cast( constBase ), &typedWrapper );
+
+ ValueType const defaultValue = ValueType( 1 );
+ typedWrapper.setDefaultValue( defaultValue );
+ typedWrapper.setApplyDefaultValue( defaultValue );
+ EXPECT_EQ( typedWrapper.getDefaultValue(), defaultValue );
+ EXPECT_TRUE( typedWrapper.hasDefaultValue() );
+
+ typedWrapper.setSizedFromParent( 1 );
+ typedWrapper.setRestartFlags( RestartFlags::WRITE_AND_READ );
+ typedWrapper.setPlotLevel( PlotLevel::LEVEL_1 );
+ typedWrapper.setInputFlag( InputFlags::OPTIONAL );
+ typedWrapper.setDescription( "standard array" );
+ typedWrapper.appendDescription( " wrapper" );
+ typedWrapper.setRegisteringObjects( "testWrapper" );
+ typedWrapper.setRTTypeName( typedWrapper.getRTTypeName() );
+ typedWrapper.setName();
+
+ EXPECT_EQ( typedWrapper.getDescription(), "standard array wrapper" );
+ EXPECT_EQ( typedWrapper.getRegisteringObjects().count( "testWrapper" ), 1 );
+ EXPECT_EQ( typedWrapper.referenceAsView().size(), typedWrapper.reference().size() );
+
+ ArrayType & array = typedWrapper.reference();
+ for( localIndex valueIndex = 0; valueIndex < array.size(); ++valueIndex )
+ {
+ array.data()[valueIndex] = static_cast< ValueType >( valueIndex + 2 );
+ }
+ }, wrapper ) );
+
+ EXPECT_FALSE( wrapper.getDefaultValueString().empty() );
+ static_cast< void >( wrapper.getTypeRegex() );
+ EXPECT_TRUE( wrapper.isPackable( false ) );
+ static_cast< void >( wrapper.isPackable( true ) );
+
+ HistoryMetadata const history = wrapper.getHistoryMetadata( 1 );
+ EXPECT_EQ( history.getName(), wrapper.getName() );
+ EXPECT_GT( history.getRank(), 0 );
+ EXPECT_GT( history.size(), 0 );
+
+ conduit::Node fields;
+ wrapper.addBlueprintField( fields, wrapper.getName(), "topology" );
+ EXPECT_GT( fields.number_of_children(), 0 );
+
+ conduit::Node mcArray;
+ wrapper.populateMCArray( mcArray );
+ EXPECT_GT( mcArray.number_of_children(), 0 );
+
+ std::unique_ptr< WrapperBase > average =
+ wrapper.averageOverSecondDim( wrapper.getName() + "Average", group );
+ ASSERT_NE( average, nullptr );
+ EXPECT_GT( average->size(), 0 );
+
+ std::unique_ptr< WrapperBase > clone = wrapper.clone( wrapper.getName() + "Clone", group );
+ ASSERT_NE( clone, nullptr );
+ EXPECT_TRUE( clone->getTypeId() == wrapper.getTypeId() );
+ EXPECT_EQ( clone->voidPointer(), wrapper.voidPointer() );
+ EXPECT_EQ( clone->bytesAllocated(), 0 );
+ EXPECT_THROW( clone->copyWrapper( wrapper ), std::runtime_error );
+
+ for( bool const withMetadata : { false, true } )
+ {
+ buffer_unit_type * unusedBuffer = nullptr;
+ localIndex const packedSize = wrapper.pack< false >( unusedBuffer, withMetadata, false, events );
+ ASSERT_GT( packedSize, 0 );
+
+ buffer_type packed( packedSize );
+ buffer_unit_type * packBuffer = packed.data();
+ EXPECT_EQ( wrapper.pack< true >( packBuffer, withMetadata, false, events ), packedSize );
+ EXPECT_EQ( packBuffer, packed.data() + packedSize );
+
+ if( withMetadata )
+ {
+ buffer_unit_type const * mismatchedBuffer = packed.data();
+ EXPECT_THROW( clone->unpack( mismatchedBuffer, true, false, events ), std::runtime_error );
+ }
+
+ EXPECT_TRUE( types::dispatch( types::ListofTypeList< types::StandardArrays >{}, [&]( auto typeTuple )
+ {
+ using ArrayType = camp::first< decltype( typeTuple ) >;
+ using ValueType = typename ArrayType::ValueType;
+ ArrayType & array = Wrapper< ArrayType >::cast( wrapper ).reference();
+ for( localIndex valueIndex = 0; valueIndex < array.size(); ++valueIndex )
+ {
+ array.data()[valueIndex] = ValueType( 0 );
+ }
+ }, wrapper ) );
+
+ buffer_unit_type const * unpackBuffer = packed.data();
+ EXPECT_EQ( wrapper.unpack( unpackBuffer, withMetadata, false, events ), packedSize );
+ EXPECT_EQ( unpackBuffer, packed.data() + packedSize );
+
+ EXPECT_TRUE( types::dispatch( types::ListofTypeList< types::StandardArrays >{}, [&]( auto typeTuple )
+ {
+ using ArrayType = camp::first< decltype( typeTuple ) >;
+ using ValueType = typename ArrayType::ValueType;
+ ArrayType const & array = Wrapper< ArrayType >::cast( wrapper ).reference();
+ for( localIndex valueIndex = 0; valueIndex < array.size(); ++valueIndex )
+ {
+ EXPECT_EQ( array.data()[valueIndex], static_cast< ValueType >( valueIndex + 2 ) );
+ }
+ }, wrapper ) );
+
+ unusedBuffer = nullptr;
+ localIndex const indexedPackedSize =
+ wrapper.packByIndex< false >( unusedBuffer, packIndices.toViewConst(), withMetadata, false, events );
+ ASSERT_GT( indexedPackedSize, 0 );
+
+ buffer_type indexedPacked( indexedPackedSize );
+ packBuffer = indexedPacked.data();
+ EXPECT_EQ( wrapper.packByIndex< true >( packBuffer,
+ packIndices.toViewConst(),
+ withMetadata,
+ false,
+ events ),
+ indexedPackedSize );
+ EXPECT_EQ( packBuffer, indexedPacked.data() + indexedPackedSize );
+
+ if( withMetadata )
+ {
+ buffer_unit_type const * mismatchedBuffer = indexedPacked.data();
+ EXPECT_THROW( clone->unpackByIndex( mismatchedBuffer,
+ packIndices.toViewConst(),
+ true,
+ false,
+ events,
+ MPI_REPLACE ),
+ std::runtime_error );
+ }
+
+ EXPECT_TRUE( types::dispatch( types::ListofTypeList< types::StandardArrays >{}, [&]( auto typeTuple )
+ {
+ using ArrayType = camp::first< decltype( typeTuple ) >;
+ using ValueType = typename ArrayType::ValueType;
+ ArrayType & array = Wrapper< ArrayType >::cast( wrapper ).reference();
+ LvArray::forValuesInSliceWithIndices( array[0], [&]( auto & value, auto const ... )
+ {
+ value = ValueType( 0 );
+ } );
+ }, wrapper ) );
+
+ unpackBuffer = indexedPacked.data();
+ EXPECT_EQ( wrapper.unpackByIndex( unpackBuffer,
+ packIndices.toViewConst(),
+ withMetadata,
+ false,
+ events,
+ MPI_REPLACE ),
+ indexedPackedSize );
+ EXPECT_EQ( unpackBuffer, indexedPacked.data() + indexedPackedSize );
+
+ EXPECT_TRUE( types::dispatch( types::ListofTypeList< types::StandardArrays >{}, [&]( auto typeTuple )
+ {
+ using ArrayType = camp::first< decltype( typeTuple ) >;
+ using ValueType = typename ArrayType::ValueType;
+ ArrayType const & array = Wrapper< ArrayType >::cast( wrapper ).reference();
+ for( localIndex valueIndex = 0; valueIndex < array.size(); ++valueIndex )
+ {
+ EXPECT_EQ( array.data()[valueIndex], static_cast< ValueType >( valueIndex + 2 ) );
+ }
+ }, wrapper ) );
+ }
+
+ wrapper.copy( 0, 1 );
+ EXPECT_TRUE( types::dispatch( types::ListofTypeList< types::StandardArrays >{}, [&]( auto typeTuple )
+ {
+ using ArrayType = camp::first< decltype( typeTuple ) >;
+ ArrayType const & array = Wrapper< ArrayType >::cast( wrapper ).reference();
+ LvArray::forValuesInSliceWithIndices( array[0], [&]( auto const & expectedValue, auto const ... indices )
+ {
+ EXPECT_EQ( array( 1, indices ... ), expectedValue );
+ } );
+ }, wrapper ) );
+ wrapper.move( hostMemorySpace, false );
+ wrapper.move( hostMemorySpace, true );
+
+ wrapper.setRestartFlags( RestartFlags::NO_WRITE );
+ wrapper.registerToWrite();
+ wrapper.finishWriting();
+ wrapper.setRestartFlags( RestartFlags::WRITE );
+ wrapper.registerToWrite();
+ wrapper.finishWriting();
+ EXPECT_FALSE( wrapper.loadFromConduit() );
+
+ stdVector< camp::idx_t > invalidMetadata( numDims + 1, 0 );
+ conduit::DataType const invalidMetadataType( conduitTypeInfo< camp::idx_t >::id,
+ invalidMetadata.size() );
+ wrapper.setRestartFlags( RestartFlags::WRITE_AND_READ );
+
+ wrapper.registerToWrite();
+ group.getConduitNode()[wrapper.getName()]["__permutation__"].set( invalidMetadataType,
+ invalidMetadata.data() );
+ EXPECT_THROW( wrapper.loadFromConduit(), std::runtime_error );
+
+ wrapper.registerToWrite();
+ camp::idx_t * const permutation =
+ group.getConduitNode()[wrapper.getName()]["__permutation__"].value();
+ permutation[0] = numDims;
+ EXPECT_THROW( wrapper.loadFromConduit(), std::runtime_error );
+
+ wrapper.registerToWrite();
+ group.getConduitNode()[wrapper.getName()]["__dimensions__"].set( invalidMetadataType,
+ invalidMetadata.data() );
+ EXPECT_THROW( wrapper.loadFromConduit(), std::runtime_error );
+
+ wrapper.registerToWrite();
+ buffer_unit_type invalidValue = 0;
+ conduit::DataType const invalidValueType( conduitTypeInfo< buffer_unit_type >::id, 1 );
+ group.getConduitNode()[wrapper.getName()]["__values__"].set( invalidValueType, &invalidValue );
+ EXPECT_THROW( wrapper.loadFromConduit(), std::runtime_error );
+
+ wrapper.registerToWrite();
+ wrapper.finishWriting();
+
+ EXPECT_THROW( wrapper.erase( std::set< localIndex >{} ), std::runtime_error );
+ wrapper.erase( { 0 } );
+ } );
+
+ EXPECT_EQ( numWrappersTested, sizeof...( ARRAY_TYPES ) );
+}
+
+} // namespace
+
template< typename T >
class WrapperSetGet : public ::testing::Test
{
@@ -338,3 +694,8 @@ TEST_F( WrapperLimitsTest, Array2dValidateEmpty )
EXPECT_NO_THROW( w.validateLimits() );
}
+
+TEST( WrapperStandardArrays, VirtualInterface )
+{
+ testStandardArrayVirtualInterface( types::StandardArrays{} );
+}
diff --git a/src/coreComponents/fileIO/Outputs/unitTests/CMakeLists.txt b/src/coreComponents/fileIO/Outputs/unitTests/CMakeLists.txt
index 04134af6c4a..30413cf24cb 100644
--- a/src/coreComponents/fileIO/Outputs/unitTests/CMakeLists.txt
+++ b/src/coreComponents/fileIO/Outputs/unitTests/CMakeLists.txt
@@ -3,6 +3,10 @@
set( gtest_outputs_tests
testMemoryStats.cpp )
+if( ENABLE_VTK )
+ list( APPEND gtest_outputs_tests testVTKPolyDataWriter.cpp )
+endif()
+
# Link against mainInterface to resolve basicSetup and ProblemManager symbols
set( dependencyList mainInterface gtest ${parallelDeps} )
diff --git a/src/coreComponents/fileIO/Outputs/unitTests/testVTKPolyDataWriter.cpp b/src/coreComponents/fileIO/Outputs/unitTests/testVTKPolyDataWriter.cpp
new file mode 100644
index 00000000000..b2af37525b3
--- /dev/null
+++ b/src/coreComponents/fileIO/Outputs/unitTests/testVTKPolyDataWriter.cpp
@@ -0,0 +1,337 @@
+/*
+ * ------------------------------------------------------------------------------------------------------------
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * Copyright (c) 2016-2024 Lawrence Livermore National Security LLC
+ * Copyright (c) 2018-2024 TotalEnergies
+ * Copyright (c) 2018-2024 The Board of Trustees of the Leland Stanford Junior University
+ * Copyright (c) 2023-2024 Chevron
+ * Copyright (c) 2019- GEOS/GEOSX Contributors
+ * All rights reserved
+ *
+ * See top level LICENSE, COPYRIGHT, CONTRIBUTORS, NOTICE, and ACKNOWLEDGEMENTS files for details.
+ * ------------------------------------------------------------------------------------------------------------
+ */
+
+#include "fileIO/vtk/VTKPolyDataWriterInterface.hpp"
+
+#include "mesh/CellElementRegion.hpp"
+#include "mesh/NodeManager.hpp"
+#include "mesh/ParticleRegion.hpp"
+#include "mesh/ParticleSubRegion.hpp"
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+using namespace geos;
+using namespace geos::dataRepository;
+
+namespace
+{
+
+/**
+ * Keep this list synchronized with the layouts in types::StandardArrays. Expanding
+ * the operations directly in the test body avoids adding a templated test helper
+ * instantiation for every supported array type to coverage's function denominator.
+ */
+#define GEOS_FOR_EACH_REAL_STANDARD_ARRAY( OP, VALUE_TYPE ) \
+ OP( VALUE_TYPE, 1, RAJA::PERM_I ) \
+ OP( VALUE_TYPE, 2, RAJA::PERM_IJ ) \
+ OP( VALUE_TYPE, 2, RAJA::PERM_JI ) \
+ OP( VALUE_TYPE, 3, RAJA::PERM_IJK ) \
+ OP( VALUE_TYPE, 3, RAJA::PERM_IKJ ) \
+ OP( VALUE_TYPE, 3, RAJA::PERM_JIK ) \
+ OP( VALUE_TYPE, 3, RAJA::PERM_JKI ) \
+ OP( VALUE_TYPE, 3, RAJA::PERM_KIJ ) \
+ OP( VALUE_TYPE, 3, RAJA::PERM_KJI ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_IJKL ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_IJLK ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_IKJL ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_IKLJ ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_ILJK ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_ILKJ ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_JIKL ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_JILK ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_JKIL ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_JKLI ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_JLIK ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_JLKI ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_KIJL ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_KILJ ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_KJIL ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_KJLI ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_KLIJ ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_KLJI ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_LIJK ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_LIKJ ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_LJIK ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_LJKI ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_LKIJ ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_LKJI )
+
+#define GEOS_FOR_EACH_INTEGRAL_STANDARD_ARRAY( OP, VALUE_TYPE ) \
+ OP( VALUE_TYPE, 1, RAJA::PERM_I ) \
+ OP( VALUE_TYPE, 2, RAJA::PERM_IJ ) \
+ OP( VALUE_TYPE, 2, RAJA::PERM_JI )
+
+#define GEOS_FOR_EACH_STANDARD_ARRAY( OP ) \
+ GEOS_FOR_EACH_REAL_STANDARD_ARRAY( OP, real32 ) \
+ GEOS_FOR_EACH_REAL_STANDARD_ARRAY( OP, real64 ) \
+ GEOS_FOR_EACH_INTEGRAL_STANDARD_ARRAY( OP, integer ) \
+ GEOS_FOR_EACH_INTEGRAL_STANDARD_ARRAY( OP, localIndex ) \
+ GEOS_FOR_EACH_INTEGRAL_STANDARD_ARRAY( OP, globalIndex )
+
+class TestVTKWriter final : public geos::vtk::VTKPolyDataWriterInterface
+{
+ using Base = geos::vtk::VTKPolyDataWriterInterface;
+
+public:
+ TestVTKWriter()
+ : Base( "unused" )
+ {
+ setOnlyPlotSpecifiedFieldNamesFlag( 1 );
+ }
+
+ using Base::writeElementFields;
+ using Base::writeNodeFields;
+ using Base::writeParticleFields;
+};
+
+struct FieldExpectation
+{
+ string name;
+ integer numDims;
+ localIndex numComponents;
+ bool labeled;
+ real64 nodeValue;
+ real64 elementValue;
+ real64 particleValue;
+};
+
+void checkField( vtkDataSetAttributes * const attributes,
+ FieldExpectation const & expected,
+ stdVector< real64 > const & expectedTupleValues )
+{
+ vtkDataArray * const data = attributes->GetArray( expected.name.c_str() );
+ ASSERT_NE( data, nullptr );
+ ASSERT_EQ( data->GetNumberOfComponents(), expected.numComponents );
+ ASSERT_EQ( data->GetNumberOfTuples(), expectedTupleValues.size() );
+
+ for( vtkIdType tuple = 0; tuple < data->GetNumberOfTuples(); ++tuple )
+ {
+ for( localIndex component = 0; component < expected.numComponents; ++component )
+ {
+ EXPECT_DOUBLE_EQ( data->GetComponent( tuple, component ), expectedTupleValues[tuple] );
+ }
+ }
+
+ if( expected.numDims == 1 )
+ {
+ EXPECT_EQ( data->GetComponentName( 0 ), nullptr );
+ return;
+ }
+
+ if( expected.numDims == 2 && !expected.labeled )
+ {
+ for( localIndex component = 0; component < expected.numComponents; ++component )
+ {
+ EXPECT_EQ( data->GetComponentName( component ), nullptr );
+ }
+ return;
+ }
+
+ std::set< string > expectedNames;
+ for( localIndex component = 0; component < expected.numComponents; ++component )
+ {
+ string componentName;
+ for( integer dim = 1; dim < expected.numDims; ++dim )
+ {
+ if( dim > 1 )
+ {
+ componentName += "/";
+ }
+ bool const high = ( component & ( 1 << ( dim - 1 ) ) ) != 0;
+ componentName += expected.labeled ? ( high ? "high" : "low" ) : ( high ? "1" : "0" );
+ }
+ expectedNames.insert( std::move( componentName ) );
+ }
+
+ std::set< string > actualNames;
+ for( localIndex component = 0; component < expected.numComponents; ++component )
+ {
+ char const * const componentName = data->GetComponentName( component );
+ ASSERT_NE( componentName, nullptr );
+ actualNames.insert( componentName );
+ }
+ EXPECT_EQ( actualNames, expectedNames );
+}
+
+TEST( VTKPolyDataWriter, StandardArrayFieldsAndMetadata )
+{
+ conduit::Node rootNode;
+ Group root( "root", rootNode );
+
+ NodeManager nodeManager( "nodes", &root );
+ nodeManager.resize( 4 );
+ array1d< localIndex > nodeIndices( 3 );
+ nodeIndices[0] = 2;
+ nodeIndices[1] = 0;
+ nodeIndices[2] = 3;
+
+ CellElementRegion elementRegion( "elements", &root );
+ CellElementSubRegion & elementSubRegion0 =
+ elementRegion.createElementSubRegion< CellElementSubRegion >( "elementSubRegion0" );
+ CellElementSubRegion & elementSubRegion1 =
+ elementRegion.createElementSubRegion< CellElementSubRegion >( "elementSubRegion1" );
+ elementSubRegion0.resize( 2 );
+ elementSubRegion1.resize( 1 );
+ std::array< ObjectManagerBase *, 2 > const elementSubRegions =
+ { { &elementSubRegion0, &elementSubRegion1 } };
+
+ ParticleRegion particleRegion( "particles", &root );
+ Group & particleSubRegionGroup =
+ particleRegion.getGroup( ParticleRegionBase::viewKeyStruct::particleSubRegions() );
+ ParticleSubRegion & particleSubRegion0 =
+ particleSubRegionGroup.registerGroup< ParticleSubRegion >( "particleSubRegion0" );
+ ParticleSubRegion & particleSubRegion1 =
+ particleSubRegionGroup.registerGroup< ParticleSubRegion >( "particleSubRegion1" );
+ particleSubRegion0.resize( 1 );
+ particleSubRegion1.resize( 2 );
+ std::array< ObjectManagerBase *, 2 > const particleSubRegions =
+ { { &particleSubRegion0, &particleSubRegion1 } };
+
+ stdVector< string > const componentLabels{ "low", "high" };
+ string_array fieldNames;
+ stdVector< FieldExpectation > expectations;
+ localIndex fieldIndex = 0;
+
+ #define GEOS_REGISTER_VTK_FIELD( VALUE_TYPE, NDIM, PERMUTATION ) \
+ do \
+ { \
+ using ArrayType = Array< VALUE_TYPE, NDIM, PERMUTATION >; \
+ string const fieldName = "standardArray_" + std::to_string( fieldIndex ); \
+ bool const labeled = fieldIndex % 2 == 0; \
+ real64 const nodeValue = 100 + fieldIndex; \
+ real64 const elementValue = 1000 + 10 * fieldIndex; \
+ real64 const particleValue = 2000 + 10 * fieldIndex; \
+ fieldNames.emplace_back( fieldName ); \
+ expectations.push_back( { fieldName, NDIM, 1 << ( NDIM - 1 ), labeled, \
+ nodeValue, elementValue, particleValue } ); \
+ \
+ Wrapper< ArrayType > & nodeWrapper = nodeManager.registerWrapper< ArrayType >( fieldName ); \
+ std::array< localIndex, NDIM > nodeDims; \
+ nodeDims.fill( 2 ); \
+ nodeDims[0] = nodeManager.size(); \
+ nodeWrapper.reference().resize( NDIM, nodeDims.data() ); \
+ std::fill( nodeWrapper.reference().data(), \
+ nodeWrapper.reference().data() + nodeWrapper.reference().size(), \
+ static_cast< VALUE_TYPE >( nodeValue ) ); \
+ if( labeled ) \
+ { \
+ for( integer dim = 1; dim < NDIM; ++dim ) \
+ { \
+ nodeWrapper.setDimLabels( dim, { componentLabels.begin(), componentLabels.end() } ); \
+ } \
+ } \
+ \
+ for( std::size_t subRegionIndex = 0; subRegionIndex < elementSubRegions.size(); \
+ ++subRegionIndex ) \
+ { \
+ ObjectManagerBase & subRegion = *elementSubRegions[subRegionIndex]; \
+ Wrapper< ArrayType > & wrapper = subRegion.registerWrapper< ArrayType >( fieldName ); \
+ std::array< localIndex, NDIM > dims; \
+ dims.fill( 2 ); \
+ dims[0] = subRegion.size(); \
+ wrapper.reference().resize( NDIM, dims.data() ); \
+ std::fill( wrapper.reference().data(), \
+ wrapper.reference().data() + wrapper.reference().size(), \
+ static_cast< VALUE_TYPE >( elementValue + subRegionIndex ) ); \
+ if( labeled ) \
+ { \
+ for( integer dim = 1; dim < NDIM; ++dim ) \
+ { \
+ wrapper.setDimLabels( dim, { componentLabels.begin(), componentLabels.end() } ); \
+ } \
+ } \
+ } \
+ \
+ for( std::size_t subRegionIndex = 0; subRegionIndex < particleSubRegions.size(); \
+ ++subRegionIndex ) \
+ { \
+ ObjectManagerBase & subRegion = *particleSubRegions[subRegionIndex]; \
+ Wrapper< ArrayType > & wrapper = subRegion.registerWrapper< ArrayType >( fieldName ); \
+ std::array< localIndex, NDIM > dims; \
+ dims.fill( 2 ); \
+ dims[0] = subRegion.size(); \
+ wrapper.reference().resize( NDIM, dims.data() ); \
+ std::fill( wrapper.reference().data(), \
+ wrapper.reference().data() + wrapper.reference().size(), \
+ static_cast< VALUE_TYPE >( particleValue + subRegionIndex ) ); \
+ if( labeled ) \
+ { \
+ for( integer dim = 1; dim < NDIM; ++dim ) \
+ { \
+ wrapper.setDimLabels( dim, { componentLabels.begin(), componentLabels.end() } ); \
+ } \
+ } \
+ } \
+ ++fieldIndex; \
+ } while( false );
+
+ GEOS_FOR_EACH_STANDARD_ARRAY( GEOS_REGISTER_VTK_FIELD )
+#undef GEOS_REGISTER_VTK_FIELD
+
+ ASSERT_EQ( fieldIndex, 75 );
+
+ string const indexedFieldName = "indexedNodeField";
+ array1d< real64 > & indexedField =
+ nodeManager.registerWrapper< array1d< real64 > >( indexedFieldName ).reference();
+ indexedField.resize( nodeManager.size() );
+ for( localIndex node = 0; node < nodeManager.size(); ++node )
+ {
+ indexedField[node] = 10 + node;
+ }
+ fieldNames.emplace_back( indexedFieldName );
+
+ TestVTKWriter writer;
+ writer.setFieldNames( fieldNames );
+
+ vtkNew< vtkUnstructuredGrid > nodeOutput;
+ writer.writeNodeFields( nodeManager, nodeIndices.toViewConst(), nodeOutput->GetPointData() );
+
+ vtkNew< vtkUnstructuredGrid > elementOutput;
+ writer.writeElementFields( elementRegion, elementOutput->GetCellData() );
+
+ vtkNew< vtkUnstructuredGrid > particleOutput;
+ writer.writeParticleFields( particleRegion, particleOutput->GetCellData() );
+
+ for( FieldExpectation const & expected : expectations )
+ {
+ SCOPED_TRACE( expected.name );
+ checkField( nodeOutput->GetPointData(), expected,
+ { expected.nodeValue, expected.nodeValue, expected.nodeValue } );
+ checkField( elementOutput->GetCellData(), expected,
+ { expected.elementValue, expected.elementValue, expected.elementValue + 1 } );
+ checkField( particleOutput->GetCellData(), expected,
+ { expected.particleValue, expected.particleValue + 1, expected.particleValue + 1 } );
+ }
+
+ FieldExpectation const indexedExpectation{ indexedFieldName, 1, 1, false, 0, 0, 0 };
+ checkField( nodeOutput->GetPointData(), indexedExpectation, { 12, 10, 13 } );
+}
+
+#undef GEOS_FOR_EACH_STANDARD_ARRAY
+#undef GEOS_FOR_EACH_INTEGRAL_STANDARD_ARRAY
+#undef GEOS_FOR_EACH_REAL_STANDARD_ARRAY
+
+} // namespace
diff --git a/src/coreComponents/integrationTests/CMakeLists.txt b/src/coreComponents/integrationTests/CMakeLists.txt
index 69edfa9b91b..45280829398 100644
--- a/src/coreComponents/integrationTests/CMakeLists.txt
+++ b/src/coreComponents/integrationTests/CMakeLists.txt
@@ -32,3 +32,7 @@ endif()
add_subdirectory( wellsTests )
add_subdirectory( wavePropagationTests )
add_subdirectory( mechanicTest )
+
+if( ENABLE_COVERAGE OR GEOS_ENABLE_LLVM_SOURCE_COVERAGE )
+ add_subdirectory( coverageSmokeTests )
+endif()
diff --git a/src/coreComponents/integrationTests/coverageSmokeTests/CMakeLists.txt b/src/coreComponents/integrationTests/coverageSmokeTests/CMakeLists.txt
new file mode 100644
index 00000000000..6c1614f9882
--- /dev/null
+++ b/src/coreComponents/integrationTests/coverageSmokeTests/CMakeLists.txt
@@ -0,0 +1,339 @@
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+# Copyright (c) 2016-2024 Lawrence Livermore National Security LLC
+# Copyright (c) 2018-2024 TotalEnergies
+# Copyright (c) 2018-2024 The Board of Trustees of the Leland Stanford Junior University
+# Copyright (c) 2023-2024 Chevron
+# Copyright (c) 2019- GEOS/GEOSX Contributors
+# All rights reserved.
+#
+# See top level LICENSE, COPYRIGHT, CONTRIBUTORS, NOTICE, and ACKNOWLEDGEMENTS files for details.
+#
+#--------------------------------------------------------------------------------------------------
+
+set( coverage_smoke_inputs
+ materialPointMethod/singleParticle/mpm_singleParticle.xml
+ materialPointMethod/particleRepartition/mpm_particleRepartition.xml
+ materialPointMethod/dfgMovingGrid/mpm_dfgMovingGrid.xml
+ lagrangianContactMechanics/LagrangeContactBubbleStab_FixedSlip_smoke.xml
+ lagrangianContactMechanics/ContactMechanics_SimpleCubes_smoke.xml
+ hydraulicFracturing/kgdViscosityDominated_smoke.xml
+ hydraulicFracturing/kgdViscosityDominated_poroelastic_smoke.xml
+ proppant/FlowProppantTransport2d_smoke.xml
+ proppant/FlowProppantBedTransport2d_smoke.xml
+ singlePhaseReactiveTransport/mixedReactionCarbonateSystemPorosityUpdateThermal_1D_smoke.xml
+ phaseField/PhaseFieldFracture_Nucleation_smoke.xml
+ phaseField/PhaseFieldPoromechanics_Nucleation_smoke.xml
+ inducedSeismicity/SeismicityRate_analytical_verification_smoke.xml
+ inducedSeismicity/SpringSliderExplicit_A_smoke.xml
+ inducedSeismicity/SpringSliderImplicit_A_smoke.xml
+ efemFractureMechanics/EmbFrac_Compression_CoulombFriction_smoke.xml
+ poromechanics/PoroDelftEggWellbore_smoke.xml
+ poromechanics/PoroViscoDruckerPrager_smoke.xml
+ poromechanicsFractures/Contact/LagrangianMultipliers/PoroElastic_conformingFracture_2d_faultSlip_smoke.xml
+ poromechanicsFractures/EDFM/PoroElastic_efem-edfm_verticalFrac_smoke.xml
+ thermoPoromechanics/ThermoPoroDruckerPrager_consolidation_smoke.xml
+ thermoPoromechanicsFractures/ThermoPoroElastic_conforming_smoke.xml
+ simplePDE/10x10x10Hex_LaplaceVEM_smoke.xml
+ solidMechanics/elasticHollowCylinder_orthotropic_smoke.xml
+ wellbore/CasedThermoElasticWellbore_ImperfectInterfaces_smoke.xml
+ compositionalMultiphaseFlow/2ph_cap_1d_ihu.xml
+ compositionalMultiphaseFlow/soreideWhitson/lockExchange/lockExchange_Z.xml
+ poromechanics/PoroElastic_staircase_co2_3d_fim_stab.xml
+ compositionalMultiphaseWell/staircase_co2_wells_3d.xml
+ constitutiveDriver/testPVT.xml
+ constitutiveDriver/testRelperm_Table_2_phase.xml
+ constitutiveDriver/testTriaxial_druckerPragerExtended.xml
+ thermalMultiphaseFlow/co2_thermal_2d.xml
+ compositionalMultiphaseFlow/4comp_2ph_1d.xml
+ wavePropagation/acous3D_pml_smoke.xml
+ multipleMeshBodies/testMultipleBodies.xml
+ singlePhaseWell/compressible_single_phase_wells_1d.xml
+ simplePDE/10x10x10Hex_LaplaceFEM_smoke.xml
+ poromechanics/PoroElastic_staircase_singlephase_3d_fim_stab.xml
+ phaseField/PhaseFieldFracture_CohesiveModel.xml
+ singlePhaseFlow/incompressible_pebi3d.xml
+ meshGeneration/multiBodyMeshGen_smoke.xml
+ phaseField/PhaseFieldFracture_DamageOnly.xml
+ compositionalMultiphaseFlow/deadoil_3ph_staircase_obl_3d.xml
+ constitutiveDriver/testTriaxial_delftEggCase1.xml
+ constitutiveDriver/testRelperm_VanGenuchten.xml
+ singlePhaseReactiveTransport/mixedReactionCarbonateSystem_1D_smoke.xml
+ wavePropagation/acouselas3D_Q2_abc_smoke.xml
+ compositionalMultiphaseWell/resvol_constraint.xml
+ compositionalMultiphaseWell/isothm_mass_inj_table.xml
+ singlePhaseFlow/PressurePerm_1d_smoke.xml
+ solidMechanics/elasticHollowCylinder_transverseIsotropic_smoke.xml
+ constitutiveDriver/testRelperm_TableHyst2ph.xml
+ thermalMultiphaseFlow/co2_thermal_obl_3d.xml
+ compositionalMultiphaseFlow/c1-ppu/grav_seg_c1ppu_hyst.xml
+ singlePhaseWell/perf_status_test.xml
+ solidMechanics/sedov_finiteStrain_smoke.xml
+ compositionalMultiphaseWell/benchmarks/Class09Pb3/class09_pb3_smoke_3d.xml
+ compositionalMultiphaseWell/benchmarks/Egg/deadOilEgg_smoke_3d.xml
+ wavePropagation/acous3D_vti_smoke.xml
+ compositionalMultiphaseFlow/co2_flux_dirichlet.xml
+ compositionalMultiphaseFlow/dbc/grav_seg_1d/grav_seg_1d_DBC.xml
+ multiphaseFlowFractures/deadOil_fractureMatrixFlow_edfm_horizontalFrac_smoke.xml
+ compositionalMultiphaseFlow/co2_flux_3d.xml
+ solidMechanics/sedov_ssle_smoke.xml
+ singlePhaseFlow/FieldCaseTutorial3_Thermal_smoke.xml
+ constitutiveDriver/testTriaxial_elasticIsotropicPressureDependent.xml
+ singlePhaseWell/staircase_single_phase_wells_3d.xml
+ compositionalMultiphaseWell/staircase_co2_wells_hybrid_3d.xml
+ thermoPoromechanics/ThermoPoroElastic_staircase_co2_smoke.xml
+ thermoPoromechanicsFractures/ThermoPoroElastic_efem-edfm_verticalFrac_smoke.xml
+ poromechanicsFractures/Contact/AugmentedLagrangianMultipliers/ALM_inclinedFault_ISG_smoke.xml
+ efemFractureMechanics/Sneddon_embeddedFrac_staticCondensation_smoke.xml
+ thermalSinglePhaseFlowFractures/fractureMatrixThermalFlow_edfm_smoke.xml
+ phaseField/PhaseFieldFracture_VolDevSplit.xml
+ inducedSeismicity/SeismicityRate_poromechanics_smoke.xml
+ constitutiveDriver/testPVT_CO2BrineTables.xml
+ singlePhaseFlow/sourceFlux_1d.xml
+ singlePhaseFlowFractures/fractureFlow_conforming_2d.xml
+ simplePDE/hybridHexPrism_LaplaceVEM_smoke.xml
+ singlePhaseFlow/vtk/3D_10x10x10_compressible_hex_gravity_smoke.xml
+ singlePhaseFlow/vtk/3D_10x10x10_compressible_tetra_gravity_smoke.xml
+ singlePhaseFlowFractures/fractureMatrixFlow_edfm_horizontalFrac_smoke.xml
+ solidMechanics/gravity.xml
+ lagrangianContactMechanics/ContactMechanics_PEBICrack_smoke.xml
+ singlePhaseFlow/FieldCaseTutorial3_Isothermal_smoke.xml
+ lagrangianContactMechanics/ContactMechanics_UnstructuredCrack_smoke.xml
+ lagrangianContactMechanics/ContactMechanics_Sneddon_smoke.xml
+ lagrangianContactMechanics/ContactMechanics_TFrac_smoke.xml
+ lagrangianContactMechanics/ContactMechanics_SingleFracCompression_smoke.xml
+ lagrangianContactMechanics/ContactMechanics_PassingCrack_smoke.xml
+ lagrangianContactMechanics/ALM_UnstructuredCrack_smoke.xml
+ lagrangianContactMechanics/ALM_Sneddon_smoke.xml
+ lagrangianContactMechanics/ALM_TFrac_smoke.xml
+ lagrangianContactMechanics/ALM_SingleFracCompression_smoke.xml
+ lagrangianContactMechanics/ALM_PassingCrack_smoke.xml )
+
+function( add_coverage_smoke_test )
+ set( options )
+ set( one_value_args NAME MPI_RANKS SCHEMA TIMEOUT )
+ set( multi_value_args INPUTS GEOS_ARGS )
+ cmake_parse_arguments( arg
+ "${options}"
+ "${one_value_args}"
+ "${multi_value_args}"
+ ${ARGN} )
+
+ if( arg_UNPARSED_ARGUMENTS )
+ message( FATAL_ERROR "Unknown arguments for coverage smoke test: ${arg_UNPARSED_ARGUMENTS}" )
+ endif()
+ if( NOT arg_NAME )
+ message( FATAL_ERROR "Coverage smoke test requires NAME" )
+ endif()
+ if( arg_INPUTS AND arg_SCHEMA )
+ message( FATAL_ERROR "Coverage smoke test ${arg_NAME} cannot use both INPUTS and SCHEMA" )
+ endif()
+ if( NOT arg_INPUTS AND NOT arg_SCHEMA )
+ message( FATAL_ERROR "Coverage smoke test ${arg_NAME} requires INPUTS or SCHEMA" )
+ endif()
+
+ set( ctest_name "coverageSmoke_${arg_NAME}" )
+ if( TEST "${ctest_name}" )
+ message( FATAL_ERROR "Duplicate coverage smoke test name: ${ctest_name}" )
+ endif()
+
+ set( input_arguments )
+ foreach( coverage_smoke_input IN LISTS arg_INPUTS )
+ set( input_path "${PROJECT_SOURCE_DIR}/../inputFiles/${coverage_smoke_input}" )
+ if( NOT EXISTS "${input_path}" )
+ message( FATAL_ERROR "Coverage smoke input does not exist: ${input_path}" )
+ endif()
+ list( APPEND input_arguments -i "${input_path}" )
+ endforeach()
+
+ set( output_directory "${CMAKE_CURRENT_BINARY_DIR}/${arg_NAME}" )
+ file( MAKE_DIRECTORY "${output_directory}" )
+
+ set( test_command "$" )
+ if( arg_MPI_RANKS )
+ if( NOT MPIEXEC_EXECUTABLE )
+ message( FATAL_ERROR "Coverage smoke test ${arg_NAME} requires MPI" )
+ endif()
+ set( test_command
+ "${MPIEXEC_EXECUTABLE}"
+ "${MPIEXEC_NUMPROC_FLAG}"
+ "${arg_MPI_RANKS}"
+ ${MPIEXEC_PREFLAGS}
+ "$"
+ ${MPIEXEC_POSTFLAGS} )
+ endif()
+
+ if( arg_SCHEMA )
+ set( run_arguments -s "${output_directory}/${arg_SCHEMA}" ${arg_GEOS_ARGS} )
+ else()
+ set( run_arguments ${input_arguments} ${arg_GEOS_ARGS} -o "${output_directory}" )
+ endif()
+
+ add_test( NAME "${ctest_name}"
+ COMMAND ${test_command} ${run_arguments} )
+
+ if( NOT arg_TIMEOUT )
+ set( arg_TIMEOUT 180 )
+ endif()
+ set_tests_properties( "${ctest_name}"
+ PROPERTIES
+ ENVIRONMENT "OMP_NUM_THREADS=1;OPENBLAS_NUM_THREADS=1"
+ LABELS coverage_smoke
+ TIMEOUT "${arg_TIMEOUT}"
+ WORKING_DIRECTORY "${output_directory}" )
+ if( arg_MPI_RANKS )
+ set_tests_properties( "${ctest_name}"
+ PROPERTIES
+ PROCESSORS "${arg_MPI_RANKS}" )
+ elseif( GEOS_ENABLE_LLVM_SOURCE_COVERAGE )
+ # Each instrumented process flushes every component DSO profile at exit
+ # (about 70 MiB per serial smoke). Account for that I/O weight so a
+ # --parallel 12 run does not issue twelve simultaneous profile flushes.
+ set_tests_properties( "${ctest_name}"
+ PROPERTIES
+ PROCESSORS 4 )
+ endif()
+endfunction()
+
+foreach( coverage_smoke_input IN LISTS coverage_smoke_inputs )
+ get_filename_component( test_name "${coverage_smoke_input}" NAME_WE )
+ set( coverage_smoke_overlays )
+
+ if( test_name STREQUAL "ALM_inclinedFault_ISG_smoke" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/ALM_inclinedFault_small_mesh.xml )
+ elseif( test_name STREQUAL "staircase_co2_wells_hybrid_3d" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/staircase_co2_wells_hybrid_small_mesh.xml )
+ elseif( test_name STREQUAL "acouselas3D_Q2_abc_smoke" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/acouselas3D_Q2_small_mesh.xml )
+ elseif( test_name STREQUAL "ALM_TFrac_smoke" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/TFrac_small_mesh.xml )
+ elseif( test_name STREQUAL "ThermoPoroElastic_staircase_co2_smoke" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/ThermoPoroElastic_staircase_small_mesh.xml )
+ elseif( test_name STREQUAL "PoroElastic_staircase_co2_3d_fim_stab" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/PoroElastic_staircase_fim_small_mesh.xml )
+ elseif( test_name STREQUAL "ThermoPoroDruckerPrager_consolidation_smoke" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/ThermoPoroDruckerPrager_fast_events.xml )
+ elseif( test_name STREQUAL "LagrangeContactBubbleStab_FixedSlip_smoke" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/LagrangeContactBubbleStab_small_mesh.xml )
+ elseif( test_name STREQUAL "PhaseFieldFracture_Nucleation_smoke" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/PhaseFieldFracture_Nucleation_fast_events.xml )
+ elseif( test_name STREQUAL "FieldCaseTutorial3_Thermal_smoke" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/FieldCaseTutorial3_Thermal_small_mesh.xml )
+ elseif( test_name STREQUAL "FieldCaseTutorial3_Isothermal_smoke" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/FieldCaseTutorial3_Isothermal_small_mesh.xml )
+ elseif( test_name STREQUAL "SeismicityRate_poromechanics_smoke" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/SeismicityRate_small_mesh.xml )
+ elseif( test_name STREQUAL "co2_thermal_obl_3d" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/co2_thermal_obl_small_mesh.xml )
+ elseif( test_name STREQUAL "deadOil_fractureMatrixFlow_edfm_horizontalFrac_smoke" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/deadOil_edfm_small_mesh.xml )
+ elseif( test_name STREQUAL "SpringSliderImplicit_A_smoke" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/SpringSliderImplicit_fast_events.xml )
+ elseif( test_name STREQUAL "staircase_co2_wells_3d" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/staircase_co2_wells_small_mesh.xml )
+ elseif( test_name STREQUAL "deadOilEgg_smoke_3d" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/deadOilEgg_small_mesh.xml )
+ elseif( test_name STREQUAL "ContactMechanics_SimpleCubes_smoke" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/ContactMechanics_SimpleCubes_small_mesh.xml )
+ elseif( test_name STREQUAL "grav_seg_1d_DBC" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/grav_seg_1d_DBC_small_mesh.xml )
+ elseif( test_name STREQUAL "co2_flux_dirichlet" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/co2_flux_dirichlet_small_mesh.xml )
+ elseif( test_name STREQUAL "co2_flux_3d" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/co2_flux_3d_small_mesh.xml )
+ elseif( test_name STREQUAL "kgdViscosityDominated_smoke" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/kgdViscosityDominated_small_mesh.xml )
+ elseif( test_name STREQUAL "FlowProppantBedTransport2d_smoke" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/FlowProppantBedTransport2d_fast_outputs.xml )
+ elseif( test_name STREQUAL "isothm_mass_inj_table" )
+ list( APPEND coverage_smoke_overlays
+ coverageSmokes/isothm_mass_inj_table_fast_outputs.xml )
+ endif()
+
+ add_coverage_smoke_test( NAME "${test_name}"
+ INPUTS "${coverage_smoke_input}" ${coverage_smoke_overlays} )
+
+ if( GEOS_ENABLE_LLVM_SOURCE_COVERAGE AND
+ ( test_name STREQUAL "ContactMechanics_SimpleCubes_smoke" OR
+ test_name STREQUAL "LagrangeContactBubbleStab_FixedSlip_smoke" ) )
+ # These nonlinear contact matrices remain sensitive to concurrent
+ # instrumented solves and profile flushes.
+ set_tests_properties( "coverageSmoke_${test_name}"
+ PROPERTIES
+ PROCESSORS 12 )
+ endif()
+endforeach()
+
+# Higher-order DG tetrahedra have very high template multiplicity. Initialize order 3 separately
+# so the order-2 timestep and order-3 setup form a bounded function-coverage milestone.
+add_coverage_smoke_test(
+ NAME acous3D_dg_order2
+ INPUTS wavePropagation/acous3D_dg_order2_coverage.xml )
+
+add_coverage_smoke_test(
+ NAME acous3D_dg_order3_setup
+ INPUTS wavePropagation/acous3D_dg_order2_coverage.xml
+ wavePropagation/acous3D_dg_order3_overlay.xml
+ wavePropagation/acous3D_dg_setup_only_overlay.xml )
+
+# One order-3 timestep atomically exercises the generated mass, stiffness, and surface kernels.
+add_coverage_smoke_test(
+ NAME acous3D_dg_order3
+ INPUTS wavePropagation/acous3D_dg_order2_coverage.xml
+ wavePropagation/acous3D_dg_order3_overlay.xml )
+
+# Schema generation expands every registered object catalog into a complete data-repository tree.
+add_coverage_smoke_test(
+ NAME schema_catalog
+ SCHEMA catalog_schema.xsd )
+
+# The second input is an ordered overlay that reduces each cube problem to a one-cycle smoke test.
+foreach( multiscale_solver IN ITEMS singlePhaseFlow solidMechanics singlePhasePoromechanics )
+ add_coverage_smoke_test(
+ NAME "multiscale_${multiscale_solver}_msrsb"
+ INPUTS "multiscalePreconditioner/${multiscale_solver}/cube_msrsb.xml"
+ "multiscalePreconditioner/cube_coverage_smoke.xml" )
+endforeach()
+
+# These decompositions cover topology migration and partition-only kernels. PROCESSORS prevents
+# CTest --parallel from oversubscribing the twelve CPU slots used for the coverage build.
+add_coverage_smoke_test(
+ NAME SurfaceGenerator_mpi_2x2x2
+ MPI_RANKS 8
+ INPUTS surfaceGeneration/SurfaceGenerator.xml
+ GEOS_ARGS -x 2 -y 2 -z 2 )
+
+add_coverage_smoke_test(
+ NAME mpm_particleRepartition_mpi_2x2x1
+ MPI_RANKS 4
+ INPUTS materialPointMethod/particleRepartition/mpm_particleRepartition.xml
+ GEOS_ARGS -x 2 -y 2 -z 1 )
+
+add_coverage_smoke_test(
+ NAME mpm_dfgMovingGrid_mpi_2x2x1
+ MPI_RANKS 4
+ INPUTS materialPointMethod/dfgMovingGrid/mpm_dfgMovingGrid.xml
+ GEOS_ARGS -x 2 -y 2 -z 1 )
diff --git a/src/coreComponents/integrationTests/linearAlgebraTests/CMakeLists.txt b/src/coreComponents/integrationTests/linearAlgebraTests/CMakeLists.txt
index 96d7e65f993..a329752611b 100644
--- a/src/coreComponents/integrationTests/linearAlgebraTests/CMakeLists.txt
+++ b/src/coreComponents/integrationTests/linearAlgebraTests/CMakeLists.txt
@@ -31,3 +31,11 @@ else()
COMMAND ${test_name} )
endif()
endforeach()
+
+set( test_name testMultiscaleMeshLevelDataTransfer )
+blt_add_executable( NAME ${test_name}
+ SOURCES ${test_name}.cpp
+ OUTPUT_DIR ${TEST_OUTPUT_DIRECTORY}
+ DEPENDS_ON ${decoratedDependencies} ${tplDependencyList} )
+geos_add_test( NAME ${test_name}
+ COMMAND ${test_name} )
diff --git a/src/coreComponents/integrationTests/linearAlgebraTests/testMultiscaleMeshLevelDataTransfer.cpp b/src/coreComponents/integrationTests/linearAlgebraTests/testMultiscaleMeshLevelDataTransfer.cpp
new file mode 100644
index 00000000000..91c396a651e
--- /dev/null
+++ b/src/coreComponents/integrationTests/linearAlgebraTests/testMultiscaleMeshLevelDataTransfer.cpp
@@ -0,0 +1,337 @@
+/*
+ * ------------------------------------------------------------------------------------------------------------
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * Copyright (c) 2026 Lawrence Livermore National Security LLC
+ * Copyright (c) 2026 GEOS/GEOSX Contributors
+ * All rights reserved
+ *
+ * See top level LICENSE, COPYRIGHT, CONTRIBUTORS, NOTICE, and ACKNOWLEDGEMENTS files for details.
+ * ------------------------------------------------------------------------------------------------------------
+ */
+
+/**
+ * @file testMultiscaleMeshLevelDataTransfer.cpp
+ * @brief Tests transfer of the standard array types through multiscale mesh levels.
+ */
+
+#include "linearAlgebra/DofManager.hpp"
+#include "linearAlgebra/multiscale/mesh/MeshData.hpp"
+#include "linearAlgebra/multiscale/mesh/MeshLevel.hpp"
+#include "linearAlgebra/unitTests/testLinearAlgebraUtils.hpp"
+#include "mainInterface/GeosxState.hpp"
+#include "mainInterface/ProblemManager.hpp"
+#include "mainInterface/initialization.hpp"
+#include "mesh/DomainPartition.hpp"
+#include "integrationTests/linearAlgebraTests/testDofManagerUtils.hpp"
+
+#include
+#include
+
+namespace geos
+{
+namespace testing
+{
+
+namespace
+{
+
+char const * const xmlInput =
+ R"xml(
+
+
+
+
+
+
+
+
+ )xml";
+
+/**
+ * Keep this list synchronized with the layouts in types::StandardArrays. Expanding
+ * the operations directly in the test body avoids adding a templated test helper
+ * instantiation for every supported array type to coverage's function denominator.
+ */
+#define GEOS_FOR_EACH_REAL_STANDARD_ARRAY( OP, VALUE_TYPE ) \
+ OP( VALUE_TYPE, 1, RAJA::PERM_I ) \
+ OP( VALUE_TYPE, 2, RAJA::PERM_IJ ) \
+ OP( VALUE_TYPE, 2, RAJA::PERM_JI ) \
+ OP( VALUE_TYPE, 3, RAJA::PERM_IJK ) \
+ OP( VALUE_TYPE, 3, RAJA::PERM_IKJ ) \
+ OP( VALUE_TYPE, 3, RAJA::PERM_JIK ) \
+ OP( VALUE_TYPE, 3, RAJA::PERM_JKI ) \
+ OP( VALUE_TYPE, 3, RAJA::PERM_KIJ ) \
+ OP( VALUE_TYPE, 3, RAJA::PERM_KJI ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_IJKL ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_IJLK ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_IKJL ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_IKLJ ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_ILJK ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_ILKJ ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_JIKL ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_JILK ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_JKIL ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_JKLI ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_JLIK ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_JLKI ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_KIJL ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_KILJ ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_KJIL ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_KJLI ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_KLIJ ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_KLJI ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_LIJK ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_LIKJ ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_LJIK ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_LJKI ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_LKIJ ) \
+ OP( VALUE_TYPE, 4, RAJA::PERM_LKJI )
+
+#define GEOS_FOR_EACH_INTEGRAL_STANDARD_ARRAY( OP, VALUE_TYPE ) \
+ OP( VALUE_TYPE, 1, RAJA::PERM_I ) \
+ OP( VALUE_TYPE, 2, RAJA::PERM_IJ ) \
+ OP( VALUE_TYPE, 2, RAJA::PERM_JI )
+
+#define GEOS_FOR_EACH_STANDARD_ARRAY( OP ) \
+ GEOS_FOR_EACH_REAL_STANDARD_ARRAY( OP, real32 ) \
+ GEOS_FOR_EACH_REAL_STANDARD_ARRAY( OP, real64 ) \
+ GEOS_FOR_EACH_INTEGRAL_STANDARD_ARRAY( OP, integer ) \
+ GEOS_FOR_EACH_INTEGRAL_STANDARD_ARRAY( OP, localIndex ) \
+ GEOS_FOR_EACH_INTEGRAL_STANDARD_ARRAY( OP, globalIndex )
+
+class MultiscaleMeshLevelDataTransferTest : public ::testing::Test
+{
+protected:
+
+ MultiscaleMeshLevelDataTransferTest()
+ : m_state( std::make_unique< CommandLineOptions >() ),
+ m_domain( m_state.getProblemManager().getDomainPartition() ),
+ m_fineMesh( "fine" ),
+ m_coarseMesh( "coarse" )
+ {
+ setupProblemFromXML( &m_state.getProblemManager(), xmlInput );
+
+ DofManager supportManager( "multiscaleTransferSupport" );
+ supportManager.setDomain( m_domain );
+ stdVector< DofManager::FieldSupport > const support =
+ { { "mesh", "Level0", { "region" } } };
+ supportManager.addField( "field",
+ FieldLocation::Elem,
+ 1,
+ support );
+ m_fineMesh.buildFineMesh( m_domain, supportManager.support( "field" ) );
+
+ LinearSolverParameters::Multiscale::Coarsening coarsening;
+ coarsening.partitionType = LinearSolverParameters::Multiscale::Coarsening::PartitionType::cartesian;
+ coarsening.ratio.resize( 3 );
+ coarsening.ratio[0] = 2;
+ coarsening.ratio[1] = 2;
+ coarsening.ratio[2] = 2;
+ m_coarseMesh.buildCoarseMesh( m_fineMesh,
+ coarsening,
+ { "xneg", "xpos", "yneg", "ypos", "zneg", "zpos" } );
+ }
+
+ GeosxState m_state;
+ DomainPartition & m_domain;
+ multiscale::MeshLevel m_fineMesh;
+ multiscale::MeshLevel m_coarseMesh;
+};
+
+TEST_F( MultiscaleMeshLevelDataTransferTest, standardArraysReachOriginalMesh )
+{
+ stdVector< string > fieldNames;
+ localIndex fieldIndex = 0;
+
+ #define GEOS_REGISTER_TRANSFER_ARRAY( VALUE_TYPE, NDIM, PERMUTATION ) \
+ do \
+ { \
+ using ArrayType = Array< VALUE_TYPE, NDIM, PERMUTATION >; \
+ string const fieldName = "standardArray_" + std::to_string( fieldIndex ); \
+ fieldNames.emplace_back( fieldName ); \
+ \
+ ArrayType & fineCellField = \
+ m_fineMesh.cellManager().registerWrapper< ArrayType >( fieldName ).reference(); \
+ std::array< localIndex, NDIM > fineCellDims; \
+ fineCellDims.fill( 2 ); \
+ fineCellDims[0] = m_fineMesh.cellManager().size(); \
+ fineCellField.resize( NDIM, fineCellDims.data() ); \
+ for( localIndex i = 0; i < fineCellField.size(); ++i ) \
+ { \
+ fineCellField.data()[i] = static_cast< VALUE_TYPE >( 256 + 512 * fieldIndex + i ); \
+ } \
+ \
+ ArrayType & fineNodeField = \
+ m_fineMesh.nodeManager().registerWrapper< ArrayType >( fieldName ).reference(); \
+ std::array< localIndex, NDIM > fineNodeDims; \
+ fineNodeDims.fill( 2 ); \
+ fineNodeDims[0] = m_fineMesh.nodeManager().size(); \
+ fineNodeField.resize( NDIM, fineNodeDims.data() ); \
+ for( localIndex i = 0; i < fineNodeField.size(); ++i ) \
+ { \
+ fineNodeField.data()[i] = static_cast< VALUE_TYPE >( 32768 + 512 * fieldIndex + i ); \
+ } \
+ \
+ ArrayType & cellField = \
+ m_coarseMesh.cellManager().registerWrapper< ArrayType >( fieldName ).reference(); \
+ std::array< localIndex, NDIM > cellDims; \
+ cellDims.fill( 2 ); \
+ cellDims[0] = m_coarseMesh.cellManager().size(); \
+ cellField.resize( NDIM, cellDims.data() ); \
+ for( localIndex i = 0; i < cellField.size(); ++i ) \
+ { \
+ cellField.data()[i] = static_cast< VALUE_TYPE >( 1024 + 512 * fieldIndex + i ); \
+ } \
+ \
+ ArrayType & nodeField = \
+ m_coarseMesh.nodeManager().registerWrapper< ArrayType >( fieldName ).reference(); \
+ std::array< localIndex, NDIM > nodeDims; \
+ nodeDims.fill( 2 ); \
+ nodeDims[0] = m_coarseMesh.nodeManager().size(); \
+ nodeField.resize( NDIM, nodeDims.data() ); \
+ for( localIndex i = 0; i < nodeField.size(); ++i ) \
+ { \
+ nodeField.data()[i] = static_cast< VALUE_TYPE >( 65536 + 512 * fieldIndex + i ); \
+ } \
+ ++fieldIndex; \
+ } while( false );
+
+ GEOS_FOR_EACH_STANDARD_ARRAY( GEOS_REGISTER_TRANSFER_ARRAY )
+#undef GEOS_REGISTER_TRANSFER_ARRAY
+
+ ASSERT_EQ( fieldIndex, 75 );
+ m_fineMesh.writeCellData( fieldNames );
+ m_fineMesh.writeNodeData( fieldNames );
+ m_coarseMesh.writeCellData( fieldNames );
+ m_coarseMesh.writeNodeData( fieldNames );
+
+ geos::MeshLevel & sourceMesh = m_domain.getMeshBody( "mesh" ).getMeshLevel( "Level0" );
+ ElementSubRegionBase & sourceSubRegion = sourceMesh.getElemManager().getRegion( 0 ).getSubRegion( 0 );
+
+ arrayView1d< localIndex const > const coarseCellIndex =
+ m_fineMesh.cellManager().getField< fields::multiscale::CoarseCellLocalIndex >();
+ arrayView1d< localIndex const > const origRegion =
+ m_fineMesh.cellManager().getField< fields::multiscale::OrigElementRegion >();
+ arrayView1d< localIndex const > const origSubRegion =
+ m_fineMesh.cellManager().getField< fields::multiscale::OrigElementSubRegion >();
+ arrayView1d< localIndex const > const origCellIndex =
+ m_fineMesh.cellManager().getField< fields::multiscale::OrigElementIndex >();
+ for( localIndex i = 0; i < m_fineMesh.cellManager().size(); ++i )
+ {
+ ASSERT_EQ( origRegion[i], 0 );
+ ASSERT_EQ( origSubRegion[i], 0 );
+ }
+
+ arrayView1d< localIndex const > const fineNodeIndex =
+ m_coarseMesh.nodeManager().getField< fields::multiscale::FineNodeLocalIndex >();
+ arrayView1d< localIndex const > const origNodeIndex =
+ m_fineMesh.nodeManager().getField< fields::multiscale::OrigNodeIndex >();
+
+ fieldIndex = 0;
+ #define GEOS_VERIFY_TRANSFERRED_ARRAY( VALUE_TYPE, NDIM, PERMUTATION ) \
+ do \
+ { \
+ using ArrayType = Array< VALUE_TYPE, NDIM, PERMUTATION >; \
+ string const fieldName = "standardArray_" + std::to_string( fieldIndex ); \
+ string const fineOutputName = m_fineMesh.name() + '_' + fieldName; \
+ string const coarseOutputName = m_coarseMesh.name() + '_' + fieldName; \
+ \
+ ArrayType const & fineCellField = \
+ m_fineMesh.cellManager().getReference< ArrayType >( fieldName ); \
+ ArrayType const & fineOutputCellField = \
+ sourceSubRegion.getReference< ArrayType >( fineOutputName ); \
+ for( integer dim = 1; dim < NDIM; ++dim ) \
+ { \
+ EXPECT_EQ( fineOutputCellField.size( dim ), fineCellField.size( dim ) ); \
+ } \
+ for( localIndex i = 0; i < m_fineMesh.cellManager().size(); ++i ) \
+ { \
+ LvArray::forValuesInSliceWithIndices( fineCellField[i], \
+ [&]( auto const & expectedValue, auto const ... indices ) \
+{ \
+ EXPECT_EQ( fineOutputCellField( origCellIndex[i], indices ... ), expectedValue ); \
+} ); \
+ } \
+ \
+ ArrayType const & coarseCellField = \
+ m_coarseMesh.cellManager().getReference< ArrayType >( fieldName ); \
+ ArrayType const & coarseOutputCellField = \
+ sourceSubRegion.getReference< ArrayType >( coarseOutputName ); \
+ for( integer dim = 1; dim < NDIM; ++dim ) \
+ { \
+ EXPECT_EQ( coarseOutputCellField.size( dim ), coarseCellField.size( dim ) ); \
+ } \
+ for( localIndex i = 0; i < m_fineMesh.cellManager().size(); ++i ) \
+ { \
+ LvArray::forValuesInSliceWithIndices( coarseCellField[coarseCellIndex[i]], \
+ [&]( auto const & expectedValue, auto const ... indices ) \
+{ \
+ EXPECT_EQ( coarseOutputCellField( origCellIndex[i], indices ... ), expectedValue ); \
+} ); \
+ } \
+ \
+ ArrayType const & fineNodeField = \
+ m_fineMesh.nodeManager().getReference< ArrayType >( fieldName ); \
+ ArrayType const & fineOutputNodeField = \
+ sourceMesh.getNodeManager().getReference< ArrayType >( fineOutputName ); \
+ for( integer dim = 1; dim < NDIM; ++dim ) \
+ { \
+ EXPECT_EQ( fineOutputNodeField.size( dim ), fineNodeField.size( dim ) ); \
+ } \
+ for( localIndex i = 0; i < m_fineMesh.nodeManager().size(); ++i ) \
+ { \
+ LvArray::forValuesInSliceWithIndices( fineNodeField[i], \
+ [&]( auto const & expectedValue, auto const ... indices ) \
+{ \
+ EXPECT_EQ( fineOutputNodeField( origNodeIndex[i], indices ... ), expectedValue ); \
+} ); \
+ } \
+ \
+ ArrayType const & coarseNodeField = \
+ m_coarseMesh.nodeManager().getReference< ArrayType >( fieldName ); \
+ ArrayType const & coarseOutputNodeField = \
+ sourceMesh.getNodeManager().getReference< ArrayType >( coarseOutputName ); \
+ for( integer dim = 1; dim < NDIM; ++dim ) \
+ { \
+ EXPECT_EQ( coarseOutputNodeField.size( dim ), coarseNodeField.size( dim ) ); \
+ } \
+ for( localIndex i = 0; i < m_coarseMesh.nodeManager().size(); ++i ) \
+ { \
+ LvArray::forValuesInSliceWithIndices( coarseNodeField[i], \
+ [&]( auto const & expectedValue, auto const ... indices ) \
+{ \
+ EXPECT_EQ( coarseOutputNodeField( origNodeIndex[fineNodeIndex[i]], indices ... ), \
+ expectedValue ); \
+} ); \
+ } \
+ ++fieldIndex; \
+ } while( false );
+
+ GEOS_FOR_EACH_STANDARD_ARRAY( GEOS_VERIFY_TRANSFERRED_ARRAY )
+#undef GEOS_VERIFY_TRANSFERRED_ARRAY
+
+ EXPECT_EQ( fieldIndex, 75 );
+}
+
+#undef GEOS_FOR_EACH_STANDARD_ARRAY
+#undef GEOS_FOR_EACH_INTEGRAL_STANDARD_ARRAY
+#undef GEOS_FOR_EACH_REAL_STANDARD_ARRAY
+
+} // namespace
+} // namespace testing
+} // namespace geos
+
+int main( int argc, char * * argv )
+{
+ geos::testing::LinearAlgebraTestScope scope( argc, argv );
+ return RUN_ALL_TESTS();
+}
diff --git a/src/coreComponents/linearAlgebra/multiscale/mesh/MeshLevel.cpp b/src/coreComponents/linearAlgebra/multiscale/mesh/MeshLevel.cpp
index b45e08c7e52..aee193f27af 100644
--- a/src/coreComponents/linearAlgebra/multiscale/mesh/MeshLevel.cpp
+++ b/src/coreComponents/linearAlgebra/multiscale/mesh/MeshLevel.cpp
@@ -519,7 +519,7 @@ void MeshLevel::writeCellDataCoarse( stdVector< string > const & fieldNames, int
dims[0] = dstField.size( 0 );
dstField.resize( ArrayType::NDIM, dims.data );
- forAll< parallelHostPolicy >( dstField.size(), [=, dstField = dstField.toView()]( localIndex const ic )
+ forAll< parallelHostPolicy >( dstField.size( 0 ), [=, dstField = dstField.toView()]( localIndex const ic )
{
LvArray::forValuesInSliceWithIndices( dstField[ ic ],
[&]( auto & dstVal, auto const ... indices )
diff --git a/src/docs/sphinx/buildGuide/BuildProcess.rst b/src/docs/sphinx/buildGuide/BuildProcess.rst
index 3fcff6c15b3..84349e83fe8 100644
--- a/src/docs/sphinx/buildGuide/BuildProcess.rst
+++ b/src/docs/sphinx/buildGuide/BuildProcess.rst
@@ -72,7 +72,7 @@ Option Default Explanation
``ENABLE_DOCS`` ``ON`` Build documentation (Sphinx and Doxygen)
``ENABLE_WARNINGS_AS_ERRORS`` ``ON`` Treat all warnings as errors
``ENABLE_TOTALVIEW_OUTPUT`` ``OFF`` Enables TotalView debugger custom view of GEOS data structures
-``ENABLE_COV`` ``OFF`` Enables code coverage
+``ENABLE_COVERAGE`` ``OFF`` Enables BLT's legacy gcov-compatible coverage backend
``GEOS_ENABLE_TESTS`` ``ON`` Enables unit testing targets
``GEOS_LA_INTERFACE`` ``Hypre`` Choiсe of Linear Algebra backend (Hypre/Petsc/Trilinos)
``GEOS_BUILD_OBJ_LIBS`` ``ON`` Use CMake Object Libraries build
@@ -81,3 +81,10 @@ Option Default Explanation
``GEOS_PARALLEL_LINK_JOBS`` Max. number of link jobs (when using Ninja), in addition to ``-j`` flag
``GEOS_INSTALL_SCHEMA`` ``ON`` Enables schema generation and installation
=============================== ========= ==============================================================================
+
+For compiler-native Clang source coverage, configure a CPU shared-library build
+with ``GEOS_ENABLE_LLVM_SOURCE_COVERAGE=ON``. The option rejects BLT's legacy
+``ENABLE_COVERAGE`` mode if a host configuration enables it, because the two
+options select different and incompatible profile formats. The CI wrapper
+exposes only the LLVM choice; ``scripts/llvm_source_branch_coverage.sh``
+generates the production report from the resulting raw profiles.
diff --git a/src/docs/sphinx/developerGuide/Contributing/CodeGeosFeatures.rst b/src/docs/sphinx/developerGuide/Contributing/CodeGeosFeatures.rst
index b72fee6159f..e90d1f45b55 100644
--- a/src/docs/sphinx/developerGuide/Contributing/CodeGeosFeatures.rst
+++ b/src/docs/sphinx/developerGuide/Contributing/CodeGeosFeatures.rst
@@ -161,10 +161,36 @@ Unit testing is not about testing every single line of code (an unrealistic goal
Code Coverage
^^^^^^^^^^^^^
-**Code coverage should never decrease.** New code contributions must maintain or improve overall code coverage.
-Use Codecov to report untested code paths.
-
-Currently, Codecov does not cover integrated tests execution but they should be taken into account, especially for minor physical kernel changes.
+New code contributions should maintain or improve overall code coverage. The
+dedicated Clang/LLVM CI job runs the ``coverage_smoke`` suite and enforces the
+repository-owned lines, functions, and canonical branch floors in
+``.github/coverage-thresholds.json``. The initial floors are 70.01% for lines,
+75.01% for functions, and 50.01% for canonical branches. Regions and the native
+instantiation-weighted branch interpretation are reported for review without
+being hard gates. The compiler-native report is published in the GitHub job
+summary and retained as a single self-contained ``index.html`` workflow
+artifact; the page puts the reviewer summary and coverage tables first, with
+the intermediate JSON, logs, and source reports in collapsible diagnostic
+sections. No hosted coverage service or secret is required.
+
+Coverage is opt-in for pull requests: add the ``ci: run code coverage`` label
+to request the job. The job also runs on pushes to ``develop`` so that trusted
+baseline artifacts can be retained.
+
+For pull requests, the same job also intersects the tested merge commit's diff
+with LLVM's detailed source records. It reports coverage of changed executable
+lines, new function entry sites, and native branch outcomes, then ranks the
+changed locations with the largest misses and suggests the kind of case needed
+to exercise each one. When an artifact exists for the exact target-branch
+commit, the summary also shows the canonical project coverage change from that
+base. A missing or incompatible base artifact is reported as unavailable rather
+than substituted with a different commit. These PR-specific signals are
+advisory; the repository coverage floors remain the enforced policy. The job's
+reviewer summary clearly separates those gates from unavailable baseline
+comparisons and uncovered changed lines.
+
+Integrated tests are not part of this focused CI coverage pass, but they should
+still be taken into account, especially for minor physical kernel changes.
Integrated Tests & Examples
^^^^^^^^^^^^^^^^^^^^^^^^^^^