为了账号安全,请及时绑定邮箱和手机立即绑定

重新执行过程的 Golang 代码覆盖率?

重新执行过程的 Golang 代码覆盖率?

Go
MM们 2022-05-18 15:47:37
为了在特定条件下发现 Linux 命名空间,我的开源 Golang 包lxkns需要重新执行它作为新子进程使用的应用程序,以便能够在 Golang 运行时启动之前切换挂载命名空间。Linux挂载命名空间的工作方式使得在运行时启动 OS 线程后无法从 Golang 应用程序切换它们。这意味着原始进程“P”重新运行自己的副本作为子“C”(reexec包),通过子环境传递一个特殊指示,该指示向子进程发出信号,只运行属于的特定“动作”函数到包含的“lxkns”包(详见下文),而不是正常运行整个应用程序(避免无休止的递归产生子代)。forkchild := exec.Command("/proc/self/exe")forkchild.Start()...forkchild.Wait()目前,我从 VisualStudio Code 调用覆盖测试,它运行:go test -timeout 30s -coverprofile=/tmp/vscode-goXXXXX/go-code-cover github.com/thediveo/lxkns因此,“P”重新执行自身的副本“C”,并告诉它运行一些动作“A”,将一些结果打印到标准输出,然后立即终止。“P”等待“C”的输出,对其进行解析,然后继续其程序流程。模块测试使用 Ginkgo/Gomega 和一个专用TestMain的,以便在测试作为子项重新执行时捕获,以便仅运行请求的“动作”功能。package lxknsimport (    "os"    "testing"    . "github.com/onsi/ginkgo"    . "github.com/onsi/gomega"    "github.com/thediveo/gons/reexec")func TestMain(m *testing.M) {    // Ensure that the registered handler is run in the re-executed child. This    // won't trigger the handler while we're in the parent, because the    // parent's Arg[0] won't match the name of our handler.    reexec.CheckAction()    os.Exit(m.Run())}func TestLinuxKernelNamespaces(t *testing.T) {    RegisterFailHandler(Fail)    RunSpecs(t, "lxkns package")}我还想从重新执行的子进程创建代码覆盖率数据。是否可以从被测程序本身启用代码覆盖,以及如何实现?是否可以将子进程写入的代码覆盖率数据附加到父进程“P”的覆盖率数据中?Golang 运行时是否仅在退出时写入覆盖率数据并覆盖指定的文件,还是追加?(我已经很高兴有一个指向相应运行时源的指针。)注意:在我的测试用例中,切换挂载命名空间不会与在新挂载命名空间中创建覆盖文件冲突。原因是这些测试挂载命名空间是初始挂载命名空间的副本,因此创建新文件也会正常显示在文件系统中。
查看完整描述

1 回答

?
杨魅力

TA贡献1811条经验 获得超6个赞

在@Volker 对我的 QI 发表评论后,我知道我必须接受挑战并直接获取 Gotesting包的源代码。虽然@marco.m 的建议在许多情况下都有帮助,但它无法处理我公认的有点奇怪的用例。testing与我原来的问题相关的机制如下,大大简化:

  • cover.go:实现coverReport()写入覆盖数据文件(ASCII 文本格式);如果文件已经存在(之前运行的旧版本),那么它将首先被截断。请注意,它coverReport()有将一些“统计”信息打印到 os.Stdout 的烦人习惯。

  • 测试.go

    • 获取 CLI 参数-test.coverprofile=-test.outputdir=from os.Args(通过 flags 包)。如果指定的话,如果还实现toOutputDir(path)了哪些地方覆盖配置文件-test.outputdir

    • 但是什么时候coverReport()被调用?简单地说,在结尾testing.M.Run()

现在有了这些知识,一个疯狂的解决方案开始出现,有点“变坏”;)

  • 包装testing.M一个特殊的启用重新执行的版本reexec.testing.M:它检测它是否在启用覆盖的情况下运行:

    • 如果它是“父”进程 P,那么它正常运行测试,然后它从重新执行的子进程 C 中收集覆盖率配置文件数据文件并将它们合并到 P 的覆盖率配置文件数据文件中。

    • 而在 P 中,当即将重新执行新的子 C 时,会为子 C 分配一个新的专用覆盖配置文件数据文件名。然后 C 通过其“个人” -test.coverprofile=CLI arg 获取文件名。

    • 在 C 中,我们运行所需的动作函数。接下来,我们需要运行一个空的测试集以触发为 C 编写覆盖率配置文件数据。为此,P 中的重新执行函数添加了test.run=一个非常特殊的“ Bielefeld测试模式”,这很可能会导致空结果。请记住,P 将——在它运行完所有测试之后——拾取单独的 C 覆盖率配置文件数据文件并将它们合并到 P 中。

  • 如果未启用覆盖分析,则无需采取任何特殊操作。

这个解决方案的缺点是它依赖于 Go 的testing一些关于如何以及何时编写代码覆盖率报告的非保证行为。但由于 Linux 内核命名空间发现包已经比 Docker 的 libnetwork 更难推动 Go,这只是一个超出边缘的量子。

对于测试开发人员来说,整个辣酱玉米饼馅都隐藏在“增强的”rxtst.M包装器中。

import (

    "testing"

    rxtst "github.com/thediveo/gons/reexec/testing"

)


func TestMain(m *testing.M) {

    // Ensure that the registered handler is run in the re-executed child.

    // This won't trigger the handler while we're in the parent. We're using

    // gons' very special coverage profiling support for re-execution.

    mm := &rxtst.M{M: m}

    os.Exit(mm.Run())

}


运行具有覆盖率的整个lxkns测试套件,最好使用go-acc(进行准确的代码覆盖率计算),然后在下面的屏幕截图中显示该函数discoverNsfsBindMounts()运行了一次 (1)。这个函数不是从 P 中的任何地方直接调用的。相反,这个函数被注册,然后在重新执行的子 C 中运行。以前,没有报告代码覆盖discoverNsfsBindMounts(),但现在在包github.com/thediveo的帮助下C 的/gons/reexec/testing代码覆盖率透明地合并到 P 的代码覆盖率中。

//img1.sycdn.imooc.com//6284a4bc000105e608330274.jpg

查看完整回答
反对 回复 2022-05-18
  • 1 回答
  • 0 关注
  • 156 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号