Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ RUN case "${SANCOV_INSTRUMENTATION_MODE:-bb}" in \
&& echo "${llvm_build_secs}" > /work/.llvm-build-time \
&& echo "=== fuzz-fill: LLVM build wall time: ${llvm_build_secs}s ==="

# llvm-reduce is a Release helper (not instrumented); install from the bootstrap
# toolchain so reduction works without rebuilding the entire sancov tree.
RUN cp /work/llvm-release/bin/llvm-reduce /work/llvm-build-sancov/bin/llvm-reduce

FROM ubuntu:24.04 AS final

ARG DEBIAN_FRONTEND=noninteractive
Expand All @@ -139,6 +143,7 @@ COPY --chown=${UID}:${GID} --from=llvm-builder /work/llvm-build-sancov /work/llv

COPY --chown=${UID}:${GID} pyproject.toml LICENCE.txt README.md /work/fuzz-fill/
COPY --chown=${UID}:${GID} src /work/fuzz-fill/src
COPY --chown=${UID}:${GID} example /work/fuzz-fill/example
COPY --chown=${UID}:${GID} tests /work/fuzz-fill/tests
COPY --chown=${UID}:${GID} integration-tests /work/fuzz-fill/integration-tests
COPY --chown=${UID}:${GID} scripts /work/fuzz-fill/scripts
Expand Down
186 changes: 180 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,22 @@ fuzz-fill identifies coverage gaps in LLVM:
1. **Gap finding** — measure baseline coverage achieved by LLVM's LIT test suite and produce a list of uncovered lines. At present `fuzz-fill` supports finding gaps in the AMDGPU and SPIR-V backends; this will be extended in the future to other backends and parts of the LLVM codebase.
- **Baseline** — all uncovered lines in a user-specified part of the LLVM codebase.
- **PR** — added or changed lines in a commit that baseline coverage still misses.

Gap filling (running a fuzz corpus against the gap list) will follow in a separate PR.
2. **Gap filling** — run a fuzz corpus against the gap list and report which gaps each candidate test covers.
3. **Reduction** — scaffold and run testcase reduction for the first *N* gap-fill hits, producing minimal LIT-ready tests.

```
┌─ Gap finding ────────────────────────────────────────────────┐
│ │
│ Baseline ──┐ │
│ ├──► Uncovered lines (CSV) │
│ PR ────────┘ │
└───────────────────────────────┬──────────────────────────────┘
┌─ Gap filling ──────────────────────────────────────────────────┐
│ candidate-test ──► incremental ──► new_coverage.csv │
└───────────────────────────────┬──────────────────────────────┘
┌─ Reduction ──────────────────────────────────────────────────┐
│ batch-from-coverage (-n N) ──► t-00001-*/ … reduced.* │
└──────────────────────────────────────────────────────────────┘
```

