Files
openhuman/scripts/ci/rust-coverage-changed.sh
T

164 lines
5.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# PR CI Rust core coverage lane — changed-files-only cargo-llvm-cov.
#
# Fast-lane policy (PRs targeting main): instead of the full ~13k-test
# instrumented suite, run only the unit tests for the modules the PR touched:
# - src/<a>/<b>/... .rs → libtest filter "<a>::<b>" (domain-level scope, so
# sibling-module tests like store_tests.rs / ops.rs still run)
# - tests/<name>.rs → that integration-test target only (--test <name>)
# Coverage from all scoped runs is merged (--no-report + report) into a single
# lcov file; the PR CI Gate's diff-cover step enforces >= 80% on changed lines.
#
# NOTE: this means changed lines must be covered by tests in their own domain
# (or a changed integration test) — coverage contributed by unrelated suites
# no longer counts on the fast lane. The full suite still runs on main→release
# PRs (Release CI).
#
# Inputs (env):
# FULL "true" → run the full suite (build-config / lib.rs / script
# changes, detected by paths-filter)
# CHANGED_FILES shell-quoted, space-separated repo-relative paths from
# dorny/paths-filter (list-files: shell)
# OUT lcov output path (default lcov-core.info)
#
# Falls back to the FULL suite whenever scoping is not clearly safe.
set -euo pipefail
FULL="${FULL:-false}"
CHANGED_FILES="${CHANGED_FILES:-}"
OUT="${OUT:-lcov-core.info}"
MAX_CHANGED_FILES="${MAX_CHANGED_FILES:-200}"
log() { echo "[ci][rust-cov-changed] $*"; }
llvm_cov() {
bash scripts/ci-cancel-aware.sh cargo llvm-cov "$@"
}
run_full() {
log "running FULL instrumented suite (reason: $1)"
llvm_cov --no-fail-fast -p openhuman --lcov --output-path "${OUT}"
exit 0
}
if [ "${FULL}" = "true" ]; then
run_full "build-config/workflow-level change detected by paths-filter"
fi
# Portable across bash 3.2 (macOS) and 5.x (CI containers): no declare -A,
# no mapfile, and no empty-array "${arr[@]}" expansion under set -u.
#
# CHANGED_FILES is the shell-quoted list from dorny/paths-filter
# (list-files: shell). Filenames are PR-controlled, so never eval it —
# xargs unquotes tokens as data without ever invoking a shell. If xargs
# can't parse it (e.g. hostile quoting), we get an empty list and fall
# back to the full suite.
declare -a files=()
while IFS= read -r f; do
[ -n "${f}" ] && files+=("${f}")
done < <(printf '%s\n' "${CHANGED_FILES}" | xargs -n1 printf '%s\n' 2>/dev/null || true)
log "received ${#files[@]} changed rust file(s)"
if [ "${#files[@]}" -eq 0 ]; then
run_full "empty changed-file list — scoping unsafe"
fi
if [ "${#files[@]}" -gt "${MAX_CHANGED_FILES}" ]; then
run_full "${#files[@]} changed files exceed MAX_CHANGED_FILES=${MAX_CHANGED_FILES}"
fi
lib_filters_raw=""
test_targets_raw=""
for f in "${files[@]}"; do
case "${f}" in
src/lib.rs | src/main.rs)
run_full "root module ${f} changed — whole-crate scope"
;;
src/bin/*)
# Standalone backfill binaries (slack-backfill, gmail-backfill-3d) have
# no unit tests; nothing to scope to.
log "ignoring standalone-binary file: ${f}"
;;
src/*.rs)
p="${f#src/}"
p="${p%.rs}"
IFS='/' read -r -a segs <<<"${p}"
n="${#segs[@]}"
if [ "${segs[n - 1]}" = "mod" ]; then
segs=("${segs[@]:0:n-1}")
n="${#segs[@]}"
fi
if [ "${n}" -ge 2 ]; then
key="${segs[0]}::${segs[1]}"
else
key="${segs[0]}"
fi
lib_filters_raw="${lib_filters_raw}${key}
"
log "${f} → libtest filter '${key}'"
;;
src/*/*)
# Non-.rs asset embedded in a domain (e.g. agent prompt markdown under
# src/openhuman/agent/prompts/) — scope to that domain's tests.
p="${f#src/}"
IFS='/' read -r -a segs <<<"${p}"
n="${#segs[@]}"
if [ "${n}" -ge 3 ]; then
key="${segs[0]}::${segs[1]}"
else
key="${segs[0]}"
fi
lib_filters_raw="${lib_filters_raw}${key}
"
log "${f} → libtest filter '${key}' (embedded asset)"
;;
tests/*.rs)
name="${f#tests/}"
name="${name%.rs}"
if [[ "${name}" == */* ]]; then
# Nested support module — can affect any integration target.
run_full "shared integration-test support file ${f} changed"
fi
test_targets_raw="${test_targets_raw}${name}
"
log "${f} → integration target '--test ${name}'"
;;
*)
run_full "unclassified rust-relevant file ${f} changed"
;;
esac
done
declare -a lib_filters=()
while IFS= read -r k; do
[ -n "${k}" ] && lib_filters+=("${k}")
done < <(printf '%s' "${lib_filters_raw}" | sort -u)
declare -a test_targets=()
while IFS= read -r k; do
[ -n "${k}" ] && test_targets+=("${k}")
done < <(printf '%s' "${test_targets_raw}" | sort -u)
if [ "${#lib_filters[@]}" -eq 0 ] && [ "${#test_targets[@]}" -eq 0 ]; then
run_full "no scoped test targets derivable from the change set"
fi
# Drop artifacts from previous coverage runs so merged profdata only reflects
# this run (build cache for dependencies is unaffected).
llvm_cov clean --workspace
if [ "${#lib_filters[@]}" -gt 0 ]; then
log "running scoped lib unit tests with filters: ${lib_filters[*]}"
# libtest ORs multiple positional filters — one run covers all domains.
llvm_cov --no-report --no-fail-fast -p openhuman --lib -- "${lib_filters[@]}"
fi
if [ "${#test_targets[@]}" -gt 0 ]; then
for t in "${test_targets[@]}"; do
log "running changed integration-test target: ${t}"
llvm_cov --no-report --no-fail-fast -p openhuman --test "${t}"
done
fi
log "merging coverage into ${OUT}"
llvm_cov report --lcov --output-path "${OUT}"