Commit 2330ae8c authored by Marcel van Lohuizen's avatar Marcel van Lohuizen

testing: finish implementation of subtests

API not exposed yet.

Change-Id: Iaba0adc0fa1ae8075e6b56796f99ee8db9177a78
Reviewed-on: https://go-review.googlesource.com/18896Reviewed-by: 's avatarRuss Cox <rsc@golang.org>
parent 1857bfca
......@@ -5,6 +5,8 @@
package testing
import (
"io/ioutil"
"sync/atomic"
"time"
)
......@@ -104,6 +106,229 @@ func TestTestContext(t *T) {
}
}
// TODO: remove this stub when API is exposed
func (t *T) Run(name string, f func(t *T)) bool { return t.run(name, f) }
func TestTRun(t *T) {
realTest := t
testCases := []struct {
desc string
ok bool
maxPar int
f func(*T)
}{{
desc: "failnow skips future sequential and parallel tests at same level",
ok: false,
maxPar: 1,
f: func(t *T) {
ranSeq := false
ranPar := false
t.Run("", func(t *T) {
t.Run("par", func(t *T) {
t.Parallel()
ranPar = true
})
t.Run("seq", func(t *T) {
ranSeq = true
})
t.FailNow()
t.Run("seq", func(t *T) {
realTest.Error("test must be skipped")
})
t.Run("par", func(t *T) {
t.Parallel()
realTest.Error("test must be skipped.")
})
})
if !ranPar {
realTest.Error("parallel test was not run")
}
if !ranSeq {
realTest.Error("sequential test was not run")
}
},
}, {
desc: "failure in parallel test propagates upwards",
ok: false,
maxPar: 1,
f: func(t *T) {
t.Run("", func(t *T) {
t.Parallel()
t.Run("par", func(t *T) {
t.Parallel()
t.Fail()
})
})
},
}, {
desc: "use Run to locally synchronize parallelism",
ok: true,
maxPar: 1,
f: func(t *T) {
var count uint32
t.Run("waitGroup", func(t *T) {
for i := 0; i < 4; i++ {
t.Run("par", func(t *T) {
t.Parallel()
atomic.AddUint32(&count, 1)
})
}
})
if count != 4 {
t.Errorf("count was %d; want 4", count)
}
},
}, {
desc: "run no more than *parallel tests concurrently",
ok: true,
maxPar: 4,
f: func(t *T) {
max := 0
in := make(chan int)
out := make(chan int)
ctx := t.context
t.Run("wait", func(t *T) {
t.Run("controller", func(t *T) {
// Verify sequential tests don't skew counts.
t.Run("seq1", func(t *T) {})
t.Run("seq2", func(t *T) {})
t.Run("seq3", func(t *T) {})
t.Parallel()
for i := 0; i < 80; i++ {
ctx.mu.Lock()
if ctx.running > max {
max = ctx.running
}
ctx.mu.Unlock()
<-in
// force a minimum to avoid a race, although it works
// without it.
if i >= ctx.maxParallel-2 { // max - this - 1
out <- i
}
}
close(out)
})
// Ensure we don't exceed the maximum even with nested parallelism.
for i := 0; i < 2; i++ {
t.Run("", func(t *T) {
t.Parallel()
for j := 0; j < 40; j++ {
t.Run("", func(t *T) {
t.Run("seq1", func(t *T) {})
t.Run("seq2", func(t *T) {})
t.Parallel()
in <- j
<-out
})
}
})
}
})
if max != ctx.maxParallel {
realTest.Errorf("max: got %d; want: %d", max, ctx.maxParallel)
}
},
}, {
desc: "alternate sequential and parallel",
// Sequential tests should partake in the counting of running threads.
// Otherwise, if one runs parallel subtests in sequential tests that are
// itself subtests of parallel tests, the counts can get askew.
ok: true,
maxPar: 1,
f: func(t *T) {
t.Run("a", func(t *T) {
t.Parallel()
t.Run("b", func(t *T) {
// Sequential: ensure running count is decremented.
t.Run("c", func(t *T) {
t.Parallel()
})
})
})
},
}, {
desc: "alternate sequential and parallel",
// Sequential tests should partake in the counting of running threads.
// Otherwise, if one runs parallel subtests in sequential tests that are
// itself subtests of parallel tests, the counts can get askew.
ok: true,
maxPar: 2,
f: func(t *T) {
for i := 0; i < 2; i++ {
t.Run("a", func(t *T) {
t.Parallel()
time.Sleep(time.Nanosecond)
for i := 0; i < 2; i++ {
t.Run("b", func(t *T) {
time.Sleep(time.Nanosecond)
for i := 0; i < 2; i++ {
t.Run("c", func(t *T) {
t.Parallel()
time.Sleep(time.Nanosecond)
})
}
})
}
})
}
},
}, {
desc: "stress test",
ok: true,
maxPar: 4,
f: func(t *T) {
t.Parallel()
for i := 0; i < 12; i++ {
t.Run("a", func(t *T) {
t.Parallel()
time.Sleep(time.Nanosecond)
for i := 0; i < 12; i++ {
t.Run("b", func(t *T) {
time.Sleep(time.Nanosecond)
for i := 0; i < 12; i++ {
t.Run("c", func(t *T) {
t.Parallel()
time.Sleep(time.Nanosecond)
t.Run("d1", func(t *T) {})
t.Run("d2", func(t *T) {})
t.Run("d3", func(t *T) {})
t.Run("d4", func(t *T) {})
})
}
})
}
})
}
},
}}
for _, tc := range testCases {
ctx := newTestContext(tc.maxPar)
root := &T{
common: common{
barrier: make(chan bool),
w: ioutil.Discard,
},
context: ctx,
}
ok := root.Run(tc.desc, tc.f)
ctx.release()
if ok != tc.ok {
t.Errorf("%s:ok: got %v; want %v", tc.desc, ok, tc.ok)
}
if ok != !root.Failed() {
t.Errorf("%s:root failed: got %v; want %v", tc.desc, !ok, root.Failed())
}
if ctx.running != 0 || ctx.numWaiting != 0 {
t.Errorf("%s:running and waiting non-zero: got %d and %d", tc.desc, ctx.running, ctx.numWaiting)
}
}
}
// TODO: remove this stub when API is exposed
func (b *B) Run(name string, f func(b *B)) bool { return b.runBench(name, f) }
......
......@@ -273,6 +273,29 @@ func (c *common) flushToParent(format string, args ...interface{}) {
c.output = c.output[:0]
}
type indenter struct {
c *common
}
func (w indenter) Write(b []byte) (n int, err error) {
n = len(b)
for len(b) > 0 {
end := bytes.IndexByte(b, '\n')
if end == -1 {
end = len(b)
} else {
end++
}
// An indent of 4 spaces will neatly align the dashes with the status
// indicator of the parent.
const indent = " "
w.c.output = append(w.c.output, indent...)
w.c.output = append(w.c.output, b[:end]...)
b = b[end:]
}
return
}
// fmtDuration returns a string representing d in the form "87.00s".
func fmtDuration(d time.Duration) string {
return fmt.Sprintf("%.2fs", d.Seconds())
......@@ -542,6 +565,7 @@ func (t *T) run(name string, f func(t *T)) bool {
},
context: t.context,
}
t.w = indenter{&t.common}
if *chatty {
fmt.Printf("=== RUN %s\n", t.name)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment