作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢!


背景

为了更方便地在云环境中测试和调试 CSharp 的后端服务,我制作了 CSharp Debug Container All-in-one
请看:https://github.com/ahfuzhang/CSharpDbgContainer

原因是多年积累的祖传代码耦合了数据库和 grpc,很难方便的拆出来做单元测试。
因此就需要把服务先启动起来,然后在线测试。

我做了如下事情来支持在线测试:

  • 制作好了 CSharp Debug Container All-in-one 镜像
    • 镜像内提供 DebugAdmin 管理进程
    • DebugAdmin 启动了 dotnet-coverage; 然后 dotnet-coverage 又启动了目标进程
  • 把服务器依赖的其他库,copy 到同一个代码仓库,然后把 csproj 文件中的引用 nuget 仓库,改为引用某个源码目录
    • 这样:相当于绝大多数的源码信息都能够被编译到符号文件中
  • 把整个服务器及其源码目录打包到镜像,使用 CSharp Debug Container All-in-one 作为基础镜像
  • 云环境中,修改 deployment 的启动参数,通过 DebugAdmin 来启动好支持在线代码覆盖率的运行环境

请参考前一篇文章,介绍了这种 在线代码覆盖率 的方法:

上面看代码覆盖率的方法在实际部署中有如下困难:

  • 所有想看到覆盖情况的代码,都要 copy 到同一目录下;且入口项目的引用信息还得修改。
  • 发布二进制的时候,必须把所有源代码 copy 到镜像之中

本文档重点介绍如何解决上面两个问题。

核心思路

  • 每个仓库都把源码编译到 pdb 文件之中
  • 把 pdb 上传到 NuGet 仓库中
  • 入口项目在编译时,把所有依赖的项目中的 pdb 文件拷贝到产物中
  • 打包镜像的时候,打包二进制、dll 及其 pdb 文件
  • 运行期间采集代码覆盖率后,从 pdb 文件中还原出源码;然后根据覆盖率文件和源码,生成详细到行的覆盖率报表

Cooking,开搞

1. 打包时编译和上传 pdb

  • 只需要在命令行中加上选项就行了
VER=0.2.4

# 注意:分号 (;) 要写成 %3b
pack:
	dotnet pack -c Release -p:PackageVersion=$(VER) \
	--no-restore \
	-p:DebugType=portable \
	-p:DebugSymbols=true \
	-p:IncludeSymbols=false \
	-p:EmbedAllSources=true \
	-p:AllowedOutputExtensionsInPackageBuildOutputFolder=".dll%3b.pdb"

# make push KEY=$(cat app_key.txt)
push:
	dotnet nuget push bin/Release/QiWa.Common.$(VER).nupkg --skip-duplicate \
		--api-key $(KEY) \
		--source https://api.nuget.org/v3/index.json

可以参考我开源项目中的文件:https://github.com/ahfuzhang/QiWa.Common/blob/main/Makefile

关键参数 -p:EmbedAllSources=true, 把所有源码编译到 pdb 中

需要特别注意: 分号对于 MS Build 而言是个特殊字符,因此 -p:AllowedOutputExtensionsInPackageBuildOutputFolder=".dll%3b.pdb" 这个位置中的分号要写成 %3b

  • 也可以在 .csproj 文件中增加选项:
  <PropertyGroup>
    <!-- 默认 pack 只收 .dll/.xml 等,pdb 要显式加进来 -->
    <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
  </PropertyGroup>

方法二

也可以单独把 pdb 文件作为 snupkg 独立上传,这样做的好处是不影响 dotnet restore 时候下载的 NuGet 库。
具体方法如下:

VER=0.2.4

# 注意:分号 (;) 要写成 %3b
pack:
	dotnet pack -c Release -p:PackageVersion=$(VER) \
	--no-restore \
	-p:DebugType=portable \
	-p:DebugSymbols=true \
	-p:IncludeSymbols=false \
	-p:EmbedAllSources=true \
	-p:SymbolPackageFormat=snupkg   # 打两个包:.nupkg 和 .snupkg

