Files
MoShiro/ci/verify-backend.sh
T

143 lines
6.7 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# 后端测试,带增量编译:把后端源码同步到一个固定目录,在那里 dotnet build / test。
# 用法:verify-backend.sh [fast|slow]
# fast(默认):除了标了 Category=Slow 的以外的全部测试,约 20 秒,出镜像之前要过;
# slow:只跑标了 Category=Slow 的端到端测试(真实时间的阶梯、要等测量块出来,合起来一百来秒),和出镜像并行跑,失败照样红,但不拦着出镜像。
#
# 为什么:每条流水线的工作区都是新克隆的(路径、文件时间都是新的),MSBuild 没法增量,每次从零编译整个解决方案要两分钟。
# 固定目录里 obj/bin 留着,同步时只更新内容真的变了的文件(按内容比较,没变的文件时间不动),MSBuild 就只重编译受影响的项目;
# 改动小的时候整个测试 20–30 秒。
#
# 同一时间只允许一条流水线用这份缓存(flock);增量编译失败(缓存坏了)就清掉缓存从头编译一次,所以最坏也只是回到原来的速度。
set -euo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
mode="${1:-fast}"
case "$mode" in
fast) filter=(--filter-not-trait "Category=Slow"); test_timeout=300 ;;
slow) filter=(--filter-trait "Category=Slow"); test_timeout=900 ;;
*) echo "usage: verify-backend.sh [fast|slow]" >&2; exit 2 ;;
esac
cache="${MOSHIRO_CI_CACHE:-/home/justme/woodpecker/cache/backend-tests}"
src="$cache/src"
mkdir -p "$cache"
# NuGet defaults to /tmp/NuGetScratch<user> on Linux and chmods that shared
# path to 0700. Some Woodpecker container runtimes expose /tmp on a filesystem
# that rejects chmod; keep NuGet's private scratch directory on the writable CI
# cache filesystem instead. This workflow already serializes access with flock.
export NUGET_SCRATCH="${NUGET_SCRATCH:-$cache/nuget-scratch}"
mkdir -p "$NUGET_SCRATCH"
chmod 700 "$NUGET_SCRATCH"
# .NET 第一次运行的欢迎信息、遥测、开发证书,在每条流水线的临时 HOME 里都要白做一遍
export DOTNET_CLI_TELEMETRY_OPTOUT=1 DOTNET_NOLOGO=1 DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 DOTNET_GENERATE_ASPNET_CERTIFICATE=false
exec 9> "$cache/.lock"
flock -w 900 9 || { echo "could not get the backend test cache lock" >&2; exit 1; }
sync_sources() {
mkdir -p "$src"
local list
list="$(mktemp)"
# 构建和测试要用的:backend/、docs/(有的测试读协议文档)、packaging/(Backend.Api 把安装脚本、compose、Quadlet 单元和发布公钥当嵌入资源编进去)、根目录的几个文件(Directory.Packages.props、global.json、NuGet 配置)
(cd "$root" && git ls-files -z -- backend packaging docs Directory.Packages.props global.json 'NuGet.*' 'nuget.*') | tr '\0' '\n' | LC_ALL=C sort -u > "$list"
local changed=0 file
while IFS= read -r file; do
[[ -f "$root/$file" ]] || continue
if ! cmp -s "$root/$file" "$src/$file" 2>/dev/null; then
mkdir -p "$(dirname "$src/$file")"
cp "$root/$file" "$src/$file"
changed=$((changed + 1))
fi
done < "$list"
# 仓库里已经删掉的文件,缓存里也要删(构建产物 artifacts 不动)
local stale=0
while IFS= read -r file; do
[[ -n "$file" ]] || continue
rm -f "$src/$file"
stale=$((stale + 1))
done < <(cd "$src" && find backend packaging docs -type f -not -path 'backend/artifacts/*' -not -path '*/bin/*' -not -path '*/obj/*' 2>/dev/null | LC_ALL=C sort | LC_ALL=C comm -23 - <(grep -E '^(backend|packaging|docs)/' "$list"))
rm -f "$list"
echo "synced sources: $changed changed, $stale removed"
}
build() { (cd "$src/backend" && dotnet build MoShiro.slnx -c Release --nologo -v:q); }
sync_sources
if ! build; then
echo "incremental build failed; wiping the cache and building from scratch" >&2
rm -rf "$src"
sync_sources
build
fi
# 测试在缓存目录里跑完整的解决方案(不再重复编译)
log="$cache/last-test.log"
if [[ -n "${MOSHIRO_QUIC_TEST_RUNTIME:-}" ]]; then
quic_runtime="$MOSHIRO_QUIC_TEST_RUNTIME"
else
quic_runtime="$cache/quic-runtime/runtime"
bash "$root/ci/fetch-quic-test-runtime.sh" "$cache/quic-runtime"
fi
export LD_LIBRARY_PATH="$quic_runtime${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
status=0
# Each test module can spawn its own testhost. Run the whole command in a new
# session and time out its process group, so orphaned testhosts cannot keep the
# Woodpecker step and the shared cache lock alive.
if ! command -v setsid >/dev/null 2>&1; then
echo "setsid is required to enforce the backend test timeout" >&2
exit 2
fi
run_with_timeout() {
local seconds="$1" marker="$cache/test-timeout.marker" test_pid watcher_pid result=0
shift
rm -f "$marker"
(
cd "$src/backend"
setsid "$@" &
test_pid=$!
setsid bash -c 'sleep "$1"; : > "$2"; kill -TERM -- "-$3" 2>/dev/null || true' _ "$seconds" "$marker" "$test_pid" >/dev/null 2>&1 &
watcher_pid=$!
wait "$test_pid" || result=$?
if [[ -f "$marker" ]]; then
kill -KILL -- "-$test_pid" 2>/dev/null || true
result=124
fi
kill -TERM -- "-$watcher_pid" 2>/dev/null || true
wait "$watcher_pid" 2>/dev/null || true
rm -f "$marker"
return "$result"
)
}
# Ignore exit 8 from other assemblies with no matching tests; both suites have a hard wall-clock cap.
run_with_timeout "$test_timeout" dotnet test --solution MoShiro.slnx -c Release --no-build --max-parallel-test-modules 16 "${filter[@]}" --ignore-exit-code 8 2>&1 | tee "$log" || status=${PIPESTATUS[0]}
[[ "$status" == 0 ]] && exit 0
if [[ "$status" == 124 || "$status" == 137 ]]; then
echo "backend tests exceeded the ${test_timeout}s timeout" >&2
exit "$status"
fi
# 个别端到端测试会偶发失败(比如 Hy2 阶梯测试里探针掉线):失败的测试单独重跑(最多两次),重跑通过就当通过(日志里留着第一次的失败);
# 失败太多说明是真的坏了,不重跑
mapfile -t failed < <(grep -E '^(failed|失败) ' "$log" | awk '{print $2}' | awk -F. '{print $NF}' | sort -u)
if ((${#failed[@]} == 0 || ${#failed[@]} > 3)); then
echo "${#failed[@]} failed test(s); not retrying" >&2
exit "$status"
fi
for name in "${failed[@]}"; do
passed=false
# 最多重跑两次:这个端到端测试在 CI 主机上不稳,一次重跑不一定够
for attempt in 1 2; do
echo "retrying $name (attempt $attempt of 2)"
# 过滤要带通配符;别的程序集里没有匹配的测试,会因为「零个测试」返回 8,忽略它
if run_with_timeout 90 dotnet test --solution MoShiro.slnx -c Release --no-build --max-parallel-test-modules 16 --filter-method "*$name*" --ignore-exit-code 8; then
passed=true
break
fi
done
[[ "$passed" == true ]] || { echo "$name failed again" >&2; exit 1; }
done
echo "the failed tests passed on retry"