ci: reuse fuzz corpora across integration runs (#9979)

ci: persist fuzz corpora between runs

Signed-off-by: dhruvxvaishnav <dhruvvaishnav687@gmail.com>
This commit is contained in:
Dhruv Vaishnav
2026-10-28 02:37:49 +00:00
committed by GitHub
parent 180e2716fc
commit 3c490bd2dc
5 changed files with 292 additions and 5 deletions
+40
View File
@@ -28,7 +28,38 @@ inputs:
runs:
using: composite
steps:
- name: Prepare Fuzz Corpus
id: corpus
shell: bash
env:
FUZZ_TARGETS: ${{ inputs.targets }}
run: bash .github/scripts/prepare-fuzz-corpus.sh
- name: Restore Fuzz Corpus
id: restore-corpus
uses: actions/cache/restore@v4
continue-on-error: true
with:
path: ${{ steps.corpus.outputs.paths }}
key: ${{ steps.corpus.outputs.key-prefix }}${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}-${{ strategy.job-index || 0 }}
restore-keys: ${{ steps.corpus.outputs.key-prefix }}
- name: Report Fuzz Corpus Cache
shell: bash
env:
CACHE_OUTCOME: ${{ steps.restore-corpus.outcome }}
CACHE_MATCHED_KEY: ${{ steps.restore-corpus.outputs.cache-matched-key }}
run: |
if [[ "${CACHE_OUTCOME}" == failure ]]; then
echo '::warning::Fuzz corpus restore failed; running with available inputs'
elif [[ -n "${CACHE_MATCHED_KEY}" ]]; then
printf 'Restored fuzz corpus: %s\n' "${CACHE_MATCHED_KEY}"
else
echo 'No cached fuzz corpus found; starting with an empty corpus'
fi
- name: Run Fuzz Tests
id: run-fuzz
shell: bash
env:
FUZZ_TARGETS: ${{ inputs.targets }}
@@ -39,4 +70,13 @@ runs:
FUZZ_COLLECT_CLUSTER_ARTIFACTS: ${{ inputs.collect-cluster-artifacts }}
FUZZ_ARTIFACT_ROOT: ${{ inputs.artifact-root }}
FUZZ_BIN_DIR: ${{ inputs.prebuilt-binaries-dir }}
FUZZ_CORPUS_ROOT: ${{ github.workspace }}/tests-fuzz/corpus
run: .github/scripts/run-fuzz-targets.sh
- name: Save Fuzz Corpus
if: ${{ !cancelled() && (steps.run-fuzz.outcome == 'success' || steps.run-fuzz.outcome == 'failure') }}
uses: actions/cache/save@v4
continue-on-error: true
with:
path: ${{ steps.corpus.outputs.paths }}
key: ${{ steps.corpus.outputs.key-prefix }}${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}-${{ strategy.job-index || 0 }}
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
: "${FUZZ_TARGETS:?FUZZ_TARGETS is required}"
: "${GITHUB_WORKSPACE:?GITHUB_WORKSPACE is required}"
: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}"
targets=()
while IFS= read -r target; do
target="${target%$'\r'}"
[[ -z "${target}" ]] && continue
if [[ ! "${target}" =~ ^[a-zA-Z0-9_]+$ ]]; then
printf 'Invalid fuzz target: %s\n' "${target}" >&2
exit 2
fi
targets+=("${target}")
done <<<"${FUZZ_TARGETS}"
if [[ "${#targets[@]}" -eq 0 ]]; then
printf 'FUZZ_TARGETS contains no targets\n' >&2
exit 2
fi
# Corpus identity depends on the targets, not the source revision or their order.
target_set="$(printf '%s\n' "${targets[@]}" | LC_ALL=C sort -u)"
target_hash="$(printf '%s\n' "${target_set}" | sha256sum | cut -d ' ' -f 1)"
corpus_root="${GITHUB_WORKSPACE}/tests-fuzz/corpus"
{
printf 'key-prefix=fuzz-corpus-v1-%s-\n' "${target_hash}"
printf 'paths<<FUZZ_CORPUS_PATHS\n'
while IFS= read -r target; do
mkdir -p "${corpus_root}/${target}"
printf '%s\n' "${corpus_root}/${target}"
done <<<"${target_set}"
printf 'FUZZ_CORPUS_PATHS\n'
} >>"${GITHUB_OUTPUT}"
+126 -2
View File
@@ -5,6 +5,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
RUNNER="${SCRIPT_DIR}/run-fuzz-targets.sh"
COLLECTOR="${SCRIPT_DIR}/collect-fuzz-target-artifacts.sh"
PREPARE_CORPUS="${SCRIPT_DIR}/prepare-fuzz-corpus.sh"
fail() {
printf 'FAIL: %s\n' "$*" >&2
@@ -32,6 +33,13 @@ new_fixture() {
set -euo pipefail
printf '%s\t%s\t%s\n' "${GT_FUZZ_DUMP_DIR}" "$*" "${MOCK_CARGO_MARKER:-}" >>"${MOCK_CARGO_LOG}"
target="$4"
corpus="$5"
[[ -d "${corpus}" ]] || exit 18
printf '%s\t%s\n' "${target}" "${corpus}" >>"${MOCK_CORPUS_LOG}"
if [[ -f "${corpus}/seed" ]]; then
cat "${corpus}/seed" >>"${MOCK_SEED_LOG}"
fi
printf '%s\n' "${target}" >"${corpus}/learned"
case " ${MOCK_FAIL_TARGETS:-} " in
*" ${target} "*) exit 17 ;;
esac
@@ -64,12 +72,15 @@ run_fixture() {
"MOCK_COLLECTOR_LOG=${fixture}/collector.log"
"MOCK_FAIL_TARGETS=${fail_targets}"
"MOCK_CARGO_MARKER=fixture"
"MOCK_CORPUS_LOG=${fixture}/corpus.log"
"MOCK_SEED_LOG=${fixture}/seeds.log"
"FUZZ_TARGETS=${targets}"
"FUZZ_GROUP=test-group"
"FUZZ_MAX_TOTAL_TIME=120"
"FUZZ_UNSTABLE=${unstable}"
"FUZZ_COLLECT_CLUSTER_ARTIFACTS=true"
"FUZZ_ARTIFACT_ROOT=${artifact_root}"
"FUZZ_CORPUS_ROOT=${fixture}/corpus with spaces"
"FUZZ_ARTIFACT_COLLECTOR=${fixture}/collector"
"GT_FUZZ_BINARY_PATH=${GT_FUZZ_BINARY_PATH:-}"
"GT_FUZZ_INSTANCE_ROOT_DIR=${GT_FUZZ_INSTANCE_ROOT_DIR:-}"
@@ -81,7 +92,7 @@ run_fixture() {
fi
set +e
env -u FUZZ_FAIL_FAST "${runner_env[@]}" \
env -u FUZZ_FAIL_FAST -u FUZZ_BIN_DIR "${runner_env[@]}" \
"${RUNNER}" >"${fixture}/stdout.log" 2>&1
fixture_status=$?
set -e
@@ -89,18 +100,30 @@ run_fixture() {
run_prebuilt_fixture() {
new_fixture
mkdir -p "${fixture}/prebuilt"
mkdir -p "${fixture}/prebuilt" "${fixture}/corpus with spaces/fuzz_create_table"
printf 'restored prebuilt seed\n' >"${fixture}/corpus with spaces/fuzz_create_table/seed"
cat >"${fixture}/prebuilt/fuzz_create_table" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\t%s\t%s\n' "${GT_FUZZ_DUMP_DIR}" "$*" "${MOCK_BINARY_MARKER:-}" >>"${MOCK_BINARY_LOG}"
[[ -d "$1" ]] || exit 18
printf '%s\n' "$1" >>"${MOCK_CORPUS_LOG}"
cat "$1/seed" >>"${MOCK_SEED_LOG}"
printf 'new prebuilt input\n' >"$1/learned"
exit "${MOCK_BINARY_EXIT_CODE}"
EOF
chmod +x "${fixture}/prebuilt/fuzz_create_table"
set +e
env \
"PATH=${fixture}/bin:${PATH}" \
"MOCK_CARGO_LOG=${fixture}/cargo.log" \
"MOCK_BINARY_LOG=${fixture}/binary.log" \
"MOCK_BINARY_MARKER=fixture" \
"MOCK_BINARY_EXIT_CODE=${1:-0}" \
"MOCK_CORPUS_LOG=${fixture}/corpus.log" \
"MOCK_SEED_LOG=${fixture}/seeds.log" \
"MOCK_COLLECTOR_LOG=${fixture}/collector.log" \
"FUZZ_TARGETS=fuzz_create_table" \
"FUZZ_GROUP=test-group" \
"FUZZ_MAX_TOTAL_TIME=120" \
@@ -108,6 +131,7 @@ EOF
"FUZZ_FAIL_FAST=true" \
"FUZZ_COLLECT_CLUSTER_ARTIFACTS=false" \
"FUZZ_ARTIFACT_ROOT=${fixture}/artifacts" \
"FUZZ_CORPUS_ROOT=${fixture}/corpus with spaces" \
"FUZZ_ARTIFACT_COLLECTOR=${fixture}/collector" \
"FUZZ_BIN_DIR=${fixture}/prebuilt" \
"GITHUB_SHA=deadbeef" \
@@ -143,6 +167,7 @@ test_successful_targets_run_in_order() {
"target order"
grep -q -- '--features=unstable' "${fixture}/cargo.log" || fail "unstable feature missing"
grep -q -- '-max_total_time=120' "${fixture}/cargo.log" || fail "fuzz time missing"
grep -q -- '-print_final_stats=1' "${fixture}/cargo.log" || fail "fuzz statistics missing"
grep -q -- '-artifact_prefix=.*/targets/fuzz_create_table/libfuzzer/' "${fixture}/cargo.log" || \
fail "target-scoped libFuzzer prefix missing"
assert_eq success "$(jq -r '.[0].status' "${fixture}/artifacts/manifest.json")" "first status"
@@ -167,8 +192,49 @@ test_prebuilt_binary_runs_without_cargo() {
[[ ! -e "${fixture}/cargo.log" ]] || fail "cargo ran for prebuilt binary"
assert_eq 1 "$(wc -l <"${fixture}/binary.log" | tr -d ' ')" "prebuilt invocation count"
grep -q -- '-max_total_time=120' "${fixture}/binary.log" || fail "prebuilt fuzz time missing"
grep -q -- '-print_final_stats=1' "${fixture}/binary.log" || fail "prebuilt fuzz statistics missing"
grep -q -- '-artifact_prefix=.*/targets/fuzz_create_table/libfuzzer/' "${fixture}/binary.log" || \
fail "prebuilt target-scoped libFuzzer prefix missing"
assert_eq "${fixture}/corpus with spaces/fuzz_create_table" "$(cat "${fixture}/corpus.log")" \
"prebuilt corpus argument"
assert_eq 'restored prebuilt seed' "$(cat "${fixture}/seeds.log")" "prebuilt restored input"
assert_file "${fixture}/corpus with spaces/fuzz_create_table/learned"
cleanup_fixture
}
test_cargo_reuses_separate_target_corpora() {
new_fixture
mkdir -p "${fixture}/corpus with spaces/fuzz_create_table" "${fixture}/corpus with spaces/fuzz_insert"
printf 'create seed\n' >"${fixture}/corpus with spaces/fuzz_create_table/seed"
printf 'insert seed\n' >"${fixture}/corpus with spaces/fuzz_insert/seed"
run_fixture $'fuzz_create_table\nfuzz_insert' false false ""
assert_eq 0 "${fixture_status}" "restored cargo run status"
assert_eq $'create seed\ninsert seed' "$(cat "${fixture}/seeds.log")" "target-scoped restored inputs"
assert_eq \
"fuzz_create_table"$'\t'"${fixture}/corpus with spaces/fuzz_create_table"$'\n'"fuzz_insert"$'\t'"${fixture}/corpus with spaces/fuzz_insert" \
"$(cat "${fixture}/corpus.log")" "cargo corpus arguments"
assert_eq fuzz_create_table "$(cat "${fixture}/corpus with spaces/fuzz_create_table/learned")" \
"first target learned input"
assert_eq fuzz_insert "$(cat "${fixture}/corpus with spaces/fuzz_insert/learned")" \
"second target learned input"
run_fixture fuzz_create_table true false ""
assert_eq 0 "${fixture_status}" "repeated cargo run status"
assert_file "${fixture}/corpus with spaces/fuzz_create_table/seed"
assert_file "${fixture}/corpus with spaces/fuzz_insert/learned"
cleanup_fixture
}
test_prebuilt_failure_preserves_corpus_and_artifacts() {
run_prebuilt_fixture 17
assert_eq 1 "${fixture_status}" "prebuilt failure status"
assert_eq 17 "$(jq -r '.[0].exit_code' "${fixture}/artifacts/manifest.json")" \
"prebuilt failure exit code"
assert_file "${fixture}/corpus with spaces/fuzz_create_table/seed"
assert_file "${fixture}/corpus with spaces/fuzz_create_table/learned"
assert_file "${fixture}/artifacts/targets/fuzz_create_table/mock-artifacts/state.txt"
cleanup_fixture
}
@@ -183,6 +249,8 @@ test_fail_fast_stops_after_first_failure() {
assert_eq skipped_after_failure "$(jq -r '.[2].status' "${fixture}/artifacts/manifest.json")" "skipped status"
assert_eq true "$(jq -r '.[2].after_prior_failure' "${fixture}/artifacts/manifest.json")" "skipped provenance"
assert_file "${fixture}/artifacts/targets/fuzz_insert/mock-artifacts/state.txt"
assert_file "${fixture}/corpus with spaces/fuzz_insert/learned"
[[ ! -e "${fixture}/corpus with spaces/fuzz_alter_table" ]] || fail "skipped target corpus created"
grep -q 'title=Fuzz target failed.*target=fuzz_insert' "${fixture}/stdout.log" || \
fail "target failure annotation missing"
grep -q 'title=Fuzz target skipped.*target=fuzz_alter_table' "${fixture}/stdout.log" || \
@@ -191,6 +259,8 @@ test_fail_fast_stops_after_first_failure() {
fail "reproduction section missing"
grep -q 'cargo +nightly fuzz run fuzz_insert' "${fixture}/artifacts/summary.md" || \
fail "reproduction command missing"
grep -q 'fuzz_insert --fuzz-dir tests-fuzz' "${fixture}/artifacts/summary.md" || \
fail "reproduction command depends on the CI corpus path"
cleanup_fixture
}
@@ -241,6 +311,56 @@ test_invalid_configuration_fails_before_cargo() {
cleanup_fixture
}
prepare_corpus_fixture() {
local targets="$1"
local output="$2"
set +e
FUZZ_TARGETS="${targets}" \
GITHUB_WORKSPACE="${fixture}/workspace with spaces" \
GITHUB_OUTPUT="${output}" \
bash "${PREPARE_CORPUS}" >"${fixture}/prepare.log" 2>&1
prepare_status=$?
set -e
}
test_corpus_cache_identity_and_paths() {
new_fixture
prepare_corpus_fixture $'fuzz_insert\r\n\nfuzz_create_table\r\nfuzz_insert\r' "${fixture}/first-output"
assert_eq 0 "${prepare_status}" "corpus preparation status"
prepare_corpus_fixture $'fuzz_create_table\nfuzz_insert' "${fixture}/second-output"
assert_eq 0 "${prepare_status}" "reordered corpus preparation status"
cmp -s "${fixture}/first-output" "${fixture}/second-output" || \
fail "cache identity depends on target order, duplicates, or line endings"
assert_eq \
"${fixture}/workspace with spaces/tests-fuzz/corpus/fuzz_create_table"$'\n'"${fixture}/workspace with spaces/tests-fuzz/corpus/fuzz_insert" \
"$(sed '1,2d; $d' "${fixture}/first-output")" "selected corpus cache paths"
[[ -d "${fixture}/workspace with spaces/tests-fuzz/corpus/fuzz_insert" ]] || \
fail "corpus directory not created"
[[ ! -e "${fixture}/workspace with spaces/tests-fuzz/corpus/fuzz_alter_table" ]] || \
fail "unselected corpus directory created"
GITHUB_SHA=another-revision prepare_corpus_fixture $'fuzz_create_table\nfuzz_insert' "${fixture}/new-revision-output"
cmp -s "${fixture}/first-output" "${fixture}/new-revision-output" || \
fail "source revision changed cache identity"
prepare_corpus_fixture fuzz_insert "${fixture}/subset-output"
assert_eq 0 "${prepare_status}" "subset corpus preparation status"
[[ "$(head -n 1 "${fixture}/first-output")" != "$(head -n 1 "${fixture}/subset-output")" ]] || \
fail "different target sets share cache identity"
cleanup_fixture
}
test_corpus_preparation_rejects_invalid_targets_before_writing() {
local targets
for targets in $'fuzz_insert\n../escape' 'fuzz target' $'\n\r\n'; do
new_fixture
prepare_corpus_fixture "${targets}" "${fixture}/output"
assert_eq 2 "${prepare_status}" "invalid corpus target status"
[[ ! -e "${fixture}/output" ]] || fail "invalid targets wrote cache outputs"
[[ ! -e "${fixture}/workspace with spaces" ]] || fail "invalid targets created directories"
cleanup_fixture
done
}
test_collector_keeps_target_scopes_separate() {
new_fixture
service_log="${fixture}/greptime.log"
@@ -373,10 +493,14 @@ EOF
test_successful_targets_run_in_order
test_prebuilt_binary_runs_without_cargo
test_cargo_reuses_separate_target_corpora
test_prebuilt_failure_preserves_corpus_and_artifacts
test_fail_fast_stops_after_first_failure
test_fail_fast_defaults_to_true
test_non_fail_fast_marks_later_targets
test_invalid_configuration_fails_before_cargo
test_corpus_cache_identity_and_paths
test_corpus_preparation_rejects_invalid_targets_before_writing
test_collector_keeps_target_scopes_separate
test_cluster_collector_honors_target_scope_and_namespace
test_setup_failure_writes_artifact_contract
+7 -3
View File
@@ -5,6 +5,7 @@ set -euo pipefail
FUZZ_ARTIFACT_ROOT="${FUZZ_ARTIFACT_ROOT:-/tmp/greptime-fuzz-artifacts}"
FUZZ_ARTIFACT_COLLECTOR="${FUZZ_ARTIFACT_COLLECTOR:-.github/scripts/collect-fuzz-target-artifacts.sh}"
FUZZ_ARTIFACT_COLLECTION_TIMEOUT_SECS="${FUZZ_ARTIFACT_COLLECTION_TIMEOUT_SECS:-180}"
FUZZ_CORPUS_ROOT="${FUZZ_CORPUS_ROOT:-tests-fuzz/corpus}"
log() {
printf '[run-fuzz-targets] %s\n' "$*"
@@ -187,19 +188,22 @@ for ((index = 0; index < ${#targets[@]}; index++)); do
started_epoch="$(date +%s)"
echo "::group::Fuzz target: ${target}"
log "run target ${target}"
mkdir -p "${FUZZ_CORPUS_ROOT}/${target}"
corpus_dir="$(cd -- "${FUZZ_CORPUS_ROOT}/${target}" && pwd)"
log "corpus directory: ${corpus_dir}"
if [[ -n "${FUZZ_BIN_DIR:-}" ]]; then
fuzz_binary="${FUZZ_BIN_DIR}/${target}"
if [[ ! -x "${fuzz_binary}" ]]; then
log "prebuilt fuzz binary does not exist or is not executable: ${fuzz_binary}"
exit 2
fi
run_args=("${fuzz_binary}" "-max_total_time=${FUZZ_MAX_TOTAL_TIME}" "-artifact_prefix=${target_dir}/libfuzzer/")
run_args=("${fuzz_binary}" "${corpus_dir}" "-max_total_time=${FUZZ_MAX_TOTAL_TIME}" "-print_final_stats=1" "-artifact_prefix=${target_dir}/libfuzzer/")
else
run_args=(cargo +"${RUSTUP_TOOLCHAIN:-nightly}" fuzz run "${target}" --fuzz-dir tests-fuzz -D -s none)
run_args=(cargo +"${RUSTUP_TOOLCHAIN:-nightly}" fuzz run "${target}" "${corpus_dir}" --fuzz-dir tests-fuzz -D -s none)
if [[ "${fuzz_unstable}" == true ]]; then
run_args+=(--features=unstable)
fi
run_args+=(-- "-max_total_time=${FUZZ_MAX_TOTAL_TIME}" "-artifact_prefix=${target_dir}/libfuzzer/")
run_args+=(-- "-max_total_time=${FUZZ_MAX_TOTAL_TIME}" "-print_final_stats=1" "-artifact_prefix=${target_dir}/libfuzzer/")
fi
if GT_FUZZ_DUMP_DIR="${target_dir}/csv" \
"${run_args[@]}" 2>&1 | tee "${target_dir}/fuzz.log"; then
+81
View File
@@ -42,6 +42,87 @@ cargo fuzz list --fuzz-dir tests-fuzz
cargo fuzz run fuzz_create_table --fuzz-dir tests-fuzz -D -s none
```
## CI Corpus Persistence
The shared `.github/actions/fuzz-test` action restores inputs before running
the selected targets and saves the updated corpus afterward, including when a
target fails. Cache failures are best-effort and do not change the fuzz result.
Crash files, diagnostic artifacts, and fail-fast behavior are unchanged.
Each target reads and writes its own `tests-fuzz/corpus/<target>` directory.
Both prebuilt executables and the `cargo fuzz` fallback receive that directory
explicitly. For local runs of `.github/scripts/run-fuzz-targets.sh`, set
`FUZZ_CORPUS_ROOT` to use a different corpus root.
The selected target directories are cached together to preserve the existing
job grouping. The stable key prefix identifies the sorted, deduplicated target
set, not a source hash, so code changes do not invalidate existing inputs.
Each job saves a new immutable entry, distinguished by run ID, attempt, job,
and matrix index. Restore uses the latest accessible entry matching the prefix.
Changing the target set starts a different cache; corpora are not shared between
different target sets. Parallel jobs may start from the same entry, but their
new inputs are not merged automatically.
The restore step logs the matched key (including prefix matches) or a cache
miss. GitHub's [cache access restrictions](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#restrictions-for-accessing-a-cache)
still apply: PR caches are scoped to that PR, while a default-branch cache can
seed other PRs. The existing scheduled workflow does not run fuzz jobs; a
maintainer can populate the default-branch corpus through the existing manual
fuzz workflow. This change does not add scheduling, target selection, or corpus
pruning. Rolling entries remain subject to GitHub's cache limits and eviction.
### Validation
The orchestration tests use mock fuzz executables and do not build Rust or
start GreptimeDB. They cover both runner paths, restored inputs, separate
target directories, retained inputs after failure, cache identity, and invalid
target names. Run them on Linux with Bash, jq, and GNU coreutils:
```bash
bash .github/scripts/run-fuzz-targets-test.sh
```
These tests do not exercise the hosted GitHub cache service. To verify that
service, inspect two fuzz workflow runs on the same PR: the first should save
an entry, and the second should report its restored key and load a nonempty
corpus. Also verify that cache save still runs after a target failure, without
turning that failure into a successful fuzz result.
To compare useful coverage, use the same fuzz executable, GreptimeDB revision,
service configuration, and time budget for empty and restored corpora. Both
runner paths enable `-print_final_stats=1`; the target's `fuzz.log` retains the
libFuzzer coverage and final statistics. Seed the restored corpus with a
separate warm-up run (or a downloaded corpus), then compare it with a fresh,
empty directory. Keep warm-up time separate from the comparison and repeat
trials to account for randomness. Report initial corpus size, final covered
edges (`cov:`), executions, and elapsed time, rather than inferring a coverage
improvement merely from a cache hit.
For example, with a prebuilt `fuzz_create_table` binary and a running standalone
service, the following creates separate warm-up, empty, and seeded corpora. Set
`FUZZ_BIN_DIR` to the absolute binary directory and `GT_MYSQL_ADDR` to the test
service address first. All runs use the same executable and 120-second budget;
the warm-up is not part of the measured comparison. This creates temporary
inputs, logs, and crash files, and executes DDL against the test service.
```bash
set -euo pipefail
: "${FUZZ_BIN_DIR:?Set the prebuilt fuzz binary directory}"
: "${GT_MYSQL_ADDR:?Set the test service address}"
comparison_dir="$(mktemp -d -t fuzz-corpus-comparison.XXXXXX)"
for trial in seed cold warm; do
mkdir -p "${comparison_dir}/${trial}" "${comparison_dir}/artifacts/${trial}"
if [[ "${trial}" == warm ]]; then
cp -a "${comparison_dir}/seed/." "${comparison_dir}/warm/"
fi
"${FUZZ_BIN_DIR}/fuzz_create_table" "${comparison_dir}/${trial}" \
-max_total_time=120 -print_final_stats=1 \
"-artifact_prefix=${comparison_dir}/artifacts/${trial}/" \
2>&1 | tee "${comparison_dir}/${trial}.log"
done
printf 'Comparison logs and corpora: %s\n' "${comparison_dir}"
```
## Crash Reproduction
If you want to reproduce a crash, you first need to obtain the Base64 encoded code, which usually appears at the end of a crash report, and store it in a file.