push:
	dotnet nuget push bin/Release/QiWa.Common.$(VER).nupkg --skip-duplicate \
		--api-key $(KEY) \
		--source https://api.nuget.org/v3/index.json
 	dotnet nuget push bin/Release/QiWa.Common.$(VER).snupkg --skip-duplicate \
 		--api-key $(KEY) \
 		--source https://api.nuget.org/v3/index.json  # 单独上传 snupkg 包

2. 项目中引用库时,要求带上 pdb 文件

  • 在 .csproj 文件中配置
<PropertyGroup>
    <CopyDebugSymbolFilesFromPackages>true</CopyDebugSymbolFilesFromPackages>
</PropertyGroup>
  • 也可以在命令行提供:
dotnet publish \
  -c Release \
  -p:CopyDebugSymbolFilesFromPackages=true

方法二

如果是通过 snupkg 上传 pdb,那么依赖的 dll 是不包含 pdb 文件的,需要额外的手动下载、解压、并拷贝到产物中:

wget -O QiWa.Common.1.2.3.snupkg \
  "https://www.nuget.org/api/v2/symbolpackage/QiWa.Common/1.2.3"
unzip -l QiWa.Common.1.2.3.snupkg  # .snupkg 本质上是 ZIP

3. 打包镜像

只需要把包含 pdb 的产物打包到镜像中就可以了。例如目录为 /wwwroot/
基础镜像为: docker.io/ahfuzhang/csharp-dbg-all-in-one:dotnet10

4. 启动参数

在云环境中启动服务器进程时,采用如下启动参数:

/usr/bin/DebugAdmin \
		  -admin.port=8081 \
		  -log.stdout.output \
		  -with.coverage \
		  -coverage.xml.settings=/src/build/code.coverage.settings.xml \
		  -coverage.exclude.re="Dapper" \
		  -coverage.exclude.re="Pipelines.Sockets.Unofficial" \
		  -coverage.exclude.re="StackExchange.Redis" \
		  -coverage.exclude.re="MySqlConnector" \
		  -coverage.exclude.re="Microsoft.IO.RecyclableMemoryStream" \
		  -coverage.exclude.re=".*Grpc\\.Protobuf" \
		  -coverage.source.from.pdb \
		  -- \
		  PG.Game.CandyBonanza.dll
  • 实现原理上:先启动管理程序,管理程序启动 dotnet-coverage, 然后 coverage 再启动服务器进程 —— 最终实现实时采集代码覆盖率。

image

各个参数的功能说明如下:

  • -admin.port=8081: 指定管理端口,用浏览器进入管理端口,会看到如下界面:

image

  • -log.stdout.output: 把服务器进程的 stdout 再输出为 DebugAdmin 进程的 stdout
  • -with.coverage: 以采集代码覆盖率的模式启动。此参数一定要加上。
  • coverage.xml.settings=config.xml: 指定一个 dotnet-coverage 命令的配置文件。配置文件中可以声明包含哪些 dll 以及排除哪些 dll。
  • -coverage.exclude.re="$regexp": 配置一个排除某些 class 的正则表达式。
  • -coverage.source.from.pdb: 当未把源码上传到镜像中时,此参数会触发:自动扫描所有 pdb 文件,然后从 pdb 文件中 dump 出 cs 文件,最后生成精确到代码行的 html report.

更多的功能介绍请移步:https://github.com/ahfuzhang/CSharpDbgContainer

总结

  • 在线代码覆盖率,对于难以做单元测试的 祖传代码 项目特别有用:
    • 以代码覆盖率为反馈,验证测试用例的有效性
    • 便于跟踪复杂的业务流程
  • 需要注意:
    • release 项目,要额外增加一个脚本删除 pdb 文件后再发布,否则可能有源码泄露的风险
  • 可以告诉 AI 做这样一个循环:
    • 清空代码覆盖率数据
    • 发出请求
    • 生成代码覆盖率数据
    • 分析代码覆盖率报告
    • 生成有效的测试用例
    • 回到第一步
    • 我的下一篇文章会仔细介绍基于这个 Debug 镜像实现的自动 AI Test 的项目

Have Fun.
😃