Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 56 additions & 23 deletions .github/workflows/shell-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,19 @@ jobs:
shell: bash
run: |
# All tracked Bash scripts outside tests are production by default.
# Check both groups even when production lint fails.
# Both groups lint in parallel, one file per process, and both are
# started before the first wait so a failure in one cannot hide the
# other. pipefail stays on, so a failed file enumeration fails its
# group even though xargs then sees empty input; any failure becomes
# a single exit 1.
set -o pipefail
status=0
git ls-files -z -- "*.sh" ":!:tests/**" | xargs -0 -r shellcheck -x --severity=warning -- || status=1
git ls-files -z -- "tests/*.sh" | xargs -0 -r shellcheck -x --severity=error -- || status=1
git ls-files -z -- "*.sh" ":!:tests/**" | xargs -0 -r -n1 -P4 shellcheck -x --severity=warning -- &
production_pid=$!
git ls-files -z -- "tests/*.sh" | xargs -0 -r -n1 -P4 shellcheck -x --severity=error -- &
test_pid=$!
wait "$production_pid" || status=1
wait "$test_pid" || status=1
exit "$status"

ubuntu-tests:
Expand All @@ -59,30 +68,42 @@ jobs:
fail-fast: false
matrix:
include:
# motd and cloudflare are cut into eight segments; each of their two
# jobs runs four of them, so a single suite spans two jobs.
# motd is cut into eleven segments spread over three jobs of four,
# four and three; push-worker is cut into seven, two jobs of four and
# three; cloudflare is cut into twelve, two jobs of five and seven
# balanced by measured cost, keeping the plan at nineteen jobs under
# the twenty-job concurrency instead of queueing a third job.
- shard: motd-a
suite: motd
segments: 8
segments: 11
segment_set: "0 1 2 3"
- shard: motd-b
suite: motd
segments: 8
segments: 11
segment_set: "4 5 6 7"
- shard: push-worker
- shard: motd-c
suite: motd
segments: 11
segment_set: "8 9 10"
- shard: push-worker-a
suite: push-worker
segments: 7
segment_set: "0 1 2 3"
- shard: push-worker-b
suite: push-worker
segments: 4
segments: 7
segment_set: "4 5 6"
- shard: push
suite: push
segments: 1
- shard: cloudflare-a
suite: cloudflare
segments: 8
segment_set: "0 1 2 3"
segments: 12
segment_set: "2 3 5 6 11"
- shard: cloudflare-b
suite: cloudflare
segments: 8
segment_set: "4 5 6 7"
segments: 12
segment_set: "0 1 4 7 8 9 10"
- shard: other
suite: other
segments: 1
Expand Down Expand Up @@ -125,30 +146,42 @@ jobs:
fail-fast: false
matrix:
include:
# motd and cloudflare are cut into eight segments; each of their two
# jobs runs four of them, so a single suite spans two jobs.
# motd is cut into eleven segments spread over three jobs of four,
# four and three; push-worker is cut into seven, two jobs of four and
# three; cloudflare is cut into twelve, two jobs of five and seven
# balanced by measured cost, keeping the plan at nineteen jobs under
# the twenty-job concurrency instead of queueing a third job.
- shard: motd-a
suite: motd
segments: 8
segments: 11
segment_set: "0 1 2 3"
- shard: motd-b
suite: motd
segments: 8
segments: 11
segment_set: "4 5 6 7"
- shard: push-worker
- shard: motd-c
suite: motd
segments: 11
segment_set: "8 9 10"
- shard: push-worker-a
suite: push-worker
segments: 7
segment_set: "0 1 2 3"
- shard: push-worker-b
suite: push-worker
segments: 4
segments: 7
segment_set: "4 5 6"
- shard: push
suite: push
segments: 1
- shard: cloudflare-a
suite: cloudflare
segments: 8
segment_set: "0 1 2 3"
segments: 12
segment_set: "2 3 5 6 11"
- shard: cloudflare-b
suite: cloudflare
segments: 8
segment_set: "4 5 6 7"
segments: 12
segment_set: "0 1 4 7 8 9 10"
- shard: other
suite: other
segments: 1
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@
生产宿主机(包括 Netcup)仅执行差异检查及改动脚本的 `bash -n`,不运行全量 ShellCheck 或完整测试集;
分析和测试交由 CI:PR 按完整差异及依赖映射选择套件,README/许可证修改无需业务测试;
公共设施或未知依赖改动回退全量,main 推送仍执行全量。两个必需 Job 始终运行,输出选择和跳过记录。
每个 OS 的测试作业按七个 shard 并行:`motd-a`/`motd-b`(`test-motd.sh`,各跑四段)、
`cloudflare-a`/`cloudflare-b`(`test-cloudflare-tunnel.sh`,各跑四段)、`push-worker`
(`test-push-worker-registration.sh`)、`push`(仅 `test-push.sh`)、`other`(其余全部套件);
每个 OS 的测试作业按九个 shard 并行:`motd-a`/`motd-b`/`motd-c`(`test-motd.sh`,11 段按 4/4/3 分配)、
`cloudflare-a`/`cloudflare-b`(`test-cloudflare-tunnel.sh`,12 段按 5/7 分配)、`push-worker-a`/`push-worker-b`
(`test-push-worker-registration.sh`,7 段按 4/3 分配)、`push`(仅 `test-push.sh`)、`other`(其余全部套件);
本机全量用 `tests/run.sh all`。
重套件(motd、Cloudflare Tunnel、push worker 注册)按语义分段,import 守卫 `seg_is`:`TEST_SEGMENTS` 个进程各跑互不重叠的一段并同时执行(本机默认 1,即按原顺序串行)。
motd 与 Cloudflare Tunnel 各切成八段:`-a` 作业用 `TEST_SEGMENT_SET="0 1 2 3"`、`-b` 作业用 `"4 5 6 7"`,两个作业的并集恰好覆盖全部段且互不重叠;push worker 注册仍为四段、单作业。
motd 切成十一段、Cloudflare Tunnel 切成十二段、push worker 注册切成七段:每个作业用 `TEST_SEGMENT_SET` 只跑自己的段,同一套件的所有作业并集恰好覆盖全部段且互不重叠;任一作业的段数、下标或重复项不合法时,在任何套件启动前以状态 2 退出。
`TEST_SEGMENT_SET` 只接受互不重复、且在 `TEST_SEGMENTS` 范围内的段下标,非法值时在任何套件启动前以状态 2 退出;未设置则照旧跑全部段。
Ubuntu 使用固定 ShellCheck v0.10.0:所有受 Git 跟踪、位于 tests/ 之外的 .sh 文件执行 warning 门槛,tests/ 与 tests/lib/ 内的 .sh 文件执行 error 门槛;新目录中的生产 .sh 文件自动纳入,不维护生产文件白名单。Zsh 配置 p10k-config.zsh 不交给 Bash ShellCheck。两组 lint 都会执行并累计失败,Debian 不重复 lint。
例外仅允许局部明确的 SC 规则编号和邻近理由;禁止文件级或全局 warning 抑制。测试 warning 不阻断不等于被 disable,禁止用抑制隐藏行为缺陷。CI 工作流改动继续触发全量测试回退;warning 门槛失败时后续 Ubuntu 行为套件不会执行,不能以 Debian 通过代替双环境验收。
Expand Down
14 changes: 7 additions & 7 deletions tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,14 @@ done
# defines. TEST_SEGMENTS sets how many processes share the work; with
# TEST_SEGMENTS=1 every suite runs in one process, all segments in order.
declare -rA SEGMENTED_SUITES=(
[test-motd.sh]=8
[test-cloudflare-tunnel.sh]=8
[test-push-worker-registration.sh]=4
[test-motd.sh]=11
[test-cloudflare-tunnel.sh]=12
[test-push-worker-registration.sh]=7
)
segments=${TEST_SEGMENTS:-1}
# One digit, at most the eight segments a suite actually has: no accepted value
# can overflow shell arithmetic and every index below stays a single character.
[[ "$segments" =~ ^[1-8]$ ]] || { printf 'Invalid TEST_SEGMENTS: %s\n' "$segments" >&2; exit 2; }
# Bounded lexical form first, so no accepted value can overflow shell arithmetic
# or smuggle in a sign, a prefix or a multi-digit index past the range checks.
[[ "$segments" =~ ^([1-9]|1[0-2])$ ]] || { printf 'Invalid TEST_SEGMENTS: %s\n' "$segments" >&2; exit 2; }

