代码之家  ›  专栏  ›  技术社区  ›  xgord

golang中如何处理平行子测验的父测验拆卸

go
  •  1
  • xgord  · 技术社区  · 7 年前

    如果我有一个带有设置和拆卸逻辑的父测试,我如何并行地运行其中的子测试,而不会遇到带有拆卸逻辑的竞争条件?

    func TestFoo(t *testing.T) {
        // setup logic
        t.Run("a", func(t *testing.T) {
            t.Parallel()
            // test code
        })
        // teardown logic
    }
    

    举一个人为的例子:假设测试需要创建一个tmp文件,该文件将被所有子测试使用,并在测试结束时将其删除。

    t.Parallel() ,因为这是我最终想要的。但是我的问题和下面的输出是相同的,即使父级不调用 t、 平行() .

    如果我按顺序运行子测试,它们不会通过任何问题:

    package main
    
    import (
        "fmt"
        "io/ioutil"
        "os"
        "testing"
    )
    
    func setup(t *testing.T) (tmpFile string) {
        f, err := ioutil.TempFile("/tmp", "subtests")
        if err != nil {
            t.Fatalf("could not setup tmp file: %+v", err)
        }
        f.Close()
        return f.Name()
    }
    
    var ncase = 2
    
    func TestSeqSubtest(t *testing.T) {
        t.Parallel()
    
        // setup test variables
        fname := setup(t)
    
        // cleanup test variables
        defer func() {
            os.Remove(fname)
        }()
    
        for i := 0; i < ncase; i++ {
            t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) {
                if _, err := os.Stat(fname); os.IsNotExist(err) {
                    t.Fatalf("file was removed before subtest finished")
                }
            })
        }
    }  
    

    输出:

    $ go test subtests  
    ok      subtests        0.001s
    

    但是,如果我并行运行子测试,那么父测试的拆卸逻辑最终会被调用 之前

    "Using Subtests and Sub-benchmarks" go博客说:

    如果测试函数调用 与顺序测试同时运行,其执行是 他回来了。

    func TestParallelSubtest(t *testing.T) {
        t.Parallel()
    
        // setup test variables
        fname := setup(t)
    
        // cleanup test variables
        defer func() {
            os.Remove(fname)
        }()
    
        for i := 0; i < ncase; i++ {
            t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) {
                t.Parallel() // the change that breaks things
                if _, err := os.Stat(fname); os.IsNotExist(err) {
                    t.Fatalf("file was removed before subtest finished")
                }
            })
        }
    }
    

    $ go test subtests  
    --- FAIL: TestParallelSubtest (0.00s)
        --- FAIL: TestParallelSubtest/test_0 (0.00s)
            main_test.go:58: file was removed before subtest finished
        --- FAIL: TestParallelSubtest/test_1 (0.00s)
            main_test.go:58: file was removed before subtest finished
    FAIL
    FAIL    subtests        0.001s
    

    带WaitGroup的并行子测验

    如上所述,并行子测试在其父测试完成之前不会执行,这意味着尝试使用 sync.WaitGroup 导致死锁:

    func TestWaitGroupParallelSubtest(t *testing.T) {
        t.Parallel()
        var wg sync.WaitGroup
    
        // setup test variables
        fname := setup(t)
    
        // cleanup test variables
        defer func() {
            os.Remove(fname)
        }()
    
        for i := 0; i < ncase; i++ {
            t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) {
                wg.Add(1)
                defer wg.Done()
                t.Parallel()
                if _, err := os.Stat(fname); os.IsNotExist(err) {
                    t.Fatalf("file was removed before subtest finished")
                }
            })
        }
        wg.Wait() // causes deadlock
    }
    

    输出:

    $ go test subtests  
    --- FAIL: TestParallelSubtest (0.00s)
        --- FAIL: TestParallelSubtest/test_0 (0.00s)
            main_test.go:58: file was removed before subtest finished
        --- FAIL: TestParallelSubtest/test_1 (0.00s)
            main_test.go:58: file was removed before subtest finished
    fatal error: all goroutines are asleep - deadlock!
    
    goroutine 1 [chan receive]:
    testing.tRunner.func1(0xc00009a000)
            /path/to/golang1.1.11/src/testing/testing.go:803 +0x1f3
    testing.tRunner(0xc00009a000, 0xc00005fe08)
            /path/to/golang1.1.11/src/testing/testing.go:831 +0xc9
    testing.runTests(0xc00000a0a0, 0x6211c0, 0x3, 0x3, 0x40b36f)
            /path/to/golang1.1.11/src/testing/testing.go:1117 +0x2aa
    testing.(*M).Run(0xc000096000, 0x0)
            /path/to/golang1.1.11/src/testing/testing.go:1034 +0x165
    main.main()
            _testmain.go:46 +0x13d
    
    goroutine 7 [semacquire]:
    sync.runtime_Semacquire(0xc0000a2008)
            /path/to/golang1.1.11/src/runtime/sema.go:56 +0x39
    sync.(*WaitGroup).Wait(0xc0000a2000)
            /path/to/golang1.1.11/src/sync/waitgroup.go:130 +0x64
    subtests.TestWaitGroupParallelSubtest(0xc00009a300)
            /path/to/go_code/src/subtests/main_test.go:91 +0x2b5
    testing.tRunner(0xc00009a300, 0x540f38)
            /path/to/golang1.1.11/src/testing/testing.go:827 +0xbf
    created by testing.(*T).Run
            /path/to/golang1.1.11/src/testing/testing.go:878 +0x353
    
    goroutine 8 [chan receive]:
    testing.runTests.func1.1(0xc00009a000)
            /path/to/golang1.1.11/src/testing/testing.go:1124 +0x3b
    created by testing.runTests.func1
            /path/to/golang1.1.11/src/testing/testing.go:1124 +0xac
    
    goroutine 17 [chan receive]:
    testing.(*T).Parallel(0xc0000f6000)
            /path/to/golang1.1.11/src/testing/testing.go:732 +0x1fa
    subtests.TestWaitGroupParallelSubtest.func2(0xc0000f6000)
            /path/to/go_code/src/subtests/main_test.go:85 +0x86
    testing.tRunner(0xc0000f6000, 0xc0000d6000)
            /path/to/golang1.1.11/src/testing/testing.go:827 +0xbf
    created by testing.(*T).Run
            /path/to/golang1.1.11/src/testing/testing.go:878 +0x353
    
    goroutine 18 [chan receive]:
    testing.(*T).Parallel(0xc0000f6100)
            /path/to/golang1.1.11/src/testing/testing.go:732 +0x1fa
    subtests.TestWaitGroupParallelSubtest.func2(0xc0000f6100)
            /path/to/go_code/src/subtests/main_test.go:85 +0x86
    testing.tRunner(0xc0000f6100, 0xc0000d6040)
            /path/to/golang1.1.11/src/testing/testing.go:827 +0xbf
    created by testing.(*T).Run
            /path/to/golang1.1.11/src/testing/testing.go:878 +0x353
    FAIL    subtests        0.003s
    

    总结

    那么,我如何在调用的父测试中拥有一个teardown方法呢 之后 并行子测试运行吗?

    1 回复  |  直到 7 年前
        1
  •  7
  •   Community Mohan Dere    6 年前

    Go Blog on subtests 提到了如何做到这一点:

    func TestParallelSubtest(t *testing.T) {
        // setup test variables
        fname := setup(t)
    
        t.Run("group", func(t *testing.T) {
            for i := 0; i < ncase; i++ {
                t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) {
                    t.Parallel()
                    if _, err := os.Stat(fname); os.IsNotExist(err) {
                        t.Fatalf("file was removed before subtest finished")
                    }
                })
            }
        })
        
        os.Remove(fname)
    }
    

    Control of Parallelism :

    每个测试都与一个测试功能相关联。如果测试函数在其实例上调用并行方法,则该测试称为并行测试 testing.T

    一个测试块,直到它的测试函数返回并且它的所有子测试都完成。这意味着由顺序测试运行的并行测试将在运行任何其他连续顺序测试之前完成。

    您的问题的具体解决方案可以在 Cleaning up after a group of parallel tests 节。

        2
  •  2
  •   dolmen    5 年前

    从Go 1.14开始, testing.T testing.B Cleanup 方法,该方法允许注册拆卸回调。

    t.Cleanup(func() {
        os.Remove(fname)
    })