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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
|
#!/bin/bash
set -e
function help() {
echo "Usage: $0 [build|test] [all|<build_configuration> <build_configuration>...]"
echo "Available build_configuration:"
for build in ${build_configurations[*]}; do
echo " $build"
done
exit 1
}
function validate() {
for build in ${build_configurations[*]}; do
if [ "$1" == "$build" ]; then
return
fi
done
help
}
# CMake records the compiler it detected inside the build directory and keeps using it on
# every later configure. Changing CC would otherwise be ignored without a word: the build
# reports success while still using the compiler the directory was first configured with,
# so anyone verifying a change against a second compiler would be reading stale results.
# Only the C compiler is consulted, because these test trees declare LANGUAGES C.
function compiler_changed() {
local build=$1
local recorded requested
[ -d "build/$build" ] || return 1
recorded=$(sed -n 's/^set(CMAKE_C_COMPILER "\(.*\)")$/\1/p' build/$build/CMakeFiles/*/CMakeCCompiler.cmake 2>/dev/null | head -1)
[ -n "$recorded" ] || return 1
requested=$(command -v "${CC:-gcc}" 2>/dev/null)
[ -n "$requested" ] || return 1
[ "$recorded" != "$requested" ]
}
function generate() {
build=$1
if compiler_changed $build; then
echo "Compiler changed since build/$build was configured. Reconfiguring from scratch."
rm -rf build/$build
fi
cmake -Bbuild/$build -GNinja -DBUILD_SHARED_LIBS=ON -DCMAKE_TOOLCHAIN_FILE=$(dirname $(realpath $0))/../cmake/linux.cmake -DCMAKE_BUILD_TYPE=$build -DTX_COVERAGE=${TX_COVERAGE:-OFF} .
}
function build() {
# -k 0 keeps Ninja going after a target fails, so one broken target no
# longer decides whether the targets after it exist. ctest reports a
# missing binary as a failing test, which turned a single link error into
# a failure count that varied with build scheduling order.
cmake --build build/$1 -- -k 0
}
function build_libs() {
if compiler_changed libs; then
echo "Compiler changed since build/libs was configured. Reconfiguring from scratch."
rm -rf build/libs
fi
cmake -Bbuild/libs -GNinja -DBUILD_SHARED_LIBS=ON -DCMAKE_TOOLCHAIN_FILE=$(dirname $(realpath $0))/../cmake/linux.cmake libs
cmake --build build/libs
}
function test() {
# Guard the pushd: with the caller capturing this function's status, set -e
# no longer aborts here, so a missing build directory would otherwise let
# ctest run in the source tree and report "no tests" as success.
pushd build/$1 || return 1
[ -z "${CTEST_PARALLEL_LEVEL}" ] && parallel="-j$2"
if [ -z "${CTEST_REPEAT_FAIL}" ];
then
repeat_fail=2
else
repeat_fail=${CTEST_REPEAT_FAIL}
fi
# ctest's status is captured rather than allowed to abort the function, and
# returned at the end. It was previously discarded by the popd that follows,
# so a configuration with failing tests returned 0 and was reported as a pass.
# Coverage is collected by collect_all_coverage once every configuration has
# been tested, not here. The gcda exist by the time a test fails, so a run
# that aborted at this point threw away coverage it had already collected --
# and that run is the one whose coverage is worth reading.
local status=0
ctest $parallel --timeout 1000 -O $1.txt -T test --no-compress-output --test-output-size-passed 4194304 --test-output-size-failed 4194304 --output-on-failure --repeat until-pass:${repeat_fail} --output-junit $1.xml || status=$?
popd
# Tolerated because this is a summary for humans, and a ctest that died early
# enough to leave no matching line must not be what stops the coverage below.
grep -E "^(\s*[0-9]+|Total)" build/$1/$1.txt >build/$1.txt || true
sed -i "s/\x1B\[[0-9;]*[JKmsu]//g" build/$1.txt
return $status
}
# Collect for any configuration that was instrumented, which is what TX_COVERAGE
# decides. The build-type match is kept as well, so a single configuration built
# by hand behaves as it did before. The coverage.sh test is a guard: it keeps
# TX_COVERAGE=ON harmless in a tree that has no coverage script.
function collect_coverage() {
if [[ $1 = *"_coverage" ]] || [ "${TX_COVERAGE:-OFF}" = "ON" ]; then
if [ ! -x ./coverage.sh ]; then
echo "No coverage.sh in $(pwd); skipping coverage for $1."
return 0
fi
./coverage.sh $1
fi
}
# Collection runs after every configuration has been tested, and one at a time.
# The merge below needs every tracefile to exist, and gcovr writes intermediate
# gcov output into the directory it is rooted at -- which coverage.sh sets to the
# repository root for all of them, so concurrent runs would share one scratch
# directory.
#
# It does not stop at the first failure, for the reason given in test(): the
# configurations that did produce data should still report.
function collect_all_coverage() {
local item status=0
for item in $builds; do
collect_coverage $item || status=$?
done
# Union the per-configuration reports. Only the union is a coverage figure:
# each configuration compiles a different set of TX_ feature macros, so a
# line one of them compiles out is absent from its denominator rather than
# uncovered in it, and an average of five percentages means nothing.
#
# Done here rather than as a separate workflow step so a local run produces
# the same merged report CI reads.
if [ "${TX_COVERAGE:-OFF}" = "ON" ] && [ -x ./coverage.sh ]; then
./coverage.sh --merge || status=$?
fi
return $status
}
cd $(dirname $0)
result=$(sed -n "/(BUILD_CONFIGURATIONS/,/)/p" CMakeLists.txt|sed ':label;N;s/\n/ /;b label'|grep -Pzo "[a-zA-Z0-9_]*build[a-zA-Z0-9_]*\s*"| tr -d '\0')
IFS=' '
read -ra build_configurations <<< "$result"
if [ $# -lt 1 ]; then
help
fi
command=$1
shift
if [ "$#" == "0" ]; then
builds=${build_configurations[0]}
elif [ "$*" == "all" ]; then
builds=${build_configurations[@]}
else
for item in $*; do
validate $item
done
builds=$*
fi
if [ "$command" == "build" ]; then
for item in $builds; do
generate $item
echo ""
done
# A failing configuration must not stop the ones after it: under set -e
# the loop would abort and leave them unbuilt, which then reads as a wall
# of missing-binary test failures. The status is accumulated and returned.
build_status=0
for item in $builds; do
echo "Building $item"
build $item || build_status=$?
echo ""
done
[ $build_status -eq 0 ] || exit $build_status
elif [ "$command" == "test" ]; then
cores=$(nproc)
if [ -z "${CTEST_PARALLEL_LEVEL}" ];
then
# Run builds in parallel
build_counts=$(echo $builds | wc -w)
parallel_jobs=$(($cores / $build_counts))
parallel_jobs=$(($parallel_jobs + 2))
pids=""
for item in $builds; do
echo "Testing $item"
test $item $parallel_jobs &
pids+=" $!"
done
exit_code=0
for p in $pids; do
wait $p || exit_code=$?
done
# A coverage failure turns the run red, but must not overwrite a test
# failure's status with its own.
coverage_status=0
collect_all_coverage || coverage_status=$?
[ $exit_code -ne 0 ] || exit_code=$coverage_status
exit $exit_code
else
# Run builds in serial. The status is collected the same way the parallel
# branch above collects it, so one failing configuration no longer stops
# the remaining ones from being tested -- or their coverage from being
# collected.
exit_code=0
for item in $builds; do
echo "Testing $item"
test $item $parallel_jobs || exit_code=$?
done
# A coverage failure turns the run red, but must not overwrite a test
# failure's status with its own.
coverage_status=0
collect_all_coverage || coverage_status=$?
[ $exit_code -ne 0 ] || exit_code=$coverage_status
exit $exit_code
fi
elif [ "$command" == "build_libs" ]; then
build_libs
else
help
fi
|