1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
|
#!/usr/bin/env bash
#
# Run a Gradle test task and report counts parsed from the JUnit XML it wrote,
# not from console text.
#
# Catches three failure modes that all look like success from the console:
#
# 1. Zero tests ran. A unit-test source set that fails to compile, or a filter
# that matches nothing, can leave the build green with nothing executed.
# "BUILD SUCCESSFUL" and "0 tests" are the same line of output.
# 2. Stale results. If Gradle decides the test task is UP-TO-DATE it does not
# re-run anything, and the XML on disk describes an older version of the
# code. Reading those numbers as current is how a passing suite gets
# reported for code that was never tested.
# 3. Results from the wrong project, via cwd drift — handled by gradle-verify.
#
# Usage:
# gradle-test <project-dir> [test-task] [--allow-stale] [-- <gradle args>]
#
# Default task: testDebugUnitTest for Android projects, test otherwise.
set -euo pipefail
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
die() { printf '%s: %s\n' "$(basename "$0")" "$*" >&2; exit 2; }
[ $# -ge 1 ] || die "usage: $(basename "$0") <project-dir> [test-task] [--allow-stale] [-- <gradle args>]"
raw_dir=$1; shift
project_dir=$(cd "$raw_dir" 2>/dev/null && pwd) || die "no such project dir: $raw_dir"
task=""
allow_stale=0
extra=()
seen_sep=0
for arg in "$@"; do
if [ "$arg" = "--" ]; then seen_sep=1; continue; fi
if [ $seen_sep -eq 1 ]; then extra+=("$arg"); continue; fi
case "$arg" in
--allow-stale) allow_stale=1 ;;
-*) extra+=("$arg") ;;
*) task=$arg ;;
esac
done
if [ -z "$task" ]; then
if grep -rqs "com.android.application\|com.android.library" \
"$project_dir"/build.gradle "$project_dir"/build.gradle.kts \
"$project_dir"/*/build.gradle "$project_dir"/*/build.gradle.kts 2>/dev/null; then
task=testDebugUnitTest
else
task=test
fi
fi
# Marker file: anything written at or after this is from the run we just did.
# Using a file rather than a clock reading avoids any filesystem/shell timestamp
# granularity mismatch.
marker=$(mktemp)
trap 'rm -f "$marker"' EXIT
sleep 1 # ensure a strictly-earlier mtime than anything the build writes
set +e
"$here/gradle-verify" "$project_dir" "$task" ${extra[@]+-- "${extra[@]}"}
gradle_status=$?
set -e
printf '\n=== gradle-test results ===\n'
mapfile -t all_xml < <(find "$project_dir" -path '*/test-results/*' -name 'TEST-*.xml' 2>/dev/null | sort)
if [ ${#all_xml[@]} -eq 0 ]; then
printf 'result : FAIL — no JUnit XML found anywhere under %s\n' "$project_dir"
printf 'hint : the test task may not have run, or writes results elsewhere\n'
exit 1
fi
fresh=(); stale=()
for f in "${all_xml[@]}"; do
if [ "$f" -nt "$marker" ]; then fresh+=("$f"); else stale+=("$f"); fi
done
printf 'task : %s\n' "$task"
printf 'xml : %d file(s) total, %d fresh, %d stale\n' \
"${#all_xml[@]}" "${#fresh[@]}" "${#stale[@]}"
if [ ${#fresh[@]} -eq 0 ]; then
if [ $allow_stale -eq 1 ]; then
printf 'note : no fresh results; counting stale ones because --allow-stale\n'
fresh=("${all_xml[@]}")
else
printf 'result : FAIL — every result file predates this run\n'
printf 'hint : Gradle treated %s as UP-TO-DATE, so these numbers describe\n' "$task"
printf ' older code. Re-run with: -- --rerun-tasks\n'
exit 1
fi
fi
counts=$(python3 - "${fresh[@]}" <<'PY'
import sys, xml.etree.ElementTree as ET
tests = failures = errors = skipped = 0
bad = []
for p in sys.argv[1:]:
try:
r = ET.parse(p).getroot()
except Exception as e:
bad.append(f"{p}: unparseable ({e})")
continue
tests += int(r.get('tests', 0))
failures += int(r.get('failures', 0))
errors += int(r.get('errors', 0))
skipped += int(r.get('skipped', 0))
print(f"{tests} {failures} {errors} {skipped}")
for b in bad:
print("UNPARSEABLE " + b, file=sys.stderr)
PY
)
read -r tests failures errors skipped <<<"$counts"
printf 'tests : %s\n' "$tests"
printf 'failures: %s\n' "$failures"
printf 'errors : %s\n' "$errors"
printf 'skipped : %s\n' "$skipped"
rc=0
[ "$gradle_status" -ne 0 ] && rc=1
[ "$failures" -gt 0 ] && rc=1
[ "$errors" -gt 0 ] && rc=1
if [ "$tests" -eq 0 ]; then
printf 'result : FAIL — zero tests executed\n'
printf 'hint : a green build with no tests is not a passing suite\n'
exit 1
fi
if [ $rc -eq 0 ]; then
printf 'result : PASS (%s tests, gradle exit %d)\n' "$tests" "$gradle_status"
else
printf 'result : FAIL (gradle exit %d, %s failures, %s errors)\n' \
"$gradle_status" "$failures" "$errors"
fi
exit $rc
|