browser4-cli 4.14.0-rc.6 → 4.14.0-rc.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,421 @@
1
+ #!/usr/bin/env bash
2
+ # reconcile-release-assets.tests.sh
3
+ # Test suite for reconcile-release-assets.sh
4
+ # Uses only bash builtins and standard Unix tools -- no external test
5
+ # frameworks and no network: `gh` is stubbed on PATH.
6
+ #
7
+ # Run:
8
+ # bash cli/scripts/tests/reconcile-release-assets.tests.sh
9
+
10
+ set -euo pipefail
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ SCRIPT_UNDER_TEST="$(cd "$SCRIPT_DIR/.." && pwd)/reconcile-release-assets.sh"
14
+
15
+ PASS=0
16
+ FAIL=0
17
+
18
+ green() { echo -e "\033[0;32m$*\033[0m"; }
19
+ red() { echo -e "\033[0;31m$*\033[0m"; }
20
+ cyan() { echo -e "\033[0;36m$*\033[0m"; }
21
+ dim() { echo -e "\033[2m$*\033[0m"; }
22
+
23
+ # test <name> <command...>
24
+ # The whole command is forwarded: `test "x" bash -c "..."` must not drop the
25
+ # `-c "..."` part, or a bare `bash` would exit 0 and the assertion would pass
26
+ # vacuously.
27
+ test() {
28
+ local name="$1"
29
+ shift
30
+ if "$@"; then
31
+ PASS=$((PASS + 1))
32
+ green " PASS $name"
33
+ else
34
+ FAIL=$((FAIL + 1))
35
+ red " FAIL $name"
36
+ fi
37
+ }
38
+
39
+ # ---- fixture: three release assets ------------------------------------
40
+
41
+ STUB_DIR="$(mktemp -d)"
42
+ cleanup() { rm -rf "$STUB_DIR"; }
43
+ trap cleanup EXIT
44
+
45
+ ASSET_DIR="$STUB_DIR/assets"
46
+ mkdir -p "$ASSET_DIR"
47
+
48
+ # make_asset <name> <bytes> -- a real file of an exact size, so the script's
49
+ # size comparison runs against real byte counts.
50
+ make_asset() {
51
+ local path="$ASSET_DIR/$1" bytes="$2" i=0
52
+ rm -f "$path"
53
+ : > "$path"
54
+ while [ "$i" -lt "$bytes" ]; do
55
+ printf 'x' >> "$path"
56
+ i=$((i + 1))
57
+ done
58
+ }
59
+
60
+ JAR="Browser4.jar"
61
+ CLI_BIN="browser4-cli-linux-x64"
62
+ BUNDLE="browser4-bundle-runtime-linux-x64.tar.gz"
63
+ JAR_BYTES=101
64
+ CLI_BIN_BYTES=202
65
+ BUNDLE_BYTES=303
66
+ make_asset "$JAR" "$JAR_BYTES"
67
+ make_asset "$CLI_BIN" "$CLI_BIN_BYTES"
68
+ make_asset "$BUNDLE" "$BUNDLE_BYTES"
69
+
70
+ ASSET_PATHS=("$ASSET_DIR/$JAR" "$ASSET_DIR/$CLI_BIN" "$ASSET_DIR/$BUNDLE")
71
+
72
+ # ---- stub gh ----------------------------------------------------------
73
+
74
+ cat > "$STUB_DIR/gh" <<'STUB'
75
+ #!/usr/bin/env bash
76
+ # Fake `gh release view` / `gh release upload`, driven by the test environment:
77
+ #
78
+ # FAKE_GH_STATE directory holding one file per asset the release "has",
79
+ # whose content is the size GitHub reports for it
80
+ # FAKE_GH_COUNTS directory holding the per-command invocation counters
81
+ # FAKE_GH_LOG append-only command log the assertions read
82
+ # FAKE_GH_MODE ok | view-fails-once | upload-fails-once |
83
+ # upload-never-succeeds | release-missing
84
+ #
85
+ # upload-fails-once and upload-never-succeeds answer with the message the
86
+ # v4.14.0-rc.6 release run died on.
87
+ set -euo pipefail
88
+
89
+ state="${FAKE_GH_STATE:?FAKE_GH_STATE is required}"
90
+ counts="${FAKE_GH_COUNTS:?FAKE_GH_COUNTS is required}"
91
+ log="${FAKE_GH_LOG:?FAKE_GH_LOG is required}"
92
+ mode="${FAKE_GH_MODE:?FAKE_GH_MODE is required}"
93
+
94
+ bump() {
95
+ local file="$counts/$1" count=0
96
+ if [ -f "$file" ]; then count="$(cat "$file")"; fi
97
+ count=$((count + 1))
98
+ printf '%s' "$count" > "$file"
99
+ printf '%s' "$count"
100
+ }
101
+
102
+ command_name="${1:-} ${2:-}"
103
+
104
+ case "$command_name" in
105
+ "release view")
106
+ printf 'view\n' >> "$log"
107
+ n="$(bump view)"
108
+
109
+ if [ "$mode" = "release-missing" ]; then
110
+ echo "release not found" >&2
111
+ exit 1
112
+ fi
113
+ if [ "$mode" = "view-fails-once" ] && [ "$n" -le 1 ]; then
114
+ echo "HTTP 502: Bad Gateway (https://api.github.com/repos/o/r/releases/tags/v1.2.3)" >&2
115
+ exit 1
116
+ fi
117
+
118
+ for asset in "$state"/*; do
119
+ [ -e "$asset" ] || continue
120
+ printf '%s %s\n' "$(basename "$asset")" "$(cat "$asset")"
121
+ done
122
+ ;;
123
+
124
+ "release upload")
125
+ # Skip the subcommand and the tag; keep the arguments that are real
126
+ # files -- --repo and its value are not.
127
+ shift 2
128
+ shift
129
+ files=()
130
+ for arg in "$@"; do
131
+ [ -f "$arg" ] || continue
132
+ files+=("$arg")
133
+ done
134
+
135
+ names=()
136
+ for f in "${files[@]}"; do names+=("${f##*/}"); done
137
+ printf 'upload %s\n' "${names[*]}" >> "$log"
138
+
139
+ n="$(bump upload)"
140
+ fail_round=0
141
+ case "$mode" in
142
+ upload-fails-once)
143
+ if [ "$n" -le 1 ]; then fail_round=1; fi
144
+ ;;
145
+ upload-never-succeeds)
146
+ fail_round=1
147
+ ;;
148
+ esac
149
+ if [ "$fail_round" -eq 1 ]; then
150
+ echo "Error creating asset temp dir" >&2
151
+ exit 1
152
+ fi
153
+
154
+ for f in "${files[@]}"; do
155
+ printf '%s' "$(wc -c < "$f" | tr -d '[:space:]')" > "$state/${f##*/}"
156
+ echo "Uploaded ${f##*/}"
157
+ done
158
+ ;;
159
+
160
+ *)
161
+ echo "fake gh: unhandled command '${command_name}'" >&2
162
+ exit 2
163
+ ;;
164
+ esac
165
+ STUB
166
+ chmod +x "$STUB_DIR/gh"
167
+
168
+ # ---- runner -----------------------------------------------------------
169
+
170
+ RUN_STATUS=0
171
+ RUN_OUTPUT=''
172
+ RUN_STATE=''
173
+ RUN_COUNTS=''
174
+ RUN_LOG=''
175
+
176
+ # new_run -- fresh release state, command log and counters for one scenario
177
+ new_run() {
178
+ RUN_STATE="$STUB_DIR/state.$$.$RANDOM"
179
+ RUN_COUNTS="$STUB_DIR/counts.$$.$RANDOM"
180
+ RUN_LOG="$STUB_DIR/log.$$.$RANDOM"
181
+ rm -rf "$RUN_STATE" "$RUN_COUNTS" "$RUN_LOG"
182
+ mkdir -p "$RUN_STATE" "$RUN_COUNTS"
183
+ : > "$RUN_LOG"
184
+ }
185
+
186
+ # seed_asset <name> [<bytes>] -- an asset the release already carries, at the
187
+ # size GitHub reports for it (defaults to the local size, i.e. a good upload)
188
+ seed_asset() {
189
+ local name="$1" bytes="${2:-}"
190
+ if [ -z "$bytes" ]; then
191
+ bytes="$(wc -c < "$ASSET_DIR/$name" | tr -d '[:space:]')"
192
+ fi
193
+ printf '%s' "$bytes" > "$RUN_STATE/$name"
194
+ }
195
+
196
+ # run_script <mode> <timeout> <interval> <settle> [file...]
197
+ run_script() {
198
+ local mode="$1" timeout="$2" interval="$3" settle="$4"
199
+ shift 4
200
+
201
+ RUN_STATUS=0
202
+ RUN_OUTPUT="$(
203
+ FAKE_GH_MODE="$mode" \
204
+ FAKE_GH_STATE="$RUN_STATE" \
205
+ FAKE_GH_COUNTS="$RUN_COUNTS" \
206
+ FAKE_GH_LOG="$RUN_LOG" \
207
+ PATH="$STUB_DIR:$PATH" \
208
+ bash "$SCRIPT_UNDER_TEST" \
209
+ --tag v1.2.3 \
210
+ --repo owner/repo \
211
+ --timeout "$timeout" \
212
+ --interval "$interval" \
213
+ --settle "$settle" \
214
+ "$@" 2>&1
215
+ )" || RUN_STATUS=$?
216
+ }
217
+
218
+ count_of() {
219
+ local file="$RUN_COUNTS/$1"
220
+ if [ -f "$file" ]; then cat "$file"; else printf '0'; fi
221
+ }
222
+
223
+ upload_calls() { count_of upload; }
224
+
225
+ # uploaded_names -- every asset name the script asked gh to upload
226
+ uploaded_names() {
227
+ sed -n 's/^upload //p' "$RUN_LOG" | tr ' ' '\n' | sed '/^$/d' | LC_ALL=C sort
228
+ }
229
+
230
+ output_has() {
231
+ case "$RUN_OUTPUT" in
232
+ *"$1"*) return 0 ;;
233
+ *) return 1 ;;
234
+ esac
235
+ }
236
+
237
+ # ---- pre-flight -------------------------------------------------------
238
+
239
+ cyan "============================================"
240
+ cyan " reconcile-release-assets.sh Test Suite"
241
+ cyan "============================================"
242
+ echo ""
243
+
244
+ dim "--- Pre-flight ---"
245
+
246
+ test "file exists" bash -c "[ -f '$SCRIPT_UNDER_TEST' ]"
247
+ test "starts with shebang" bash -c "head -c 20 '$SCRIPT_UNDER_TEST' | grep -q '^#!/bin/bash'"
248
+ test "bash syntax check passes" bash -c "bash -n '$SCRIPT_UNDER_TEST' 2>&1"
249
+ # Byte-oriented on purpose: `grep -P` is unavailable in some environments and
250
+ # decodes the file through the host locale.
251
+ test "no non-ASCII bytes" bash -c "! LC_ALL=C grep -q '[^[:print:][:space:]]' '$SCRIPT_UNDER_TEST'"
252
+
253
+ echo ""
254
+
255
+ # ---- behaviour --------------------------------------------------------
256
+
257
+ # The common case: the publishing action already uploaded everything. The
258
+ # script must cost one listing call, no upload, and say so.
259
+ test_verifies_a_complete_release_without_uploading() {
260
+ new_run
261
+ seed_asset "$JAR"
262
+ seed_asset "$CLI_BIN"
263
+ seed_asset "$BUNDLE"
264
+ run_script ok 20 1 0 "${ASSET_PATHS[@]}"
265
+ [ "$RUN_STATUS" -eq 0 ] || return 1
266
+ output_has "Verified 3 asset(s) on release v1.2.3" || return 1
267
+ [ "$(upload_calls)" -eq 0 ] || return 1
268
+ [ "$(count_of view)" -eq 1 ] || return 1
269
+ }
270
+
271
+ # The v4.14.0-rc.6 failure: some assets never made it. Only those are sent
272
+ # again -- re-uploading the whole set is what makes the job slow.
273
+ test_uploads_only_the_missing_assets() {
274
+ new_run
275
+ seed_asset "$JAR"
276
+ run_script ok 20 1 0 "${ASSET_PATHS[@]}"
277
+ [ "$RUN_STATUS" -eq 0 ] || return 1
278
+ [ "$(uploaded_names)" = "$(printf '%s\n%s\n' "$BUNDLE" "$CLI_BIN" | LC_ALL=C sort)" ] || return 1
279
+ [ "$(count_of view)" -ge 2 ] || return 1
280
+ }
281
+
282
+ # A failed upload can leave a partial asset behind: size, not presence, decides.
283
+ test_repairs_a_partial_asset_by_size() {
284
+ new_run
285
+ seed_asset "$JAR"
286
+ seed_asset "$CLI_BIN"
287
+ seed_asset "$BUNDLE" 7
288
+ run_script ok 20 1 0 "${ASSET_PATHS[@]}"
289
+ [ "$RUN_STATUS" -eq 0 ] || return 1
290
+ output_has "size mismatch: the release has 7 bytes, the file has 303" || return 1
291
+ [ "$(uploaded_names)" = "$BUNDLE" ] || return 1
292
+ [ "$(cat "$RUN_STATE/$BUNDLE")" = "$BUNDLE_BYTES" ] || return 1
293
+ }
294
+
295
+ # The CI failure itself: GitHub answers "Error creating asset temp dir". The
296
+ # round must be retried instead of failing the release.
297
+ test_survives_the_transient_upload_error() {
298
+ new_run
299
+ run_script upload-fails-once 20 1 0 "${ASSET_PATHS[@]}"
300
+ [ "$RUN_STATUS" -eq 0 ] || return 1
301
+ output_has "Error creating asset temp dir" || return 1
302
+ output_has "WARN: gh release upload failed" || return 1
303
+ output_has "Verified 3 asset(s) on release v1.2.3" || return 1
304
+ [ "$(upload_calls)" -ge 2 ] || return 1
305
+ [ "$(cat "$RUN_STATE/$BUNDLE")" = "$BUNDLE_BYTES" ] || return 1
306
+ }
307
+
308
+ # The listing API is just as capable of a 5xx as the upload endpoint.
309
+ test_survives_a_transient_listing_error() {
310
+ new_run
311
+ seed_asset "$JAR"
312
+ seed_asset "$CLI_BIN"
313
+ seed_asset "$BUNDLE"
314
+ run_script view-fails-once 20 1 0 "${ASSET_PATHS[@]}"
315
+ [ "$RUN_STATUS" -eq 0 ] || return 1
316
+ output_has "WARN: could not list the assets of release v1.2.3" || return 1
317
+ output_has "Verified 3 asset(s) on release v1.2.3" || return 1
318
+ }
319
+
320
+ # A missing release is the creation path, not the upload path: say so instead
321
+ # of recreating it here with metadata the workflow did not pass.
322
+ test_fails_when_the_release_does_not_exist() {
323
+ new_run
324
+ run_script release-missing 20 1 0 "${ASSET_PATHS[@]}"
325
+ [ "$RUN_STATUS" -eq 1 ] || return 1
326
+ output_has "release v1.2.3 does not exist" || return 1
327
+ output_has "release not found" || return 1
328
+ }
329
+
330
+ # Uploads that never take effect must fail the job, with the offenders named.
331
+ test_fails_when_uploads_never_take_effect() {
332
+ new_run
333
+ run_script upload-never-succeeds 2 1 0 "${ASSET_PATHS[@]}"
334
+ [ "$RUN_STATUS" -eq 1 ] || return 1
335
+ output_has "still does not match the build output after 2s" || return 1
336
+ output_has " - $BUNDLE: missing from the release" || return 1
337
+ output_has "Re-run the failed jobs of this workflow" || return 1
338
+ }
339
+
340
+ # ... and it must give up on time rather than sleep past its budget.
341
+ test_respects_the_time_budget() {
342
+ local started elapsed
343
+ new_run
344
+ started=$SECONDS
345
+ run_script upload-never-succeeds 2 1 0 "${ASSET_PATHS[@]}"
346
+ elapsed=$((SECONDS - started))
347
+ [ "$RUN_STATUS" -eq 1 ] || return 1
348
+ [ "$elapsed" -le 10 ] || return 1
349
+ }
350
+
351
+ # The release workflow hands over the manifest it already builds: one asset
352
+ # path per line, with blank lines and CRLF tolerated.
353
+ test_reads_a_newline_separated_manifest() {
354
+ local manifest="$STUB_DIR/manifest.txt"
355
+ {
356
+ printf '%s\r\n' "${ASSET_PATHS[0]}"
357
+ printf '\n'
358
+ printf '%s\n%s\n' "${ASSET_PATHS[1]}" "${ASSET_PATHS[2]}"
359
+ } > "$manifest"
360
+
361
+ new_run
362
+ seed_asset "$JAR"
363
+ seed_asset "$CLI_BIN"
364
+ seed_asset "$BUNDLE"
365
+ run_script ok 20 1 0 --files-from "$manifest"
366
+ [ "$RUN_STATUS" -eq 0 ] || return 1
367
+ output_has "Verified 3 asset(s) on release v1.2.3" || return 1
368
+ [ "$(upload_calls)" -eq 0 ] || return 1
369
+ }
370
+
371
+ test_rejects_a_missing_asset_file() {
372
+ new_run
373
+ run_script ok 20 1 0 "$ASSET_DIR/not-built.tar.gz"
374
+ [ "$RUN_STATUS" -eq 1 ] || return 1
375
+ output_has "does not exist" || return 1
376
+ [ "$(count_of view)" -eq 0 ] || return 1
377
+ }
378
+
379
+ test_requires_a_tag() {
380
+ local output status
381
+ output="$(PATH="$STUB_DIR:$PATH" bash "$SCRIPT_UNDER_TEST" "${ASSET_PATHS[@]}" 2>&1)" && status=0 || status=$?
382
+ [ "$status" -ne 0 ] || return 1
383
+ [[ "$output" == *"--tag is required"* ]] || return 1
384
+ [[ "$output" == *"Usage:"* ]] || return 1
385
+ }
386
+
387
+ test_requires_asset_files() {
388
+ local output status
389
+ output="$(PATH="$STUB_DIR:$PATH" bash "$SCRIPT_UNDER_TEST" --tag v1.2.3 2>&1)" && status=0 || status=$?
390
+ [ "$status" -ne 0 ] || return 1
391
+ [[ "$output" == *"no asset files given"* ]] || return 1
392
+ [[ "$output" == *"Usage:"* ]] || return 1
393
+ }
394
+
395
+ dim "--- Behaviour ---"
396
+
397
+ test "complete release: verified, nothing uploaded" test_verifies_a_complete_release_without_uploading
398
+ test "uploads only the missing assets" test_uploads_only_the_missing_assets
399
+ test "repairs a partial asset by size" test_repairs_a_partial_asset_by_size
400
+ test "survives the transient upload error that failed v4.14.0-rc.6" test_survives_the_transient_upload_error
401
+ test "survives a transient listing error" test_survives_a_transient_listing_error
402
+ test "fails when the release does not exist" test_fails_when_the_release_does_not_exist
403
+ test "fails when uploads never take effect" test_fails_when_uploads_never_take_effect
404
+ test "respects the time budget" test_respects_the_time_budget
405
+ test "reads a newline-separated manifest" test_reads_a_newline_separated_manifest
406
+ test "rejects a missing asset file" test_rejects_a_missing_asset_file
407
+ test "requires a tag" test_requires_a_tag
408
+ test "requires asset files" test_requires_asset_files
409
+
410
+ # ---- summary ----------------------------------------------------------
411
+
412
+ echo ""
413
+ cyan "============================================"
414
+ if [ "$FAIL" -eq 0 ]; then
415
+ green " All $PASS tests passed"
416
+ else
417
+ red " $FAIL of $((PASS + FAIL)) tests failed"
418
+ fi
419
+ cyan "============================================"
420
+
421
+ [ "$FAIL" -eq 0 ]