Files
dingtalk-workspace-cli/scripts/ci/run-app-race-tests.sh

237 lines
7.3 KiB
Bash
Executable File

#!/bin/sh
set -eu
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
usage() {
printf '%s\n' \
"usage: $0 verify <app-package>" \
" $0 run <app-package> [partition]" \
" $0 list-partitions" >&2
exit 2
}
# Single source of truth for the partition set. CI runs one job per partition and
# pins its shard names to this list, so a name that appears here without a
# dispatch entry below fails closed rather than silently skipping tests.
APP_PARTITIONS='schema a-b c-a-l c-m-o c-p-r c-s-z c-other d-r s-z-example-fuzz'
mode="${1:-}"
partition=""
case "$mode" in
list-partitions)
[ "$#" -eq 1 ] || usage
for name in $APP_PARTITIONS; do
printf '%s\n' "$name"
done
exit 0
;;
verify)
[ "$#" -eq 2 ] || usage
app_package="$2"
;;
run)
[ "$#" -eq 2 ] || [ "$#" -eq 3 ] || usage
app_package="$2"
partition="${3:-}"
;;
*) usage ;;
esac
workdir="$(mktemp -d "${TMPDIR:-/tmp}/dws-app-race-tests.XXXXXX")"
trap 'rm -rf "$workdir"' EXIT HUP INT TERM
tests="$workdir/tests"
duplicates="$workdir/duplicates"
list_output="$workdir/list-output"
cd "$ROOT"
if ! go test "$app_package" -list '^(Test|Example|Fuzz)' > "$list_output"; then
printf 'app race partition discovery failed for %s\n' "$app_package" >&2
exit 1
fi
awk '/^(Test|Example|Fuzz)/ { print $1 }' "$list_output" > "$tests"
if [ ! -s "$tests" ]; then
printf 'app race partition discovery found no tests in %s\n' "$app_package" >&2
exit 1
fi
LC_ALL=C sort "$tests" | uniq -d > "$duplicates"
if [ -s "$duplicates" ]; then
printf '%s\n' 'app race partition discovery found duplicate top-level tests:' >&2
sed 's/^/ /' "$duplicates" >&2
exit 1
fi
schema_count=0
ab_count=0
cal_count=0
cmo_count=0
cpr_count=0
csz_count=0
cother_count=0
dr_count=0
sz_count=0
unmatched_count=0
while IFS= read -r test_name; do
case "$test_name" in
Test*Schema*) schema_count=$((schema_count + 1)) ;;
Test[A-B]*) ab_count=$((ab_count + 1)) ;;
TestCrossPlatformCoverage[A-L]*) cal_count=$((cal_count + 1)) ;;
TestCrossPlatformCoverage[M-O]*) cmo_count=$((cmo_count + 1)) ;;
TestCrossPlatformCoverage[P-R]*) cpr_count=$((cpr_count + 1)) ;;
TestCrossPlatformCoverage[S-Z]*) csz_count=$((csz_count + 1)) ;;
TestC*) cother_count=$((cother_count + 1)) ;;
Test[D-R]*) dr_count=$((dr_count + 1)) ;;
Test[S-Z]*|Example*|Fuzz*) sz_count=$((sz_count + 1)) ;;
*)
printf 'unmatched app race test: %s\n' "$test_name" >&2
unmatched_count=$((unmatched_count + 1))
;;
esac
done < "$tests"
if [ "$unmatched_count" -ne 0 ]; then
exit 1
fi
# The loop variable is deliberately not named "partition": that name holds the
# partition requested on the command line, and run mode still executes this
# discovery pass before dispatching.
classified=''
for spec in \
"schema:$schema_count" \
"a-b:$ab_count" \
"c-a-l:$cal_count" \
"c-m-o:$cmo_count" \
"c-p-r:$cpr_count" \
"c-s-z:$csz_count" \
"c-other:$cother_count" \
"d-r:$dr_count" \
"s-z-example-fuzz:$sz_count"
do
name="${spec%%:*}"
count="${spec#*:}"
classified="$classified $name"
if [ "$count" -eq 0 ]; then
printf 'app race partition %s is empty\n' "$name" >&2
exit 1
fi
done
# The counters above and APP_PARTITIONS must describe the same set in both
# directions. A counted partition missing from APP_PARTITIONS would never be
# dispatched by any CI job, and a dispatchable partition with no counter would
# escape the exact-coverage check above.
for name in $APP_PARTITIONS; do
case " $classified " in
*" $name "*) ;;
*)
printf 'app partition %s has no coverage counter\n' "$name" >&2
exit 1
;;
esac
done
for name in $classified; do
case " $APP_PARTITIONS " in
*" $name "*) ;;
*)
printf 'counted partition %s is not a dispatchable app partition\n' "$name" >&2
exit 1
;;
esac
done
total_count="$(wc -l < "$tests" | tr -d ' ')"
assigned_count=$((schema_count + ab_count + cal_count + cmo_count + cpr_count + csz_count + cother_count + dr_count + sz_count))
if [ "$assigned_count" -ne "$total_count" ]; then
printf 'app race partitions assigned %s tests, want %s\n' "$assigned_count" "$total_count" >&2
exit 1
fi
printf 'app race partitions cover %s top-level tests exactly once: schema=%s a-b=%s c-a-l=%s c-m-o=%s c-p-r=%s c-s-z=%s c-other=%s d-r=%s s-z-example-fuzz=%s\n' \
"$total_count" "$schema_count" "$ab_count" "$cal_count" "$cmo_count" "$cpr_count" "$csz_count" "$cother_count" "$dr_count" "$sz_count"
if [ "$mode" = "verify" ]; then
exit 0
fi
run_partition() {
name="$1"
instrumentation="$2"
run_pattern="$3"
skip_pattern="${4:-}"
# Fail closed on an unrecognized mode: a typo must not silently drop race
# instrumentation from a partition that is supposed to carry it.
case "$instrumentation" in
race|no-race) ;;
*)
printf 'unknown instrumentation %s for app partition %s\n' \
"$instrumentation" "$name" >&2
exit 1
;;
esac
printf 'running internal/app %s partition %s\n' "$instrumentation" "$name"
set -- -v -count=1 -timeout=15m -run "$run_pattern"
if [ -n "$skip_pattern" ]; then
set -- "$@" -skip "$skip_pattern"
fi
if [ "$instrumentation" = race ]; then
set -- -race "$@"
fi
go test "$@" "$app_package"
}
# Schema assembly has the largest transient memory footprint. Run those tests
# in a fresh process, then keep each remaining name range in its own process so
# command trees retained by process-global registries are released between
# partitions. The complementary run/skip patterns preserve the full test set.
#
# The schema partition runs uninstrumented. Its tests assert structural
# Schema-to-Cobra contracts over a single goroutine: none of them call
# t.Parallel or start a goroutine, so the race detector has no concurrent access
# to observe here. The process-global lazy metadata that does need race coverage
# (schema_source_root's atomic.Value, the parameter-binding lazy loaders) is
# exercised by internal/cli's concurrent tests, which stay instrumented. The
# instrumentation is not free on this partition: its shared sync.Once Catalog
# build is allocation-heavy, and -race made the partition roughly 11x slower
# (26s -> 291s locally, 357s in CI) without being able to report anything.
schema_pattern='^Test.*Schema'
# Dispatch table for the partition set declared in APP_PARTITIONS. CI passes one
# partition per job so they run concurrently; running without a partition keeps
# the original end-to-end behaviour for local use and for any caller that wants
# the whole package in one invocation.
run_named_partition() {
case "$1" in
schema) run_partition schema no-race "$schema_pattern" ;;
a-b) run_partition a-b race '^Test[A-B]' "$schema_pattern" ;;
c-a-l) run_partition c-a-l race '^TestCrossPlatformCoverage[A-L]' "$schema_pattern" ;;
c-m-o) run_partition c-m-o race '^TestCrossPlatformCoverage[M-O]' "$schema_pattern" ;;
c-p-r) run_partition c-p-r race '^TestCrossPlatformCoverage[P-R]' "$schema_pattern" ;;
c-s-z) run_partition c-s-z race '^TestCrossPlatformCoverage[S-Z]' "$schema_pattern" ;;
c-other) run_partition c-other race '^TestC' '^Test.*Schema|^TestCrossPlatformCoverage' ;;
d-r) run_partition d-r race '^Test[D-R]' "$schema_pattern" ;;
s-z-example-fuzz)
run_partition s-z-example-fuzz race '^(Test[S-Z]|Example|Fuzz)' "$schema_pattern"
;;
*)
printf 'unknown app partition: %s\n' "$1" >&2
exit 1
;;
esac
}
if [ -n "$partition" ]; then
run_named_partition "$partition"
exit 0
fi
for name in $APP_PARTITIONS; do
run_named_partition "$name"
done