Skip to content

Commit ea3cd18

Browse files
committed
test(e2e): cover the link-command SCALE axis, which no CI job reached (#346)
Every mcpp CI job builds either mcpp itself (tens of TUs) or a synthetic e2e project (single digits). Link-line length, the response-file path and ninja graph size over a large object set therefore had no coverage at all, and the whole command-length defect family surfaced in the ecosystem instead: #274 ninja goals argv 50781 chars vs cmd.exe 8191 #247 Windows CreateProcess 32 KiB #345 POSIX MAX_ARG_STRLEN 128 KiB #360 link.exe LNK1170, response-file line capped at 128 KiB 190 asserts the shape of the generated rule at 25 objects. 191 asserts the scale: 1400 C TUs whose object list is 140 KiB, past the largest command-line ceiling in cmdlimits.cppm, required to link and to run. The regime is asserted rather than assumed. Object naming, the disambiguation prefix and the file count all influence how large the list actually is, so the response file's size is checked against the ceiling directly: if it ever falls back under, the test reports that it has stopped covering the axis instead of passing quietly. Verified to fail without the fix — reverting the generated cxx_link rule to its pre-#345 inline form on this project reproduces the original symptom verbatim: `ninja: fatal: posix_spawn: Argument list too long`. Cost: 1.3s wall on a developer machine. Padded file names carry the object paths to ~100 bytes so the ceiling is reached at a file count this small, bounded on the other side by Windows MAX_PATH.
1 parent 00a8043 commit ea3cd18

1 file changed

Lines changed: 140 additions & 0 deletions

File tree

tests/e2e/191_link_scale.sh

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#!/usr/bin/env bash
2+
# 191_link_scale.sh — a link edge with more objects than a command line can hold.
3+
#
4+
# WHAT THIS COVERS THAT NOTHING ELSE DID
5+
#
6+
# mcpp#346: no CI job builds a package of the opencv/ffmpeg magnitude. Every
7+
# job builds either mcpp itself (tens of TUs) or a synthetic e2e project
8+
# (single digits). Link-line length, the response-file path, and ninja graph
9+
# size over a large object set therefore had ZERO coverage, and the whole
10+
# command-length defect family surfaced in the ecosystem rather than in CI:
11+
#
12+
# #274 ninja goals argv 50781 chars vs cmd.exe 8191
13+
# #247 Windows CreateProcess 32 KiB
14+
# #345 POSIX MAX_ARG_STRLEN 128 KiB (ninja spawns `sh -c "<whole cmd>"`)
15+
# #360 link.exe LNK1170, response-file LINE capped at 128 KiB
16+
#
17+
# 190 asserts the SHAPE of the generated rule (`rspfile_content = $in_newline`,
18+
# 25 objects). This asserts the SCALE: it builds a link edge whose object list
19+
# does not fit in a command line on any supported platform, and requires it to
20+
# link and run. A regression that puts objects back on the command line fails
21+
# here for the same reason opencv-module failed in the ecosystem — except in
22+
# 20 seconds, with no external package, and with a diagnostic that names the
23+
# cause.
24+
#
25+
# WHY THE SIZE ASSERTION IS PART OF THE TEST
26+
#
27+
# The regime is what gives this test its value, and the regime depends on
28+
# incidental things — object naming, the disambiguation prefix, how many files
29+
# the loop below writes. If any of them shrinks the object list back under the
30+
# ceiling, the test would keep passing while covering nothing. So the response
31+
# file's size is asserted directly: below the ceiling the test reports that it
32+
# has stopped covering the axis, rather than passing quietly.
33+
#
34+
# VERIFIED TO FAIL WITHOUT THE FIX
35+
#
36+
# Reverting the generated `cxx_link` rule to its pre-#345 inline form on this
37+
# exact project reproduces the original symptom verbatim:
38+
#
39+
# ninja: fatal: posix_spawn: Argument list too long
40+
#
41+
# COST
42+
#
43+
# C sources, one trivial function each. Measured at 1.3s wall on a developer
44+
# machine (1400 TUs, parallel). Windows is the expensive leg — per-process
45+
# spawn cost dominates there — and is bounded by the suite's 600s per-test
46+
# timeout.
47+
#
48+
# The names are padded so each object path is ~100 bytes, which is what
49+
# reaches the 128 KiB ceiling at a file count this small: padding trades
50+
# compile time (expensive) for path length (free). Padding is bounded on the
51+
# other side by Windows MAX_PATH — 100 bytes relative, plus the temp directory
52+
# and the build directory, stays near 200 of the 260 available.
53+
set -e
54+
55+
TMP=$(mktemp -d)
56+
trap "rm -rf $TMP" EXIT
57+
cd "$TMP"
58+
59+
# ── The ceiling this test has to clear ────────────────────────────────────
60+
# POSIX MAX_ARG_STRLEN, the largest of the command-line limits in
61+
# src/build/cmdlimits.cppm — clearing the largest clears all of them, so one
62+
# number works for every platform the suite runs on.
63+
CEILING=$((128 * 1024))
64+
65+
PAD=$(printf 'x%.0s' $(seq 1 88))
66+
N=1400
67+
68+
mkdir -p scale/src
69+
cat > scale/mcpp.toml <<'EOF'
70+
[package]
71+
name = "scale"
72+
version = "0.1.0"
73+
74+
[build]
75+
c_standard = "c11"
76+
EOF
77+
78+
i=1
79+
while [ "$i" -le "$N" ]; do
80+
printf 'int f%04d(void) { return %d; }\n' "$i" "$i" > "scale/src/f${i}_${PAD}.c"
81+
i=$((i + 1))
82+
done
83+
# The bin target is inferred from src/main.cpp; the objects it links are the C
84+
# TUs above. Referencing the FIRST and the LAST of them is what makes a
85+
# truncated object list a link error rather than a smaller binary: a response
86+
# file cut short at any point drops one of these two.
87+
cat > scale/src/main.cpp <<EOF
88+
extern "C" int f0001(void);
89+
extern "C" int f$(printf '%04d' "$N")(void);
90+
int main() { return (f0001() == 1 && f$(printf '%04d' "$N")() == $N) ? 0 : 1; }
91+
EOF
92+
93+
cd scale
94+
"$MCPP" build > b.log 2>&1 || { tail -40 b.log; echo "FAIL: build $N objects"; exit 1; }
95+
echo " ok: built $((N + 1)) objects"
96+
97+
ninja_file=$(find target -name build.ninja | head -1)
98+
[ -n "$ninja_file" ] || { echo "FAIL: no build.ninja"; exit 1; }
99+
bdir=$(dirname "$ninja_file")
100+
bin_rel=$(cd "$bdir" && ls bin/ 2>/dev/null | head -1)
101+
[ -n "$bin_rel" ] || { echo "FAIL: nothing was linked"; exit 1; }
102+
103+
# Relink with the response file kept, so its real contents can be inspected —
104+
# the same technique as 190, at a size that matters.
105+
(cd "$bdir" && rm -f "bin/$bin_rel" && ninja -d keeprsp "bin/$bin_rel" > relink.log 2>&1) || {
106+
tail -20 "$bdir/relink.log"; echo "FAIL: relink under -d keeprsp"; exit 1; }
107+
108+
rsp=$(find "$bdir" -name '*.rsp' | head -1)
109+
[ -n "$rsp" ] || { echo "FAIL: -d keeprsp left no response file"; exit 1; }
110+
111+
bytes=$(wc -c < "$rsp" | tr -d ' ')
112+
objects=$(( $(wc -l < "$rsp" | tr -d ' ') + 1 ))
113+
114+
# 1. NON-VACUITY: the object list must not fit in a command line. Without
115+
# this, everything below could pass on a link edge small enough that the
116+
# inline form would have worked too.
117+
[ "$bytes" -gt "$CEILING" ] || {
118+
echo "response file is $bytes bytes over $objects objects; the ceiling is $CEILING"
119+
echo "FAIL: this test no longer reaches the regime it exists to cover."
120+
echo " Raise N or PAD until the object list exceeds the ceiling again."
121+
exit 1; }
122+
echo " ok: object list is $bytes bytes ($objects objects), past the ${CEILING}-byte command-line ceiling"
123+
124+
# 2. No single line approaches link.exe's per-line cap (the LNK1170 axis) —
125+
# asserted here at real scale rather than 190's 25 objects.
126+
longest=$(awk '{ if (length($0) > m) m = length($0) } END { print m+0 }' "$rsp")
127+
[ "$longest" -lt 4096 ] || {
128+
echo "FAIL: longest response-file line is $longest chars — objects are not one per line"
129+
exit 1; }
130+
echo " ok: longest response-file line $longest chars"
131+
132+
# 3. The objects were really consumed. main.cpp calls into both ends of the
133+
# object list, so a truncated response file cannot reach this point — it
134+
# fails at link time with an undefined reference. Running the binary closes
135+
# the remaining gap: that the values arriving at runtime are the ones the
136+
# two TUs return.
137+
"./$bdir/bin/$bin_rel" || { echo "FAIL: linked binary did not run cleanly"; exit 1; }
138+
echo " ok: linked binary runs"
139+
140+
echo "OK"

0 commit comments

Comments
 (0)