re-executing 进程的 Golang 代码覆盖率?
Golang code coverage for re-executing process?
为了在某些条件下发现 Linux 命名空间,我的开源 Golang 包 lxkns needs to re-execute the application it is used in as a new child process in order to be able to switch mount namespaces before the Golang runtime spins up. The way Linux mount namespaces 工作使得在 运行 时间结束后无法从 Golang 应用程序切换它们 OS 个线程。
这意味着原始进程"P"re-runs自身的副本作为child"C"(reexec包),传递特殊指示通过 child 的环境向 child 发出信号,仅 运行 属于包含的 "lxkns" 包的特定 "action" 函数(详情见下文) ,而不是 运行 正常运行整个应用程序(避免无休止的递归生成 children)。
forkchild := exec.Command("/proc/self/exe")
forkchild.Start()
...
forkchild.Wait()
目前,我从 VisualStudio Code 调用覆盖率测试,运行s:
go test -timeout 30s -coverprofile=/tmp/vscode-goXXXXX/go-code-cover github.com/thediveo/lxkns
因此,"P" re-executes 自身的副本 "C",并告诉它 运行 一些操作 "A",将一些结果打印到标准输出,然后立即终止。 "P" 等待 "C" 的输出,解析它,然后继续其程序流程。
模块测试使用 Ginkgo/Gomega 和专用的 TestMain
以便在测试获得 re-executed 时捕获 child 以便 运行仅请求的 "action" 函数。
package lxkns
import (
"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")
}
我还想从 re-executed child 过程创建代码覆盖率数据。
- 是否可以从被测程序本身启用代码覆盖率,如何实现?
- 是否可以将 child 编写的代码覆盖率数据附加到 parent 进程的覆盖率数据 "P"?
- Golang 运行time 是只在退出时写入覆盖率数据并覆盖指定的文件,还是追加? (如果有指向相应 运行 时间源的指针,我会很高兴。)
注意:切换挂载命名空间不会与在我的测试用例中的新挂载命名空间中创建覆盖文件发生冲突。原因是这些测试挂载命名空间是初始挂载命名空间的副本,因此创建新文件也会正常显示在文件系统中。
@Volker 对我的问题发表评论后,我知道我必须接受挑战并直接获取 Go 的 testing
包的源代码。虽然@marco.m 的建议在很多情况下都有帮助,但它无法处理我承认有点奇怪的用例。 testing
与我的原始问题相关的机制如下,已大大简化:
- cover.go:实现
coverReport()
写入覆盖率数据文件(ASCII文本格式);如果文件已经存在(之前 运行 的过时版本),那么它将首先被 t运行 处理。请注意 coverReport()
有打印一些“统计”信息到 os.Stdout 的烦人习惯。
- testing.go:
- 从
os.Args
获取 CLI 参数 -test.coverprofile=
和 -test.outputdir=
(通过 flags 包)。如果还实现 toOutputDir(path)
,如果指定,它将封面配置文件放在 -test.outputdir
中。
- 但是
coverReport()
什么时候被调用?简单的说,在testing.M.Run()
. 的结尾
现在有了这些知识,一个疯狂的解决方案开始出现,有点像 "Go-ing Bad" ;)
- 将
testing.M
包装在一个特殊的启用重新执行的版本中reexec.testing.M
:它检测是否启用了运行覆盖:
- 如果是"parent"进程P,则运行正常进行测试,然后从重新执行的子进程C中收集覆盖率分析数据文件,并将它们合并到P中覆盖率概况数据文件。
- 在 P 中,当即将重新执行一个新的子 C 时,为子 C 分配了一个新的专用覆盖率配置文件数据文件名。C 然后通过其 "personal"
-test.coverprofile=
CLI 参数。
- 在 C 中,我们 运行 需要的动作函数。接下来,我们需要 运行 一个空的测试集,以便触发写入 C 的覆盖率配置文件数据。为此,P 中的重新执行函数添加了一个
test.run=
非常特殊的“Bielefeld 测试模式”,这很可能会导致空结果。请记住,P 将——在它完成 运行 所有测试之后——选取单独的 C 覆盖率配置文件数据文件并将它们合并到 P 中。
- 如果未启用覆盖分析,则无需采取任何特殊操作。
此解决方案的缺点在于,它依赖于 Go testing
在编写代码覆盖率报告的方式和时间方面的一些不确定行为。但是由于 Linux-内核命名空间发现包已经推动 Go 可能比 Docker 的 libnetwork 更难,这只是一个量子更远的边缘。
对于测试开发人员来说,整个辣酱玉米饼馅都隐藏在 "enhanced" 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
(go accurate code coverage calculation), then shows in the screenshot below that the function discoverNsfsBindMounts()
was run once (1). This function isn't directly called from anywhere in P. Instead, this function is registered and then run in a re-executed child C. Previously, no code coverage was reported for discoverNsfsBindMounts()
, but now with the help of package github.com/thediveo/gons/reexec/testing C 的代码覆盖率透明地合并到 P 的代码覆盖率中。
为了在某些条件下发现 Linux 命名空间,我的开源 Golang 包 lxkns needs to re-execute the application it is used in as a new child process in order to be able to switch mount namespaces before the Golang runtime spins up. The way Linux mount namespaces 工作使得在 运行 时间结束后无法从 Golang 应用程序切换它们 OS 个线程。
这意味着原始进程"P"re-runs自身的副本作为child"C"(reexec包),传递特殊指示通过 child 的环境向 child 发出信号,仅 运行 属于包含的 "lxkns" 包的特定 "action" 函数(详情见下文) ,而不是 运行 正常运行整个应用程序(避免无休止的递归生成 children)。
forkchild := exec.Command("/proc/self/exe")
forkchild.Start()
...
forkchild.Wait()
目前,我从 VisualStudio Code 调用覆盖率测试,运行s:
go test -timeout 30s -coverprofile=/tmp/vscode-goXXXXX/go-code-cover github.com/thediveo/lxkns
因此,"P" re-executes 自身的副本 "C",并告诉它 运行 一些操作 "A",将一些结果打印到标准输出,然后立即终止。 "P" 等待 "C" 的输出,解析它,然后继续其程序流程。
模块测试使用 Ginkgo/Gomega 和专用的 TestMain
以便在测试获得 re-executed 时捕获 child 以便 运行仅请求的 "action" 函数。
package lxkns
import (
"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")
}
我还想从 re-executed child 过程创建代码覆盖率数据。
- 是否可以从被测程序本身启用代码覆盖率,如何实现?
- 是否可以将 child 编写的代码覆盖率数据附加到 parent 进程的覆盖率数据 "P"?
- Golang 运行time 是只在退出时写入覆盖率数据并覆盖指定的文件,还是追加? (如果有指向相应 运行 时间源的指针,我会很高兴。)
注意:切换挂载命名空间不会与在我的测试用例中的新挂载命名空间中创建覆盖文件发生冲突。原因是这些测试挂载命名空间是初始挂载命名空间的副本,因此创建新文件也会正常显示在文件系统中。
@Volker 对我的问题发表评论后,我知道我必须接受挑战并直接获取 Go 的 testing
包的源代码。虽然@marco.m 的建议在很多情况下都有帮助,但它无法处理我承认有点奇怪的用例。 testing
与我的原始问题相关的机制如下,已大大简化:
- cover.go:实现
coverReport()
写入覆盖率数据文件(ASCII文本格式);如果文件已经存在(之前 运行 的过时版本),那么它将首先被 t运行 处理。请注意coverReport()
有打印一些“统计”信息到 os.Stdout 的烦人习惯。 - testing.go:
- 从
os.Args
获取 CLI 参数-test.coverprofile=
和-test.outputdir=
(通过 flags 包)。如果还实现toOutputDir(path)
,如果指定,它将封面配置文件放在-test.outputdir
中。 - 但是
coverReport()
什么时候被调用?简单的说,在testing.M.Run()
. 的结尾
- 从
现在有了这些知识,一个疯狂的解决方案开始出现,有点像 "Go-ing Bad" ;)
- 将
testing.M
包装在一个特殊的启用重新执行的版本中reexec.testing.M
:它检测是否启用了运行覆盖:- 如果是"parent"进程P,则运行正常进行测试,然后从重新执行的子进程C中收集覆盖率分析数据文件,并将它们合并到P中覆盖率概况数据文件。
- 在 P 中,当即将重新执行一个新的子 C 时,为子 C 分配了一个新的专用覆盖率配置文件数据文件名。C 然后通过其 "personal"
-test.coverprofile=
CLI 参数。 - 在 C 中,我们 运行 需要的动作函数。接下来,我们需要 运行 一个空的测试集,以便触发写入 C 的覆盖率配置文件数据。为此,P 中的重新执行函数添加了一个
test.run=
非常特殊的“Bielefeld 测试模式”,这很可能会导致空结果。请记住,P 将——在它完成 运行 所有测试之后——选取单独的 C 覆盖率配置文件数据文件并将它们合并到 P 中。
- 如果未启用覆盖分析,则无需采取任何特殊操作。
此解决方案的缺点在于,它依赖于 Go testing
在编写代码覆盖率报告的方式和时间方面的一些不确定行为。但是由于 Linux-内核命名空间发现包已经推动 Go 可能比 Docker 的 libnetwork 更难,这只是一个量子更远的边缘。
对于测试开发人员来说,整个辣酱玉米饼馅都隐藏在 "enhanced" 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
(go accurate code coverage calculation), then shows in the screenshot below that the function discoverNsfsBindMounts()
was run once (1). This function isn't directly called from anywhere in P. Instead, this function is registered and then run in a re-executed child C. Previously, no code coverage was reported for discoverNsfsBindMounts()
, but now with the help of package github.com/thediveo/gons/reexec/testing C 的代码覆盖率透明地合并到 P 的代码覆盖率中。