Ginkgo源码分析

ginkgo cmd

执行ginkgo cmd默认运行的是ginkgo run

入口函数

1func (r *SpecRunner) RunSpecs(args []string, additionalArgs []string) {

主要做两件事,编译和运行suite

编译

使用 go test -c -o 得到.test可执行文件

要理解.test文件中的测试用例(spec)是怎么执行的,就要理解ginkgo是怎么构造specs tree的

构造specs tree

Describe等是container Node,BeforeXXXAfterXXX等是setup Node,It是subject Node。Describe通过var _ = Describe的方式实现在golang源文件顶层执行函数,这会使得在编译时最先执行Describe。

他们底层都是通过pushNode,从外向内一层一层的push Node,然后在RunSpecs入口中的BuildTree构造出如下的specs tree数据结构:

运行

ginkgo

在SUITE_LOOP中循环运行每一个编译出来的.test

1suites[suiteIdx] = internal.RunCompiledSuite(suites[suiteIdx], r.suiteConfig, r.reporterConfig, r.cliConfig, r.goFlagsConfig, additionalArgs)

每个suite都会编译成一个.test,如果有多个test suite,一个一个的编译运行

如果以parallel的模式运行,则启动server

1server, err := parallel_support.NewServer(numProcs, reporters.NewDefaultReporter(reporterConfig, formatter.ColorableStdOut))

生成go test flag,如果运行在parallel模式,启动多个.test进程并发执行测试用例。ginkgo会根据机器的核心数决定启动多少个test进程,每个test进程运行的都是同一组specs,但是test具体运行哪一个spec由ginkgo server决定。

启动goroutine等待子进程退出并卡住等待结果

 1go func() {
 2	cmd.Wait()
 3	exitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()
 4	procResults <- procResult{
 5		passed:               (exitStatus == 0) || (exitStatus == types.GINKGO_FOCUS_EXIT_CODE),
 6		hasProgrammaticFocus: exitStatus == types.GINKGO_FOCUS_EXIT_CODE,
 7	}
 8}()
 9
10// 然后卡住等待子进程结果
11passed := true
12for proc := 1; proc <= cliConfig.ComputedProcs(); proc++ {
13	result := <-procResults
14	passed = passed && result.passed
15	suite.HasProgrammaticFocus = suite.HasProgrammaticFocus || result.hasProgrammaticFocus
16}

.test

.test文件的入口:

1func TestE2E(t *testing.T) {
2	gomega.RegisterFailHandler(ginkgo.Fail)
3	ginkgo.RunSpecs(t, "Sample Suite")
4}

如果运行在parallel模式,则启动client并连接server

1client = parallel_support.NewClient(suiteConfig.ParallelHost)
2if !client.Connect() {
3	client = nil
4	exitIfErr(types.GinkgoErrors.UnreachableParallelHost(suiteConfig.ParallelHost))
5}

Build specs tree

1err := global.Suite.BuildTree()

Run Suite

  1. 把specs随机打散分组

    ordered specs分在一组,普通的spec单独一个是一组,serial specs分为一组

    1groupedSpecIndices, serialGroupedSpecIndices := OrderSpecs(specs, suite.config)
    
  2. 以group为单位runSpecs。具体下一步运行哪一组从server获得下标

    1nextIndex = suite.client.FetchNextCounter
    
  3. 如果是serial group,那么必须当前是#1 process,而且要等到其他process都退出。

    1if suite.config.ParallelProcess == 1 && len(serialGroupedSpecIndices) > 0 {
    2    groupedSpecIndices, serialGroupedSpecIndices, nextIndex = serialGroupedSpecIndices, GroupedSpecIndices{}, MakeIncrementingIndexCounter()
    3    suite.client.BlockUntilNonprimaryProcsHaveFinished()
    4    continue
    5}
    
  4. 开始Run group specs

    每一组里可能多个specs,一个spec一个spc的执行

    runSpec

    1. 判断interruptStatus,来决定要不要skip 这个spec

    2. 根据attempt derocator的定义,尝试attempts次

    3. spec中的nodes分为两批执行,先把spec中的setup Node和subject Node(It)组成为一批nodes依次运行

      1nodes := spec.Nodes.WithType(types.NodeTypeBeforeAll)
      2nodes = append(nodes, spec.Nodes.WithType(types.NodeTypeBeforeEach)...).SortedByAscendingNestingLevel()
      3nodes = append(nodes, spec.Nodes.WithType(types.NodeTypeJustBeforeEach).SortedByAscendingNestingLevel()...)
      4nodes = append(nodes, spec.Nodes.FirstNodeWithType(types.NodeTypeIt))
      
    4. 再把cleanup Node组成一批Nodes依次运行。

      1nodes := spec.Nodes.WithType(types.NodeTypeAfterEach)
      2nodes = append(nodes, spec.Nodes.WithType(types.NodeTypeAfterAll)...).SortedByDescendingNestingLevel()
      3nodes = append(spec.Nodes.WithType(types.NodeTypeJustAfterEach).SortedByDescendingNestingLevel(), nodes...)
      
    5. nodes中的Node一个一个的执行,runNode

      runNode

      1. 判断interruptStatus,来决定Node要不要跳过,但是cleanup、report的Node必须要收到多次信号才会真正跳过。

      2. It中用户定义的closure在底层就是通过goroutine执行:

         1go func() {
         2    finished := false
         3    defer func() {
         4        if e := recover(); e != nil || !finished {
         5            suite.failer.Panic(types.NewCodeLocationWithStackTrace(2), e)
         6        }
         7           
         8        outcomeFromRun, failureFromRun := suite.failer.Drain()
         9        failureFromRun.TimelineLocation = suite.generateTimelineLocation()
        10        outcomeC <- outcomeFromRun
        11        failureC <- failureFromRun
        12    }()
        13           
        14    // It 中定义的closure
        15    node.Body(sc)
        16    finished = true
        17}()
        
      3. 这个goroutine有几个退出条件:

        超时和interrupt的情况下都会再等待gracePeriod,但是如果Node没定义context,则gracePeriod为0

        1if !node.HasContext {
        2    gracePeriod = 0
        3}
        

        如果gracePeriod时间到了Node还没有执行完,那就leak Node,也就是leak goroutine,这可能会导致无法预测的行为。

        interrupt机制:

        这个channel是从interrupt handler复制过来的,有几种interrupt的情况

        • interrupt handler每隔500ms去服务端轮询是否可以Abort。

          什么时候可以Abort呢:如果运行在paralle模式,并且开启了fail-fast,这样有任何一个process失败,都会通知服务端现在可以开始Abort了

          如果可以那么就会触发这个interrupt channel close。

          这个地方的实现是通过轮询来实现的,状态更新会有500ms的延迟。存在两个问题:

          1. 在运行Serial的Node或者cleanup Node时,会先检查一下状态,再决定是否运行。但是可能当时server端已经设置为需要abort了,可是还需要等500ms才能拿到实际状态。这个时候会去运行Node,但是实际上是应该skip的。
          2. cleanup Node应该直接忽略Abort的channel。

          我提交了一个PR https://github.com/onsi/ginkgo/pull/1178 解决了这个问题

        • 收到SIGINT和SIGTERM信号

          1func NewInterruptHandler(client parallel_support.Client, signals ...os.Signal) *InterruptHandler {
          2    if len(signals) == 0 {
          3        signals = []os.Signal{os.Interrupt, syscall.SIGTERM}
          4    }
          

运行AfterSuiteCleanup Node