Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Exploring the Implementation of “t.Run”, “t.Parallel”, and “t.Cleanup”

Akari
April 27, 2024

Exploring the Implementation of “t.Run”, “t.Parallel”, and “t.Cleanup”

Akari

April 27, 2024
Tweet

More Decks by Akari

Other Decks in Programming

Transcript

  1. Exploring the Implementation of “t.Run”, “t.Parallel”, and “t.Cleanup” Kyoto.go #50

    オフラインLT会 28 April, 2024 Keisuke Akari ( @k-akari @akarin0519)
  2. 1. What Will You Make Comments on This PR? func

    TestA(t *testing.T) { setup(t) defer cleanup(t) cases := map[string]struct { // }{ "Case 1": {}, } for name, tt := range cases { t.Run(name, func(t *testing.T) { t.Parallel() // }) } }
  3. func TestA(t *testing.T) { setup(t) defer cleanup(t) cases := map[string]struct

    { // }{ "Case 1": {}, } for name, tt := range cases { t.Run(name, func(t *testing.T) { t.Parallel() // }) } } 2. You Will Leave Comments as Follows See Test parallelization in Go: Understanding the t.Parallel() method for detailed explanation.
  4. 4. Let’s Understand the “T” Type in “testing” Package -

    The “T” struct can have a parent of the same type and multiple children. package testing type T struct { common isEnvSet bool context *testContext } type common struct { // parent *common sub []*T // Queue of subtests to be run in parallel. // } var t *testing.T var t *testing.T var t *testing.T t.parent t.sub
  5. 5. When Does a “T”-typed Value Register Its Parent? -

    “t.Run” creates a new child “T”-typed value using its receiver as the parent. func (t *T) Run(name string, f func(t *T)) bool { // t = &T{ common: common{ // parent: &t.common, level: t.level + 1, }, context: t.context, } // go tRunner(t, f) // } var t *testing.T var t *testing.T t.parent func tRunner(t *T, fn func(t *T)) { // fn(t) // }
  6. 6. How Are Child Tests Registered? - “t.Parallel” informs the

    parent that its receiver is a child. func (t *T) Parallel() { // Add to the list of tests to be released by the parent. t.parent.sub = append(t.parent.sub, t) // } var t *testing.T var t *testing.T t.sub
  7. 7. Exploring the Logic Behind the “t.Cleanup” - “t.Cleanup” simply

    adds the received function to the “cleanups” field. type common struct { // optional functions to be called at the end of the test cleanups []func() // } func (c *common) Cleanup(f func()) { // fn := func() { // f() } // c.cleanups = append(c.cleanups, fn) }
  8. 8. Exploring How Go Waits for All Parallel Subtests to

    Complete Before Executing Functions Registered with “t.Cleanup()"
  9. type T struct { common // } type common struct

    { // parent *common sub []*T signal chan bool } func (t *T) Run(name string, f func(t *T)) bool { // t = &T{ common: common{ // parent: &t.common, level: t.level + 1, }, context: t.context, } // go tRunner(t, f) // } type panicHandling int const ( normalPanic panicHandling = iota recoverAndReturnPanic ) func tRunner(t *T, fn func(t *T)) { // defer func() { // signal := true // defer func() { // t.signal <- signal }() // if len(t.sub) > 0 { // // Wait for subtests to complete. for _, sub := range t.sub { <-sub.signal } // err := t.runCleanup(recoverAndReturnPanic) // } // }() defer func() { if len(t.sub) == 0 { t.runCleanup(normalPanic) } }() // fn(t) // } (1) (2) (3) func (c *common) runCleanup( ph panicHandling ) (panicVal any) { // for { var cleanup func() c.mu.Lock() if len(c.cleanups) > 0 { last := len(c.cleanups) - 1 cleanup = c.cleanups[last] c.cleanups = c.cleanups[:last] } c.mu.Unlock() if cleanup == nil { return nil } cleanup() } }
  10. 9. Summary 1. Understanding the “T” Struct: a. The T

    struct supports a hierarchical testing structure with a single parent of the same type and multiple child tests that are executed in parallel. 2. Building Hierarchical Testing with “t.Run” and “t.Parallel”: a. “t.Run” creates a child “T”-typed value from its receiver and executes a function, which is received as a second argument, with this child “T”-typed value. b. “t.Parallel” registers its receiver as a child to the receiver's parent. 3. Execution Order and Cleanup Process: a. Just before “t.Run” finishes, it sends a signal to its receiver to notify the end of the test. The parent “T”-typed value waits for all subtests to receive this end signal, then executes all post-processing tasks registered by “t.Cleanup”.