test(backend): parallelize isolated fixtures [CI] [SKIP TESTS]
ci/woodpecker/push/verify Pipeline was successful
ci/woodpecker/push/web-build Pipeline was successful
ci/woodpecker/push/dev-image Pipeline was successful

This commit is contained in:
justme committed 2026-10-10 04:54:18 +08:00
1 parent 77d8404f59
commit 238046d959
29 files changed
+84 -66

No files matched your search

@@ -8,8 +8,7 @@ using MoShiro.TestSupport;
namespace MoShiro.Backend.Tests;
/// <summary>public_host 和 ipv6_host 自动识别:Agent 上报公网地址,手填的优先,可以撤销手填。</summary>
[Collection("probe-e2e")]
public sealed class AddressTests
public sealed class AddressTests : IClassFixture<BackendFixture>
{
private const string V4 = "8.8.4.4";
private const string V6 = "2606:4700:4700::1111";
@@ -6,8 +6,7 @@ using MoShiro.TestSupport;
namespace MoShiro.Backend.Tests;
/// <summary>真实的后端 + 真实的 Agent(进程内)+ 假 Probe:覆盖 Agent 与 Backend 的链路、Probe 与 Agent 的点对点会话和数据面。</summary>
[Collection("backend")]
public sealed class AgentEndToEndTests
public sealed class AgentEndToEndTests : IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
@@ -4,8 +4,7 @@ using System.Text.Json.Nodes;
namespace MoShiro.Backend.Tests;
[Collection("backend")]
public sealed class ApiBasicsTests
public sealed class ApiBasicsTests : IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
@@ -226,9 +225,3 @@ public sealed class ApiBasicsTests
Assert.Contains("moshiro_analysis_queue_length", text);
}
}
[CollectionDefinition("backend")]
public sealed class BackendCollection : ICollectionFixture<BackendFixture>;
[CollectionDefinition("probe-e2e")]
public sealed class ProbeEndToEndCollection : ICollectionFixture<BackendFixture>;
@@ -4,8 +4,7 @@ using System.Text.Json.Nodes;
namespace MoShiro.Backend.Tests;
/// <summary>审计日志:写操作、登录成败都会记一行;谁做的(用户名或 key:令牌名)、结果、细节。</summary>
[Collection("probe-e2e")]
public sealed class AuditTests
public sealed class AuditTests : IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
@@ -9,8 +9,7 @@ using MoShiro.Backend.Application;
namespace MoShiro.Backend.Tests;
/// <summary>能力模型(docs/security/access.md 第 3 节):每个管理端点声明能力;密钥只能拿到自己拥有能力的子集;写蕴含读。</summary>
[Collection("probe-e2e")]
public sealed class CapabilityTests
public sealed class CapabilityTests : IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
@@ -5,8 +5,7 @@ using MoShiro.TestSupport;
namespace MoShiro.Backend.Tests;
[Collection("backend")]
public sealed class EnrollmentAndTaskSetTests
public sealed class EnrollmentAndTaskSetTests : IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
@@ -115,8 +115,7 @@ public sealed class HostSamplerTests
}
}
[Collection("backend")]
public sealed class HostStatusTests
public sealed class HostStatusTests : IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
@@ -7,8 +7,7 @@ using MoShiro.TestSupport;
namespace MoShiro.Backend.Tests;
[Collection("backend")]
public sealed class IngestAndQueryTests
public sealed class IngestAndQueryTests : IClassFixture<BackendFixture>
{
private const long Minute = 60_000;
@@ -7,8 +7,7 @@ namespace MoShiro.Backend.Tests;
/// 一行安装:Agent 和 Probe 共用同一份<b>静态</b>脚本(所有人拿到的字节都一样,所以哈希固定、可以先核对再执行),
/// 面板地址、角色、一次性代码都走命令里的环境变量;脚本再向面板取 CI 签名的发布清单来验证镜像。
/// </summary>
[Collection("probe-e2e")]
public sealed class InstallTests
public sealed class InstallTests : IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
@@ -10,9 +10,8 @@ namespace MoShiro.Backend.Tests;
/// <summary>真实双栈回环:同一身份的两种会话、原始块、查询与测试结果必须各自独立。</summary>
// 慢的端到端测试(真实时间的阶梯、要等测量块出来):CI 里单独一批跑(Category=Slow),不拦着出镜像,见 ci/verify-backend.sh
[Collection("probe-e2e")]
[Trait("Category", "Slow")]
public sealed class Ipv6Tests
public sealed class Ipv6Tests : IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
@@ -4,8 +4,7 @@ using MoShiro.TestSupport;
namespace MoShiro.Backend.Tests;
[Collection("probe-e2e")]
public sealed class LinkTests
public sealed class LinkTests : IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
@@ -6,8 +6,7 @@ using MoShiro.TestSupport;
namespace MoShiro.Backend.Tests;
[Collection("backend")]
public sealed class LiveTests
public sealed class LiveTests : IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
@@ -174,8 +174,7 @@ public sealed partial class NodeInfoTests
/// 访客能拿到的东西里不能有 IP 等敏感信息:公开接口的整个响应里不出现 IP 形状的字符串,也没有 ip、city、asn、经纬度这些字段。
/// Agent 和 Probe 这里是真的连上了(Probe 的来源地址是 127.0.0.1),所以如果哪里把地址带出来了,这里一定能发现。
/// </summary>
[Collection("probe-e2e")]
public sealed partial class NodePrivacyTests
public sealed partial class NodePrivacyTests : IClassFixture<BackendFixture>
{
private static readonly string[] s_forbiddenKeys = ["ip", "city", "asn", "latitude", "longitude", "cidr", "remote", "public_host", "geo_host", "host_name", "hostname"];
@@ -6,8 +6,7 @@ using MoShiro.TestSupport;
namespace MoShiro.Backend.Tests;
[Collection("backend")]
public sealed class OnDemandTestsTests
public sealed class OnDemandTestsTests : IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
@@ -7,8 +7,7 @@ namespace MoShiro.Backend.Tests;
/// 对照 docs/api/api.md 第 7 节 检查运行时生成的 OpenAPI 文档:operationId、路径风格、错误响应格式、字段命名。
/// 新增端点违反规范时,这里会直接失败(等价于文档里的 <c>eng/check-openapi</c>)。
/// </summary>
[Collection("backend")]
public sealed partial class OpenApiConformanceTests
public sealed partial class OpenApiConformanceTests : IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
@@ -12,8 +12,7 @@ using MoShiro.TestSupport;
namespace MoShiro.Backend.Tests;
[Collection("backend")]
public sealed class OperationsAndSecurityTests
public sealed class OperationsAndSecurityTests : IClassFixture<BackendFixture>
{
private const long Minute = 60_000;
@@ -38,8 +38,7 @@ public sealed class OwnerPolicyTests
}
}
[Collection("probe-e2e")]
public sealed class OwnerEndToEndTests : IDisposable
public sealed class OwnerEndToEndTests : IDisposable, IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
private readonly BackendOptions _options;
@@ -86,8 +86,7 @@ public sealed class SoftwarePasskey : IDisposable
}
/// <summary>passkey 登录:注册、登录、拒绝各种错误的断言、只能管理自己的 passkey。设计见 docs/security/access.md 第 4 节。</summary>
[Collection("probe-e2e")]
public sealed class PasskeyTests
public sealed class PasskeyTests : IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
@@ -4,9 +4,8 @@ namespace MoShiro.Backend.Tests;
/// <summary>真实的后端 + 真实的 Agent + 真实的 Probe(都在进程内,回环网络)。</summary>
// 慢的端到端测试(真实时间的阶梯、要等测量块出来):CI 里单独一批跑(Category=Slow),不拦着出镜像,见 ci/verify-backend.sh
[Collection("probe-e2e")]
[Trait("Category", "Slow")]
public sealed class ProbeEndToEndTests
public sealed class ProbeEndToEndTests : IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
@@ -12,8 +12,7 @@ namespace MoShiro.Backend.Tests;
/// 二进制通道都有对应的收件箱,文档里写了每一个方法。新增方法绕不过声明,声明了没人处理也会被发现。
/// 注册处理函数时路由器本身还会核对方向、类型和角色,不符就在启动时失败。
/// </summary>
[Collection("probe-e2e")]
public sealed class ProtocolDeclarationTests
public sealed class ProtocolDeclarationTests : IClassFixture<BackendFixture>
{
private static readonly MethodTable s_table = MoShiroMethodTable.Table;
@@ -11,8 +11,7 @@ namespace MoShiro.Backend.Tests;
/// 可信资产来源:官方发布密钥始终信任;部署的人可以另外导入自己的发布公钥(默认没有),用自己的私钥给自己构建的清单签名。
/// 面板只收公钥,拒绝私钥;删除之后只有它签过的清单不再被接受,已登记的也丢掉。
/// </summary>
[Collection("probe-e2e")]
public sealed class ReleaseKeyTests
public sealed class ReleaseKeyTests : IClassFixture<BackendFixture>
{
private static long s_seq = TimeProvider.System.GetUtcNow().ToUnixTimeSeconds() * 1000 + 500_000;
@@ -11,8 +11,7 @@ namespace MoShiro.Backend.Tests;
/// CI 登记清单不需要令牌,但清单必须带 CI 发布密钥的有效签名:令牌存在面板的数据库里,清库、重装或还没有 passkey 的时候就拿不到,
/// 而清单本来就是签名的,面板直接验签更简单,也没有长期令牌可偷。
/// </summary>
[Collection("probe-e2e")]
public sealed class ReleaseOfferTests
public sealed class ReleaseOfferTests : IClassFixture<BackendFixture>
{
// openssl 生成的测试密钥对和签名(`openssl dgst -sha256 -sign`,DER):证明验签接受 CI 脚本实际产出的格式
private const string TestKey = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQwlE2KjYWszeaTsng6CfPxhOqeIV\n+7dm940f7XfHJr9A6/OTiZ2YnhFWWipZ4Z4poPtb6hMW04tIARO8+Km34w==\n-----END PUBLIC KEY-----";
@@ -7,8 +7,7 @@ using MoShiro.TestSupport;
namespace MoShiro.Backend.Tests;
/// <summary>定时重启:默认开、北京时间 04:30;节点连上时补发,管理端改了设置马上群发,关掉就发空。</summary>
[Collection("probe-e2e")]
public sealed class ScheduleTests
public sealed class ScheduleTests : IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
@@ -132,8 +132,7 @@ public sealed class StaticSiteTests
}
/// <summary>HTTP 层:后端自己的路径不会被前端的回退吞掉;响应头对。</summary>
[Collection("probe-e2e")]
public sealed class StaticSiteHttpTests
public sealed class StaticSiteHttpTests : IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
@@ -0,0 +1 @@
[assembly: Xunit.v3.Parallelization(MaxThreads = 16, Mode = Xunit.Sdk.ParallelMode.Collections)]
@@ -11,11 +11,26 @@ namespace MoShiro.Backend.Tests;
public sealed class TestReleaseKey : IReleaseKey
{
private static readonly ECDsa s_key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
private static readonly object s_gate = new();
public static TestReleaseKey Instance { get; } = new();
public string Pem => s_key.ExportSubjectPublicKeyInfoPem();
public string Pem
{
get
{
lock (s_gate)
{
return s_key.ExportSubjectPublicKeyInfoPem();
}
}
}
public static string Sign(string manifest) =>
Convert.ToBase64String(s_key.SignData(Encoding.UTF8.GetBytes(manifest), HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence));
public static string Sign(string manifest)
{
lock (s_gate)
{
return Convert.ToBase64String(s_key.SignData(Encoding.UTF8.GetBytes(manifest), HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence));
}
}
}
@@ -17,8 +17,7 @@ namespace MoShiro.Backend.Tests;
/// 信任根:操作员(passkey)签操作员集合;安装命令里带着创世指纹和属主(都是公开值),节点注册时验证面板的集合并固定在本地。
/// 设计见 docs/security/access.md 第 8 节。
/// </summary>
[Collection("probe-e2e")]
public sealed class TrustTests
public sealed class TrustTests : IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
@@ -5,8 +5,7 @@ using MoShiro.TestSupport;
namespace MoShiro.Backend.Tests;
[Collection("probe-e2e")]
public sealed class UpdateTests
public sealed class UpdateTests : IClassFixture<BackendFixture>
{
private const string Digest = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
+39 -5
View File
@@ -14,8 +14,8 @@ set -euo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
mode="${1:-fast}"
case "$mode" in
fast) filter=(--filter-not-trait "Category=Slow") ;;
slow) filter=(--filter-trait "Category=Slow") ;;
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}"
@@ -70,9 +70,43 @@ quic_runtime="$cache/quic-runtime"
bash "$root/ci/fetch-quic-test-runtime.sh" "$quic_runtime"
export LD_LIBRARY_PATH="$quic_runtime/runtime${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
status=0
# 别的程序集里没有匹配的测试会因为「零个测试」返回 8,忽略它
(cd "$src/backend" && dotnet test --solution MoShiro.slnx -c Release --no-build "${filter[@]}" --ignore-exit-code 8) 2>&1 | tee "$log" || status=${PIPESTATUS[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 阶梯测试里探针掉线):失败的测试单独重跑(最多两次),重跑通过就当通过(日志里留着第一次的失败);
# 失败太多说明是真的坏了,不重跑
@@ -87,7 +121,7 @@ for name in "${failed[@]}"; do
for attempt in 1 2; do
echo "retrying $name (attempt $attempt of 2)"
# 过滤要带通配符;别的程序集里没有匹配的测试,会因为「零个测试」返回 8,忽略它
if (cd "$src/backend" && dotnet test --solution MoShiro.slnx -c Release --no-build --filter-method "*$name*" --ignore-exit-code 8); then
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