# TEST_SEGMENT_SET narrows one job to a subset of the segments ("0 1 2 3"), so a
# single suite can be spread over several jobs. Unset or blank keeps every
Expand All @@ -94,7 +94,7 @@ if [[ -n "${TEST_SEGMENT_SET:-}" && -n "${TEST_SEGMENT_SET//[[:space:]]/}" ]]; t
segment_value=${segment_value//$'\n'/ }
read -r -a segment_entries <<< "$segment_value"
for segment_index in "${segment_entries[@]}"; do
[[ "$segment_index" =~ ^[0-7]$ ]] \
[[ "$segment_index" =~ ^([0-9]|1[01])$ ]] \
|| { printf 'Invalid TEST_SEGMENT_SET entry: %s\n' "$segment_index" >&2; exit 2; }
(( segment_index < segments )) \
|| { printf 'Segment %s out of range for TEST_SEGMENTS=%s\n' "$segment_index" "$segments" >&2; exit 2; }
Expand Down
33 changes: 27 additions & 6 deletions tests/test-cloudflare-tunnel.sh
Original file line number Diff line number Diff line change
Expand Up @@ -507,8 +507,11 @@ find "$CLOUDFLARED_STATE_DIR" -maxdepth 1 -type d -name 'uninstall-*' -print -qu
pass "configure then real repository uninstall removes source and preserves keyring"
fi

if seg_is 3; then
for signal in HUP INT TERM; do
# The uninstall signal matrix is 39 independent cases (3 signals x 13 phases),
# each building its own fixture. Segments 3 and 4 split it by signal, so the
# serial order stays HUP, INT, TERM exactly as before.
uninstall_signal_phase_cases() {
local signal=$1 phase
case "$signal" in HUP) expected=129 ;; INT) expected=130 ;; TERM) expected=143 ;; esac
for phase in before-snapshot-create capture-auto_update_script capture-auto_update_timer capture-legacy_marker lock-acquired before-disable-auto-update after-disable-auto-update before-apt-remove after-apt-remove before-source-remove after-source-remove final-apt-update before-lock-release; do
new_case
Expand Down Expand Up @@ -537,10 +540,18 @@ for signal in HUP INT TERM; do
release_repository_lock || fail "uninstall $signal at $phase lock cleanup failed"
pass "uninstall $signal=$expected cleans $phase"
done
done
}