Expand Down Expand Up @@ -68,6 +75,8 @@ Use `spirv` instead of `amdgpu` for SPIR-V backend tests.
- [LLVM builds](#llvm-builds)
- [Gap finding (baseline)](#gap-finding-baseline)
- [Gap finding (PR)](#gap-finding-pr)
- [Gap filling](#gap-filling)
- [Reduction](#reduction)
- [CLI reference](#cli-reference)
- [Uncovered-lines CSV contract](#uncovered-lines-csv-contract)
- [Environment variables](#environment-variables)
Expand All @@ -77,6 +86,9 @@ Use `spirv` instead of `amdgpu` for SPIR-V backend tests.
- [PR image build and reuse](#pr-image-build-and-reuse)
- [Gap finding (baseline) in Docker](#gap-finding-baseline-in-docker)
- [Gap finding (PR) in Docker](#gap-finding-pr-in-docker)
- [Gap filling in Docker](#gap-filling-in-docker)
- [Reduction in Docker](#reduction-in-docker)
- [End-to-end workflow in Docker](#end-to-end-workflow-in-docker)
- [Run integration tests](#run-integration-tests)
- [Run a container](#run-a-container)
- [Tests](#tests)
Expand Down Expand Up @@ -250,6 +262,82 @@ Under `$OUTPUT_DIR`:

---

## Gap filling

[`scripts/gap-filling.sh`](scripts/gap-filling.sh) runs `coverage candidate-test` then `coverage incremental` against a gap list from [gap finding](#gap-finding-baseline) or [PR gap finding](#gap-finding-pr).

**Inputs:** `line_coverage_uncovered.csv` (or `target_lines_uncovered.csv`) plus matching `llc_address_line_map.csv` from the same baseline run, and a fuzz corpus directory.

**Main output:** `<output-dir>/incremental/new_coverage.csv` with columns `test_name`, `file`, `line`, `covered-points`.

```bash
./scripts/gap-filling.sh \
--output-dir ./data/gap-fill-out \
--line-coverage-uncovered-csv ./data/baseline/line_coverage_uncovered.csv \
--llc-address-line-map-csv ./data/baseline/llc_address_line_map.csv \
--candidate-tests-dir ./candidate-tests-dataset/amdgcn-amd-amdhsa \
-n 100 \
--llvm-repo /path/llvm-project \
--llvm-bin /path/llvm-project/build/bin \
--instrumented-bin-dir /path/llvm-project/build-sancov/bin \
-j "$(nproc)"
```

Docker: [`scripts/docker/gap-filling.sh`](scripts/docker/gap-filling.sh) — same flags plus `--image` / `--pr-id`.

---

## Reduction

[`scripts/reduction.sh`](scripts/reduction.sh) scaffolds (and optionally runs) reduction for the **first N rows** of `incremental/new_coverage.csv` from a [gap-fill](#gap-filling) output directory. Each row becomes a case directory `t-00001-<short>/` with `config.json`, `interesting_ir.sh`, and a copied `.bc` (mirroring [`example/amd/new-test-1/`](example/amd/new-test-1/)).

**Default output:** `<gap-fill-dir>/reduced/` (override with `--output-dir`).

```bash
# Scaffold one hit (inspect harness before running llvm-reduce):
./scripts/reduction.sh \
--gap-fill-dir ./data/gap-fill-out \
-n 1 \
--scaffold-only

# Scaffold and reduce the first three hits:
./scripts/reduction.sh \
--gap-fill-dir ./data/gap-fill-out \
-n 3 \
--llvm-repo /path/llvm-project \
--llvm-bin /path/llvm-project/build/bin \
--instrumented-bin-dir /path/llvm-project/build-sancov/bin \
--pipeline llvm_reduce_ir
```

Docker: [`scripts/docker/reduction.sh`](scripts/docker/reduction.sh).

**Single-case reduction** (hand-written harness under [`example/`](example/)):

```bash
python -m reduce \
--config example/amd/si-i1-copies/config.json \
--llc /path/llvm-project/build-sancov/bin/llc \
--llvm-reduce /path/llvm-project/build/bin/llvm-reduce
```

**CLI directly:**

```bash
python -m reduce batch-from-coverage \
--csv ./data/gap-fill-out/incremental/new_coverage.csv \
--candidate-tests ./data/gap-fill-out/candidate_tests \
--output ./data/reduced-out \
--n 3 \
--scaffold-only
```

See `python -m reduce batch-from-coverage --help` for pipeline options (`--pipeline`, `--pass-under-test`, `--mtriple`, `--with-creduce`, …).

Integration test: [`integration-tests/reduction-batch.test`](integration-tests/reduction-batch.test) (scaffold-only, `n=1` and `n=3`).

---

## CLI reference

The scripts above call these modules. Use `--help` on any command for the full flag list.
Expand All @@ -261,7 +349,8 @@ The scripts above call these modules. Use `--help` on any command for the full f
| `python -m coverage incremental` | Suite gaps filled by fuzz tests (gap filling) |
| `python -m coverage target-lines` | PR added lines vs `line_coverage_uncovered.csv` |
| `python -m added_lines` | Lines added by a git commit |
| `python -m reduce` | Testcase reduction |
| `python -m reduce` | Single-case testcase reduction (`--config`) |
| `python -m reduce batch-from-coverage` | Scaffold (and optionally run) reduction for the first N gap-fill hits |

### Uncovered-lines CSV contract

Expand Down Expand Up @@ -366,7 +455,7 @@ Optional commands outside the main workflows in fuzz-fill.

The Docker image bundles an official LLVM release bootstrap, a dual-build SanitizerCoverage LLVM tree (instrumented `llc`/`opt` plus Release helpers), and a fuzz-fill venv. Use it when you want to run integration tests or experiment without building LLVM locally.

**Scripts** (under [`scripts/docker/`](scripts/docker/)): [`build-image.sh`](scripts/docker/build-image.sh), [`build-image-pr.sh`](scripts/docker/build-image-pr.sh), [`ensure-image.sh`](scripts/docker/ensure-image.sh), [`gap-finding-baseline.sh`](scripts/docker/gap-finding-baseline.sh), [`gap-finding-pr.sh`](scripts/docker/gap-finding-pr.sh), [`test-image.sh`](scripts/docker/test-image.sh), [`tmp-container.sh`](scripts/docker/tmp-container.sh)
**Scripts** (under [`scripts/docker/`](scripts/docker/)): [`build-image.sh`](scripts/docker/build-image.sh), [`build-image-pr.sh`](scripts/docker/build-image-pr.sh), [`ensure-image.sh`](scripts/docker/ensure-image.sh), [`gap-finding-baseline.sh`](scripts/docker/gap-finding-baseline.sh), [`gap-finding-pr.sh`](scripts/docker/gap-finding-pr.sh), [`gap-filling.sh`](scripts/docker/gap-filling.sh), [`reduction.sh`](scripts/docker/reduction.sh), [`run-full-workflow.sh`](scripts/docker/run-full-workflow.sh), [`test-image.sh`](scripts/docker/test-image.sh), [`tmp-container.sh`](scripts/docker/tmp-container.sh)

The image bakes a copy of fuzz-fill at `/work/fuzz-fill` when built. Pass **`--bind-repo`** on a docker runner to mount your local checkout over that path (venv stays at `/work/fuzz-fill-venv`) when you need code that is newer than the image.

Expand Down Expand Up @@ -508,6 +597,91 @@ For AMDGPU images, baseline defaults to the twelve LIT prefixes in [`scripts/lit

Main output: `<output-dir>/commit_lines_report/target_lines_uncovered.csv`.

### Gap filling in Docker

[`scripts/docker/gap-filling.sh`](scripts/docker/gap-filling.sh) runs `candidate-test` and `incremental` in a container. Requires gap-list CSV paths and a bind-mounted corpus.

```bash
./scripts/docker/gap-filling.sh \
--output-dir ./data/gap-fill-out \
--line-coverage-uncovered-csv ./data/baseline/line_coverage_uncovered.csv \
--llc-address-line-map-csv ./data/baseline/llc_address_line_map.csv \
--candidate-tests-dir /path/to/corpus \
-n 100 \
-j "$(nproc)"
```

Main output: `<output-dir>/incremental/new_coverage.csv`.

### Reduction in Docker

[`scripts/docker/reduction.sh`](scripts/docker/reduction.sh) runs `batch-from-coverage` in a container. Gap-fill output is mounted read-only at `/mounted-gap-fill/`; case directories are written under `--output-dir` (default: `<gap-fill-dir>/reduced/`).

```bash
./scripts/docker/reduction.sh \
--bind-repo \
--gap-fill-dir ./data/gap-fill-out \
--candidate-corpus-dir ./candidate-tests-dataset/amdgcn-amd-amdhsa \
-n 1

./scripts/docker/reduction.sh \
--gap-fill-dir ./data/gap-fill-out \
-n 3 \
--scaffold-only
```

Pass **`--candidate-corpus-dir`** when gap filling used a bind-mounted fuzz corpus (required so reduction can resolve each test’s input `.bc`/`.ll` from `test.sh`). For **`.bc` inputs**, full reduction also needs `llvm-dis` (the image sets `FUZZ_FILL_LLVM_DIS`; with `--bind-repo`, your local checkout supplies the wiring).

### End-to-end workflow in Docker

[`scripts/docker/run-full-workflow.sh`](scripts/docker/run-full-workflow.sh) runs the full pipeline in one shot: **gap finding (baseline)** → **gap filling** → **reduction**. It is intended for local smoke testing of the Docker runners.

**Prerequisites:** a built Docker image ([`build-image.sh`](#build)) and, for a real fuzz corpus, a local checkout of the candidate tests (for example `./candidate-tests-dataset/amdgcn-amd-amdhsa` — not baked into the image; gap filling bind-mounts it from the host).

**`--bind-repo` is on by default** — every step mounts your local fuzz-fill checkout at `/work/fuzz-fill` (venv stays at `/work/fuzz-fill-venv`). Use **`--no-bind-repo`** to run the copy baked into the image instead.

Quick smoke test (tiny fixture corpus, scaffold-only reduction):

```bash
./scripts/docker/run-full-workflow.sh
```

Full reduction with the AMDGPU fuzz corpus:

```bash
CORPUS=./candidate-tests-dataset/amdgcn-amd-amdhsa \
GAP_FILL_N=20 \
FULL_REDUCE=1 \
./scripts/docker/run-full-workflow.sh
```

Build the image first, then run the workflow:

```bash
LLVM=/path/to/llvm-project ./scripts/docker/run-full-workflow.sh --build-image
```

| Option / env | Meaning |
|--------------|---------|
| `--build-image` | Build `fuzz-fill-test:latest` before running (pass `LLVM=...` if not `../llvm-project`) |
| `--no-bind-repo` | Use fuzz-fill from the image instead of mounting the local checkout |
| `DATA` | Output root (default: `./data/workflow-test`) |
| `CORPUS` | Fuzz corpus for gap filling and reduction (default: `integration-tests/fixtures/coverage-new-tests`) |
| `GAP_FILL_N` | Run the first *N* candidate tests from `CORPUS` (default: `1`) |
| `REDUCE_N` | Reduce the first *N* rows of `new_coverage.csv` (default: `1`) |
| `FULL_REDUCE` | If `1`, run `llvm-reduce` (default: `0` = scaffold-only) |
| `J` | Parallel jobs (default: `nproc`) |

On success, outputs land under `$DATA` (default `./data/workflow-test/`):

| Path | Contents |
|------|----------|
| `gap-finding/baseline/` | Baseline gap list |
| `gap-fill/incremental/new_coverage.csv` | Gap-fill hits |
| `gap-fill/reduced/t-00001-*/` | Reduction harness (and `reduced/reduced.ll` when `FULL_REDUCE=1`) |

By default, reduction processes the **first row** of `new_coverage.csv`. To target a specific test, reorder or filter that CSV before re-running [`reduction.sh`](#reduction-in-docker), or increase `REDUCE_N`.

### Run integration tests

```bash
Expand Down Expand Up @@ -624,7 +798,7 @@ export GH_TOKEN="$(gh auth token)" # or ${{ github.token }} in CI
-v integration-tests/e2e/gap-finding-pr/
```

E2E tests use [`scripts/prepare-pr-llvm.sh`](scripts/prepare-pr-llvm.sh) with `--plain-clone` (same squash + self-contained export as the Docker PR path), then build SanitizerCoverage and run gap finding. The first run can take several hours (LLVM build + amdgpu LIT slice).
E2E tests use [`scripts/prepare-pr-llvm.sh`](scripts/prepare-pr-llvm.sh) with `--plain-clone` (same squash + self-contained export as the Docker PR path), then build SanitizerCoverage and run gap finding, gap filling, and **llvm-reduce** on the pinned gap-fill fixture. The first run can take several hours (LLVM build + amdgpu LIT slice). The reduction step checks that `reduced/reduced.ll` exists and is smaller than the disassembled input (not a byte-for-byte golden match).

---

Expand Down
8 changes: 8 additions & 0 deletions integration-tests/e2e/gap-finding-pr/pr-214457.test
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@
# RUN: %FileCheck %s --input-file=%t/gap-fill-pr/incremental/new_coverage.csv --check-prefix=GAP_FILL_PR_NOT
# GAP_FILL_PR_NOT-NOT: SIFoldOperands.cpp,1712,

# COM: Reduction: full llvm-reduce on the first baseline gap-fill hit.
# COM: Assert reduced.ll exists, is smaller than disassembled input, and preserves COVERED.
# RUN: %repo-root/scripts/reduction.sh --gap-fill-dir %t/gap-fill-baseline --candidate-corpus-dir %S/../fixtures/candidate-tests --llvm-repo %t/llvm-project --llvm-bin %t/build-sancov/bin --instrumented-bin-dir %t/build-sancov/bin -n 1 --pipeline llvm_reduce_ir
# RUN: test -f %t/gap-fill-baseline/reduced/t-00001-test_0_0/reduced/reduced.ll
# RUN: bash -c 'd=%t/gap-fill-baseline/reduced/t-00001-test_0_0; input="$d/test_0_0000a5c12a4ab6566d1f8bf7495ff7fa30d9c93f326071d7e75cea0a1172a0c5.bc"; reduced="$d/reduced/reduced.ll"; bl=$(mktemp); %t/build-sancov/bin/llvm-dis -o "$bl" "$input"; test $(stat -c%s "$reduced") -lt $(stat -c%s "$bl"); rm -f "$bl"'
# RUN: %t/gap-fill-baseline/reduced/t-00001-test_0_0/interesting_ir.sh %t/gap-fill-baseline/reduced/t-00001-test_0_0/reduced/reduced.ll 2>&1 | %FileCheck %s --check-prefix=REDUCED_COVERAGE
# REDUCED_COVERAGE: COVERED (0x{{[0-9a-fA-F]+}}) is present in sancov --print output

# COM: Drop the llvm-project clone and sancov build after coverage runs to save disk space.
# COM: Delete at the end so it is deleted only on success.
# RUN: rm -rf %t/llvm-project %t/build-sancov %t/build-sancov-helpers
4 changes: 4 additions & 0 deletions integration-tests/fixtures/reduction-batch/new_coverage.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
test_name,file,line,covered-points
test-a,/build/llvm/llvm/lib/Target/AMDGPU/FooA.cpp,100,0x1000
test-b,/build/llvm/llvm/lib/Target/AMDGPU/FooB.cpp,200,0x3000;0x2000
test-c,/build/llvm/llvm/lib/Target/AMDGPU/FooC.cpp,300,0x4000
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Build a minimal gap-fill output tree under DEST for reduction-batch lit tests.
set -euo pipefail

if [[ $# -ne 1 ]]; then
echo "usage: $(basename "$0") <dest-gap-fill-dir>" >&2
exit 2
fi

dest="$1"
fixture_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

mkdir -p "${dest}/incremental" "${dest}/candidate_tests" "${dest}/fake-llvm/bin"
cp "${fixture_dir}/new_coverage.csv" "${dest}/incremental/"

fake_llc="${dest}/fake-llvm/bin/llc"
printf '#!/bin/sh\n' >"${fake_llc}"
chmod +x "${fake_llc}"

for name in test-a test-b test-c; do
test_dir="${dest}/candidate_tests/${name}"
mkdir -p "${test_dir}"
cp "${fixture_dir}/sample.bc" "${test_dir}/sample.bc"
cat >"${test_dir}/test.sh" <<EOF
#!/bin/bash
${fake_llc} -O1 -mtriple=amdgcn-amd-amdhsa ${test_dir}/sample.bc
EOF
chmod +x "${test_dir}/test.sh"
done

@mgcarrasco mgcarrasco Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a new one or we could use an existing one under integration-tests?

Binary file not shown.
24 changes: 24 additions & 0 deletions integration-tests/reduction-batch.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# COM: Scaffold-only batch-from-coverage for n=1 and n=3 (no llvm-reduce/creduce).

# RUN: rm -rf %t && mkdir -p %t
# RUN: bash %S/fixtures/reduction-batch/prepare-gap-fill-fixture.sh %t/gap-fill

@mgcarrasco mgcarrasco Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to run it on a real but small case?

We could run a simple baseline with a one-test filter, run one candidate test, this will necessarily identify "gaps", and we reduce in regard to that.

In this way we are testing a real scenario without having to maintain prepare-gap-fill-fixture.sh. It is creating many stubs, that if any other component is updated, the stub generation would have to be updated as well.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a real case in the e2e pipeline - do you mean something else?

The prepare-gap-fill-fixture.sh is to test batch reduction, where we reduce many tests rather than just 1, so it is testing something a bit different.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be handy to make sure that tool is performing a true reduction (not using a fake llc) without having to analyze a PR. Also, while making it real it could also reduce the boilerplate around the test (i.e., prepare-gap-fill-fixture.sh)

The prepare-gap-fill-fixture.sh is to test batch reduction, where we reduce many tests rather than just 1, so it is testing something a bit different.

The integration test should be a real execution of the tool. This test is not only stubbing some initial files but almost the entire reduction. prepare-gap-fill-fixture.sh prepares a fake llc. The integration tests for finding and filling are executed in the LLVM build that they have available.

# RUN: %reduce batch-from-coverage --csv %t/gap-fill/incremental/new_coverage.csv --candidate-tests %t/gap-fill/candidate_tests --output %t/out-n1 --template-dir %repo-root/example/amd/new-test-1 --n 1
# RUN: test -d %t/out-n1/t-00001-test-a
# RUN: test -f %t/out-n1/t-00001-test-a/config.json
# RUN: test -f %t/out-n1/t-00001-test-a/interesting_ir.sh
# RUN: %FileCheck %s --input-file %t/out-n1/t-00001-test-a/config.json --check-prefix=CFG1
# RUN: %FileCheck %s --input-file %t/out-n1/t-00001-test-a/interesting_ir.sh --check-prefix=IR1

# RUN: %reduce batch-from-coverage --csv %t/gap-fill/incremental/new_coverage.csv --candidate-tests %t/gap-fill/candidate_tests --output %t/out-n3 --template-dir %repo-root/example/amd/new-test-1 --n 3

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference between this call and the one above? Could we have just one?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference is the number of tests - I am testing reducing 1 or reducing 3. I guess we could just have the reduce 3 if you want to simplify?

# RUN: test -d %t/out-n3/t-00001-test-a
# RUN: test -d %t/out-n3/t-00002-test-b
# RUN: test -d %t/out-n3/t-00003-test-c
# RUN: %FileCheck %s --input-file %t/out-n3/t-00002-test-b/config.json --check-prefix=CFG2
# RUN: %FileCheck %s --input-file %t/out-n3/t-00002-test-b/interesting_ir.sh --check-prefix=IR2

# CFG1: "file": "llvm/lib/Target/AMDGPU/FooA.cpp",
Comment thread
ambergorzynski marked this conversation as resolved.
# CFG1: "line": 100,
# CFG2: "file": "llvm/lib/Target/AMDGPU/FooB.cpp",
# CFG2: "line": 200,
# IR1: COVERED="0x1000"
# IR2: COVERED="0x2000"
Loading
Loading