解决MySql布尔型新旧版本兼容问题,采用枚举来表示布尔型的数据表。由正向工程赋值
|
# 单元测试稳定性规约
> 目标:VS2026 全量跑测与 `dotnet test`(含 `Tools/Scripts/run-tests.ps1` 全宿主)**首跑即绿**,且不显著变慢。
> 本规约约束 NewLife.Core 仓库全部测试工程(XUnitTest.Core / XUnitTest.Net6 / XUnitTest.Compat / XUnitTest.SourceGenerator)。
## 1. 硬规则(新增/修改测试必须遵守)
| # | 规则 | 反例 | 正例 |
|---|------|------|------|
| 1 | **不依赖外网与外部服务**:公网 HTTP、真实 DNS 一律本地化(本地服务桩/注入桩/自签证书) | 直接请求 `star.newlifex.com`,网络抖动即红 | 本地 `HttpServer` 桩 + `DnsResolver.Set` 把域名指向本机 |
| 2 | **等待一律条件轮询**,禁止用固定 Sleep 作为唯一等待手段;超时仅作防挂起兜底(≥5 秒),不作为性能判定 | `Thread.Sleep(1000)` 后断言计数 | `TestWait.Until(() => count >= 1, 10_000)` |
| 3 | **断言右值不得使用断言时刻的 `DateTime.Now`**;涉及时间推移的断言必须捕获动作前时间戳 | `Assert.True(endpoint.NextTime >= DateTime.Now.AddSeconds(1))` | `var t0 = DateTime.Now; ...` 断言 `NextTime >= t0.AddSeconds(1)` |
| 4 | **进程级全局态改写必须恢复或串行化**(`Tracer/XTrace/BasePath/Setting/ActivityListener/DnsResolver.Instance` 等);不能恢复的用例并入 `GlobalState`/`Telemetry` 串行集合 | 并行用例挂/卸全局 ActivityListener | `[Collection("Telemetry")]` + `DisableParallelization=true` 集合 |
| 5 | **网络用例端口策略**:单协议用动态端口(`Port=0`);双协议组合用 `TestPort` 分配,不得硬编码端口 | `new NetServer { Port = 8080 }` | `Port = 0`,或 `TestPort.StartAny(...)` 顺延重试 |
| 6 | **性能/吞吐阈值只允许语义化口径**,严格阈值断言迁至 `Benchmark/` 工程 | 断言"100ms 内必须完成" | 断言"取消后不等到读超时才结束(10 秒兜底)" |
| 7 | **环境工具缺失显式跳过并留痕**,不得失败 | 无 7z.exe 时直接开工导致异常 | 探测 `SevenZip` 可用性,缺失时写入日志并 return |
| 8 | **跨平台用例**:平台专属 API(WMI、Windows Tcp 表等)显式跳过并留痕;文本比对按换行归一(`TestText.NormalizeNewline`);HTTP 报文数据用 `TestText.Crlf` 显式构造;文件/目录名按真实大小写引用 | Linux 上断言 WMI 结果非空;原始字符串直接比较;路径写 `http/` | `if (!Runtime.Windows) { XTrace.WriteLine(...); return; }`;`Assert.Equal(a.NormalizeNewline(), b.NormalizeNewline())` |
## 2. 工具箱
| 工具 | 位置 | 用途 |
|------|------|------|
| `TestWait.Until/UntilAsync/CompletedAsync` | `XUnitTest.Core/Common/TestWait.cs` | 条件轮询等待、任务限时完成 |
| `TestPort.StartAny` | `XUnitTest.Core/Net/TestPort.cs` | 双协议用例的动态端口分配(候选段顺延重试) |
| `DnsResolver.Instance.Set(host, addrs)` | 产品 `NewLife.Core/Net/IDnsResolver.cs` | 把域名指向本机,构造脱网用例 |
| `DnsResolver.ResolveHostAsync`(`protected virtual`) | 同上 | 子类注入确定性解析桩(测试专用最小缝) |
| 本地服务桩 | `HttpServer`/`HttpListener` + 仓库自签 pfx(`certs/newlifex.com.pfx`) | 替代公网站点,覆盖 HTTP/TLS 语义 |
| `TestBlackhole` | `XUnitTest.Core/Common/TestBlackhole.cs` | 本机连接黑洞(loopback 打满 backlog + 自检),构造建连超时用例,替代文档网段 |
## 3. 验收流程
| 场景 | 命令/方式 | 通过标准 |
|------|----------|---------|
| 本地轮次压测 | `pwsh Tools/Scripts/run-tests.ps1 -Repeat 10`(`-SkipBuild` 可复用产物) | 全部轮次零失败 |
| VS2026 全量 | Test Explorer 全量跑测(根 `.runsettings` 已把宿主并行限为 5) | 首跑零失败 |
| CI | `test.yml`(Windows 矩阵 10 个 job + 跨平台矩阵 + musl 容器 + LA64(loong64 容器 + QEMU,仅人工触发)+ 指令集矩阵(riscv64 / s390x,QEMU,push/PR 跑 smoke 子集、全量人工触发);`test.ci.runsettings`/`test.ci.macos.runsettings` 按 runner 核数限并行;附 `--blame-hang` 挂起转储) | 阻断门禁(Windows 矩阵 + linux-x64/linux-arm64)全绿 |
| 断网验证 | 阻断外网后跑全量 | 零失败(证明用例完全本地) |
## 4. 已知踩坑(保留案例,防止回归)
- **竞速调用用例**:断言用断言时刻 `DateTime.Now` 作右值,满载调度停顿 >2s 即误报(已改为捕获动作前 `t0`)。
- **OTel 监听器串扰**:采集器用例挂载"监听全部源"的进程级监听器,与跟踪器"无监听者"用例并行即互相污染(已并入 `Telemetry` 串行集合)。
- **DnsResolver 真实解析**:同步刷新走真实 `Dns.GetHostAddressesAsync` 且 5s 超时,满载超时即保留旧值误报(已加 `ResolveHostAsync` 缝 + 确定性桩)。
- **公网冒烟**:`GetStringWithDnsResolver2` 曾因站点 502 在 CI 整批变红(本地化进行中)。
- **7z.exe 环境依赖**:CI runner 缺失工具导致 `SevenZipTests` 四例失败(改为工具探测跳过)。
- **7z 目录通配符的反斜杠(已修)**:`SevenZip.Compress` 对目录拼通配符用 `EnsureEnd("\\") + "*"`,Windows 正常;Linux/macOS 下 7z 按字面路径匹配不到任何文件,以警告码 1(被容忍)退出并生成空归档,解压端表现为 `file1.txt` 缺失——ubuntu runner 自带 7z 会让用例真正执行并暴露,而 WSL 无 7z 时自动跳过(本地掩盖)。已改用 `Path.Combine` 平台分隔符;Unix 上工具可用的用例需在两条路径都验证。
- **UDP 并发吞吐的共享 runner 降级**:突发 5000~10000 包时服务端消费慢于突发,socket 缓冲溢出成批丢包,收到率不足 50% 直接超时(linux-x64/arm64、musl 实测)。已加"每 20 包让出约 1ms"抑制突发;`CI=true` 时等待窗口放宽到 180s、送达门槛降为 10% 且不做 TPS 结论性断言(共享 runner 挂钟含调度抖动),本地/专用机保持 50% 与 3000 TPS 硬门禁。
- **runner 镜像收紧旧版 SDK 目标包**:windows-latest 镜像不再预装 .NET 8/9 的 WindowsDesktop 目标包,只装 `10.x` 的 job 构建 `net8.0-windows`/`net9.0-windows` 报 `NETSDK1127`;build/pack/analyzer 三个 job 一律补装 `8.x`/`9.x`(与测试矩阵条目一致)。
- **LA64 必须容器内运行**:裸宿主 `qemu-user-static` 缺 LA64 动态加载器(`Could not open /lib64/ld-linux-loongarch-lp64d.so.1`),LA64 版 dotnet 无法直接执行;改用 docker + QEMU(loong64 binfmt)+ `lcr.loongnix.cn/debian:14-slim` 容器(自带 loader、apt 源指向 deb.debian.org,容器内补装 libicu/libssl)。**LA64 上 vstest 不可用**:龙芯 SDK 自带的 vstest 不识别 LoongArch64 架构而抛 `NotSupportedException`(10.0.112/10.0.113 均复现),宿主 SDK 的 vstest 是 x64 R2R 复合镜像、加载即 `BadImageFormatException`;因此改为**自宿主 xunit 执行器**(`xunit.runner.utility` 构建的极简 runner,纯 IL 可在 LA64 运行,复制进测试输出目录以保证 `AppContext.BaseDirectory` 与 VSTest 一致),宿主管还原与构建、容器内以 LA64 运行时直接执行测试程序集。AOT 的 LA64 冒烟与单测 LA64 自 2026-10-17 起均为观察期:仅人工 `workflow_dispatch` 触发,push/PR 不参与、失败不阻塞(QEMU 下整套单测实测 ≥2 小时仍未跑完)。
- **自宿主执行器已工具化**:新架构统一用 [Tools/SelfHostRunner](/NewLife/X/Blob/dev/Doc/../Tools/SelfHostRunner/Program.cs)(`xr.dll`),参数 `--namespace A,B`(**精确匹配**)、`--class X`、`--root`(仅根命名空间)、`--list`;**过滤无匹配时返回 3**,避免命名空间拼错导致"零用例假绿"。LA64 仍沿用 job 内联版,后续统一。
- **riscv64 运行时来自社区构建**:官方不发 runtime pack / SDK,CI 固定 `filipnavara/dotnet-riscv` 的 `10.0.113`(跟踪 dotnet VMR 全量构建,glibc / musl 两档);断供时按《AOT兼容性分析》§7.4 的替换渠道处理。仿真架构下 vstest 不可靠,统一走自宿主执行器,并设 `DOTNET_EnableHWIntrinsic=0` / `DOTNET_EnableWriteXorExecute=0` 规避仿真层未实现指令。
- **s390x 是大端**:`BitConverter.IsLittleEndian == false`,而 `SpanReader/Writer` 默认 `IsLittleEndian=true`。该平台首轮失败必须分流——环境/端序假设导致的失败按规则 7/8 显式跳过并留痕;产品真实字节序缺陷记入《功能清单》待修复项后修复,**不得放宽断言换绿灯**(否则大端验证失去意义)。实测两类样本:①产品缺陷——`IOHelper.ToUInt32/ToUInt64` 用 `BitConverter` 按主机序读取(`isLittleEndian:true` 失效)、`BinaryGeneral` 按"主机必为小端"取反(已修);②用例假设——`SpanWriterTests` 用 `BitConverter` 校验显式小端写入(已改 `BinaryPrimitives`)、`/proc/net/tcp` 的 IP 解析与 `CsvFile` 行数(已修)。
- **WSL 复现 ISA 的固定动作**:`ubuntu-base` rootfs + 目标架构运行时 + `chroot`;chroot 前必须 `mount --bind /dev`、`mount -t proc proc`,否则 .NET 直接报 `Mono requires /proc to be mounted`。仿真下**整套测试不要开并行**:QEMU 用户态下并行执行偶发 binfmt `SIGSEGV`(s390x `Log` 命名空间可复现,单线程连续多轮稳定),指令集 job 固定 `maxParallelThreads=1`,并注入 `NEWLIFE_TEST_QEMU=1` 让时序敏感的 TLS 用例显式跳过(wss 自签证书握手、协议模式 SSL 大帧);容器脚本在"无 TOTAL 输出"时自动重试一次,覆盖仿真层偶发崩溃。
- **类初始化互等死锁**:finalizer 线程经 `Pool.OnGen2 → Runtime.TickCount64` 触碰静态初始化,与 `TimerScheduler` 监视器互等导致整轮挂起(产品已修两处;CI 加 `--blame-hang` 监控)。
- **UDP 用例**:UDP 不可靠,满载下回环也会丢包(多数据报拉取曾 15 秒超时);等待必须"有界重发 + 事件断言",不得单发单等,超时须取消挂起的接收。
- **跨平台换行**:仓库不同文件行尾不一(部分按 LF 存储),原始字符串直接比较、或直接把字面量 GetBytes 当 HTTP 报文,会随检出平台假红(已加 `TestText.NormalizeNewline`/`TestText.Crlf` 两类助手)。
- **大小写敏感文件系统**:静态文件(`Http/`)与等级日志目录(`LevelLog/Debug`)必须按真实产物大小写引用,Windows 上碰巧通过不代表正确。
- **平台专属 API**:WMI、Windows Tcp 表在 Linux/macOS 不可用,用例一律 `!Runtime.Windows` 显式跳过并留痕;7z 由产品按平台探测(Windows 7z.exe、Unix PATH 中的 7z/7za/7zr/7zz)。
- **Net6 宿主的 Linux 全量并行**:net6.0 宿主在 Linux 全量并行下偶发挂起(隔离单跑全过,疑似测试宿主并行交互),Net6 维持仅 Windows 矩阵,跨平台矩阵从 net8.0 起步。
- **连接超时用例**:曾连文档网段 `192.0.2.1` 制造黑洞,网络环境不同即结果不定(已改 `TestBlackhole` 本机黑洞与本地拒绝端口)。
- **竞速负载均衡**:默认探测会真实访问服务地址并异步改写端点状态(Rtt/Errors),与排序断言存在竞态窗口(已注入确定性探测桩)。
- **blame-hang 参数大小写**:`--blame-hang-dump-type` 取值 `full/mini/none` 区分大小写,写成 `Mini` 会被 `dotnet test` 判为非法参数,整个 job 秒退(测试一条没跑,日志里只剩 Usage)。
- **容器等精简环境缺机器信息源**:alpine 容器不挂载 `/dev/disk/by-*`,arm64 的 `/proc/cpuinfo` 也不带型号键,`MachineInfoTests` 的磁盘标识与 `Processor` 断言按"环境缺失→显式跳过并留痕"处理;`TcpSessionTests.BindTest4` 同理(网卡只有环回地址时跳过)。
- **抢锁用例的 GC 竞态**:`MemoryCacheTests.TestLock2` 未用 `using` 持有锁引用,Release 下局部变量可被提前回收、锁被终结器提前释放,时序断言偶发失真(Linux 满载实测复现);已 `GC.KeepAlive` 保持引用到用例末尾,并把到期清理的"睡死等待"改为轮询等待键消失(最长 3 秒)。
- **停滞判定的墙钟回拨**:`HttpSession.MaxRequestIdle` 原用 `DateTime.Now` 计算停滞间隔,WSL2 实测墙钟每约 30 秒回拨 ~2 秒(75 秒累计 -3.8 秒),落在观察窗内即令慢速攻击判定失效(`StalledRequest_ReturnsRequestTimeout` 偶发假 200,约 8%),墙钟前跳还会误杀持续推进的慢速上传;产品已改单调时钟 `Runtime.TickCount64`,用例客户端等待同时由 3.5s 提到 5s 留调度余量。
- **加密用例宿主崩溃(观察)**:Linux(WSL)全量并行下曾偶发一次测试宿主进程崩溃(`dmesg` 显示 .NET Finalizer 线程在 `libcrypto.so.3` 段错误),隔离复跑 Security 子集 15 轮未再现;暂按运行时/环境偶发观察,跨平台矩阵转阻断后若再出现需取崩溃转储排查。
- **超时故障注入的固定 Sleep(已修)**:`HttpConfigProviderTests.TestLoadAll_Timeout` 的桩用 `Thread.Sleep(3000)` 阻塞处理线程,满载并行下恢复期的第二次 `LoadAll` 偶发仍超时(客户端 300ms);改为 `ManualResetEventSlim` 门控,超时后放行并等待首个请求收尾,恢复期换默认 15s 超时的新客户端。
- **NetServer 启动失败未顺延端口(已修)**:`TestPort.StartAny` 原先只捕 `SocketException`,端口在探测与实际绑定之间被抢占时 `NetServer.Start` 抛普通 `Exception`(Failed to listen to all ports),不会顺延候选端口,导致网络集成用例整套连带失败;改为捕获启动异常并顺延,耗尽候选段后带内层异常抛出。此外单协议被抢占时 `NetServer` 会静默跳过该监听仍返回成功,表现为"服务端已启动但 TCP 全部连接被拒、UDP 正常",启动后还需校验 TCP/UDP 双协议都已监听,缺失则释放实例并顺延。
- **零分配断言的首调分配(已修)**:`SpanWriterTests.TestWriteStruct` 在 net6.0-windows 满载并行下偶发多计 1480 字节(首次泛型 `Write<T>` 实例化的运行时分配);改为测量前先以同实例化预热一次。
|