if seg_is 3; then
uninstall_signal_phase_cases HUP
uninstall_signal_phase_cases INT
fi

if seg_is 4; then
uninstall_signal_phase_cases TERM
fi

if seg_is 5; then
new_case
set_old_generation
configure_repository
Expand Down Expand Up @@ -667,7 +678,7 @@ for schema_case in version generation duplicate unknown order state mode uid gid
done
fi

if seg_is 5; then
if seg_is 6; then
for ownership_case in owned foreign inode mode owner gid symlink directory fifo; do
(
new_case
Expand Down Expand Up @@ -966,7 +977,7 @@ for restore_id in auto_update_script auto_update_service auto_update_timer sourc
done
fi

if seg_is 6; then
if seg_is 7; then
for target_id in auto_update_script auto_update_service auto_update_timer source current legacy_marker; do
for bad_metadata in mode owner gid symlink directory fifo; do
(
Expand Down Expand Up @@ -998,6 +1009,9 @@ for target_id in auto_update_script auto_update_service auto_update_timer source
pass "capture rejects $target_id $bad_metadata without deleting target"
done
done
fi

if seg_is 8; then

(
unset CLOUDFLARED_TRUST_ANCHOR
Expand Down Expand Up @@ -1049,7 +1063,7 @@ for creation_phase in before-create-marker before-create-manifest-stage before-c
done
fi

if seg_is 7; then
if seg_is 9; then
for bookkeeping_fault in marker stage rename; do
(
new_case
Expand Down Expand Up @@ -1148,6 +1162,9 @@ for capture_file in old-key old-source old-current; do
pass "repository capture $capture_file $capture_fault preserves formal bytes and metadata"
done
done
fi

if seg_is 10; then

(
new_case
Expand Down Expand Up @@ -1256,6 +1273,10 @@ for restore_object in key source current; do
done
done

fi

if seg_is 11; then

for operation in install_cloudflared upgrade_cloudflared; do
(
new_case
Expand Down
78 changes: 69 additions & 9 deletions tests/test-lint-coverage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,41 @@ awk '
block { step=0; block=0 }
END { if (found != 1 || lines == 0) exit 1 }
' "$ROOT_DIR/.github/workflows/shell-tests.yml" > "$root/lint.sh"
# Both groups must keep the per-file, four-way parallel invocation.
if [[ "$(grep -c -- '-n1 -P4' "$root/lint.sh")" != 2 ]]; then
printf 'the lint block no longer starts both groups with -n1 -P4\n' >&2
exit 1
fi
mkdir -p "$root/bin" "$root/repo"
cat > "$root/bin/shellcheck" <<'STUB'
#!/usr/bin/env bash
set -euo pipefail
[[ $1 == -x && $3 == -- && $# -gt 3 ]] || exit 90
# One file per call (xargs -n1), classified by severity: anything else is a defect.
[[ $# -eq 4 && $1 == -x && $3 == -- ]] || exit 90
level=$2
shift 3
for path; do printf "%s\t%s\n" "$level" "$path" >> "$CAPTURE"; done
printf "%s\t%s\n" "$level" "$1" >> "$CAPTURE"
case $level in
--severity=warning) exit "$PRODUCTION_STATUS" ;;
--severity=error) exit "$TEST_STATUS" ;;
*) exit 91 ;;
esac
STUB
chmod +x "$root/bin/shellcheck"
real_git=$(command -v git)
cat > "$root/bin/git" <<STUB
#!/usr/bin/env bash
set -euo pipefail
# Only a file enumeration may be made to fail; everything else is real Git.
for arg in "\$@"; do
case "\${ENUM_FAIL:-none} \$arg" in
"production :!:tests/**"|"both :!:tests/**") exit 1 ;;
"tests tests/*.sh"|"both tests/*.sh") exit 1 ;;
esac
done
exec "$real_git" "\$@"
STUB
chmod +x "$root/bin/git"
export PATH="$root/bin:$PATH" CAPTURE="$root/capture"
cd "$root/repo"
git init -q
Expand All @@ -41,19 +61,59 @@ touch untracked.sh
printf "%s\n" \
$'--severity=warning\tentry.sh' \
$'--severity=warning\tnew/deep/production.sh' \
$'--severity=warning\tnew/space name.sh' \
$'--severity=warning\tnew/space name.sh' > "$root/production.expected"
printf "%s\n" \
$'--severity=error\ttests/direct.sh' \
$'--severity=error\ttests/nested/deep.sh' > "$root/expected"
$'--severity=error\ttests/nested/deep.sh' > "$root/tests.expected"
cat "$root/production.expected" "$root/tests.expected" > "$root/all.expected"
# Multiset comparison: every tracked script of both groups exactly once, with
# its own group's severity. Sorting keeps the parallel order out of the verdict,
# while a duplicated or a missing file still shows up as a difference.
check_linted() {
local label=$1 expected=$2
LC_ALL=C sort "$root/capture" > "$root/capture.sorted"
LC_ALL=C sort "$expected" > "$root/expected.sorted"
if ! diff -u "$root/expected.sorted" "$root/capture.sorted"; then
printf 'FAIL: %s: linted file set is not the tracked split\n' "$label" >&2
exit 1
fi
}
run_lint() {
local status=0
bash --noprofile --norc -e -o pipefail "$root/lint.sh" || status=$?
printf '%s' "$status"
}
# Both groups must always be linted in full, and a failure in either one must
# fail the step: the four result combinations cover that.
export ENUM_FAIL=none
for PRODUCTION_STATUS in 0 1; do
for TEST_STATUS in 0 1; do
export PRODUCTION_STATUS TEST_STATUS
: > "$CAPTURE"
status=0
bash --noprofile --norc -e -o pipefail "$root/lint.sh" || status=$?
status=$(run_lint)
expected=0
if (( PRODUCTION_STATUS != 0 || TEST_STATUS != 0 )); then expected=1; fi
[[ $status == "$expected" ]]
diff -u "$root/expected" "$CAPTURE"
printf "PASS: workflow lint production=%s tests=%s exit=%s\n" "$PRODUCTION_STATUS" "$TEST_STATUS" "$status"
[[ $status == "$expected" ]] \
|| { printf 'FAIL: lint exited %s, expected %s\n' "$status" "$expected" >&2; exit 1; }
check_linted "production=$PRODUCTION_STATUS tests=$TEST_STATUS" "$root/all.expected"
printf "PASS: workflow lint production=%s tests=%s exit=%s\n" \
"$PRODUCTION_STATUS" "$TEST_STATUS" "$status"
done
done
# A failed enumeration must fail the step without hiding the other group: the
# background pipeline keeps pipefail, so git failing is not swallowed by the
# empty xargs run behind it.
for ENUM_FAIL in production tests both; do
export ENUM_FAIL PRODUCTION_STATUS=0 TEST_STATUS=0
: > "$CAPTURE"
status=$(run_lint)
[[ $status == 1 ]] \
|| { printf 'FAIL: %s enumeration failure exited %s\n' "$ENUM_FAIL" "$status" >&2; exit 1; }
case "$ENUM_FAIL" in
production) check_linted "$ENUM_FAIL enumeration failure" "$root/tests.expected" ;;
tests) check_linted "$ENUM_FAIL enumeration failure" "$root/production.expected" ;;
both) [[ ! -s "$CAPTURE" ]] \
|| { printf 'FAIL: both enumerations failed but files were linted\n' >&2; exit 1; } ;;
esac
printf 'PASS: %s enumeration failure exits 1\n' "$ENUM_FAIL"
done
Loading