runtime: move test programs out of source code, coalesce

Now there are just three programs to compile instead of many,
and repeated tests can reuse the compilation result instead of
rebuilding it.

Combined, these changes reduce the time spent testing runtime
during all.bash on my laptop from about 60 to about 30 seconds.
(All.bash itself runs in 5½ minutes.)

For #10571.

Change-Id: Ie2c1798b847f1a635a860d11dcdab14375319ae9
Reviewed-on: https://go-review.googlesource.com/18085
Reviewed-by: Austin Clements <austin@google.com>
Run-TryBot: Austin Clements <austin@google.com>
This commit is contained in:
Russ Cox 2015-12-21 10:29:21 -05:00
parent a699320512
commit 8d5ff2e182
24 changed files with 994 additions and 820 deletions

View file

@ -16,9 +16,18 @@ import (
"strings"
"sync"
"testing"
"text/template"
)
var toRemove []string
func TestMain(m *testing.M) {
status := m.Run()
for _, file := range toRemove {
os.RemoveAll(file)
}
os.Exit(status)
}
func testEnv(cmd *exec.Cmd) *exec.Cmd {
if cmd.Env != nil {
panic("environment already set")
@ -38,55 +47,63 @@ func testEnv(cmd *exec.Cmd) *exec.Cmd {
return cmd
}
func executeTest(t *testing.T, templ string, data interface{}, extra ...string) string {
var testprog struct {
sync.Mutex
dir string
target map[string]buildexe
}
type buildexe struct {
exe string
err error
}
func runTestProg(t *testing.T, binary, name string) string {
testenv.MustHaveGoBuild(t)
exe, err := buildTestProg(t, binary)
if err != nil {
t.Fatal(err)
}
got, _ := testEnv(exec.Command(exe, name)).CombinedOutput()
return string(got)
}
func buildTestProg(t *testing.T, binary string) (string, error) {
checkStaleRuntime(t)
st := template.Must(template.New("crashSource").Parse(templ))
dir, err := ioutil.TempDir("", "go-build")
if err != nil {
t.Fatalf("failed to create temp directory: %v", err)
}
defer os.RemoveAll(dir)
src := filepath.Join(dir, "main.go")
f, err := os.Create(src)
if err != nil {
t.Fatalf("failed to create file: %v", err)
}
err = st.Execute(f, data)
if err != nil {
f.Close()
t.Fatalf("failed to execute template: %v", err)
}
if err := f.Close(); err != nil {
t.Fatalf("failed to close file: %v", err)
}
for i := 0; i < len(extra); i += 2 {
fname := extra[i]
contents := extra[i+1]
if d, _ := filepath.Split(fname); d != "" {
if err := os.Mkdir(filepath.Join(dir, d), 0755); err != nil {
t.Fatal(err)
}
}
if err := ioutil.WriteFile(filepath.Join(dir, fname), []byte(contents), 0666); err != nil {
t.Fatal(err)
testprog.Lock()
defer testprog.Unlock()
if testprog.dir == "" {
dir, err := ioutil.TempDir("", "go-build")
if err != nil {
t.Fatalf("failed to create temp directory: %v", err)
}
testprog.dir = dir
toRemove = append(toRemove, dir)
}
cmd := exec.Command("go", "build", "-o", "a.exe")
cmd.Dir = dir
if testprog.target == nil {
testprog.target = make(map[string]buildexe)
}
target, ok := testprog.target[binary]
if ok {
return target.exe, target.err
}
exe := filepath.Join(testprog.dir, binary+".exe")
cmd := exec.Command("go", "build", "-o", exe)
cmd.Dir = "testdata/" + binary
out, err := testEnv(cmd).CombinedOutput()
if err != nil {
t.Fatalf("building source: %v\n%s", err, out)
exe = ""
target.err = fmt.Errorf("building %s: %v\n%s", binary, err, out)
testprog.target[binary] = target
return "", err
}
got, _ := testEnv(exec.Command(filepath.Join(dir, "a.exe"))).CombinedOutput()
return string(got)
target.exe = exe
testprog.target[binary] = target
return exe, nil
}
var (
@ -115,7 +132,12 @@ func testCrashHandler(t *testing.T, cgo bool) {
type crashTest struct {
Cgo bool
}
output := executeTest(t, crashSource, &crashTest{Cgo: cgo})
var output string
if cgo {
output = runTestProg(t, "testprogcgo", "Crash")
} else {
output = runTestProg(t, "testprog", "Crash")
}
want := "main: recovered done\nnew-thread: recovered done\nsecond-new-thread: recovered done\nmain-again: recovered done\n"
if output != want {
t.Fatalf("output:\n%s\n\nwanted:\n%s", output, want)
@ -126,8 +148,8 @@ func TestCrashHandler(t *testing.T) {
testCrashHandler(t, false)
}
func testDeadlock(t *testing.T, source string) {
output := executeTest(t, source, nil)
func testDeadlock(t *testing.T, name string) {
output := runTestProg(t, "testprog", name)
want := "fatal error: all goroutines are asleep - deadlock!\n"
if !strings.HasPrefix(output, want) {
t.Fatalf("output does not start with %q:\n%s", want, output)
@ -135,23 +157,23 @@ func testDeadlock(t *testing.T, source string) {
}
func TestSimpleDeadlock(t *testing.T) {
testDeadlock(t, simpleDeadlockSource)
testDeadlock(t, "SimpleDeadlock")
}
func TestInitDeadlock(t *testing.T) {
testDeadlock(t, initDeadlockSource)
testDeadlock(t, "InitDeadlock")
}
func TestLockedDeadlock(t *testing.T) {
testDeadlock(t, lockedDeadlockSource)
testDeadlock(t, "LockedDeadlock")
}
func TestLockedDeadlock2(t *testing.T) {
testDeadlock(t, lockedDeadlockSource2)
testDeadlock(t, "LockedDeadlock2")
}
func TestGoexitDeadlock(t *testing.T) {
output := executeTest(t, goexitDeadlockSource, nil)
output := runTestProg(t, "testprog", "GoexitDeadlock")
want := "no goroutines (main called runtime.Goexit) - deadlock!"
if !strings.Contains(output, want) {
t.Fatalf("output:\n%s\n\nwant output containing: %s", output, want)
@ -159,15 +181,15 @@ func TestGoexitDeadlock(t *testing.T) {
}
func TestStackOverflow(t *testing.T) {
output := executeTest(t, stackOverflowSource, nil)
want := "runtime: goroutine stack exceeds 4194304-byte limit\nfatal error: stack overflow"
output := runTestProg(t, "testprog", "StackOverflow")
want := "runtime: goroutine stack exceeds 1474560-byte limit\nfatal error: stack overflow"
if !strings.HasPrefix(output, want) {
t.Fatalf("output does not start with %q:\n%s", want, output)
}
}
func TestThreadExhaustion(t *testing.T) {
output := executeTest(t, threadExhaustionSource, nil)
output := runTestProg(t, "testprog", "ThreadExhaustion")
want := "runtime: program exceeds 10-thread limit\nfatal error: thread exhaustion"
if !strings.HasPrefix(output, want) {
t.Fatalf("output does not start with %q:\n%s", want, output)
@ -175,7 +197,7 @@ func TestThreadExhaustion(t *testing.T) {
}
func TestRecursivePanic(t *testing.T) {
output := executeTest(t, recursivePanicSource, nil)
output := runTestProg(t, "testprog", "RecursivePanic")
want := `wrap: bad
panic: again
@ -187,7 +209,7 @@ panic: again
}
func TestGoexitCrash(t *testing.T) {
output := executeTest(t, goexitExitSource, nil)
output := runTestProg(t, "testprog", "GoexitExit")
want := "no goroutines (main called runtime.Goexit) - deadlock!"
if !strings.Contains(output, want) {
t.Fatalf("output:\n%s\n\nwant output containing: %s", output, want)
@ -211,15 +233,15 @@ func TestGoexitDefer(t *testing.T) {
}
func TestGoNil(t *testing.T) {
output := executeTest(t, goNilSource, nil)
output := runTestProg(t, "testprog", "GoNil")
want := "go of nil func value"
if !strings.Contains(output, want) {
t.Fatalf("output:\n%s\n\nwant output containing: %s", output, want)
}
}
func TestMainGoroutineId(t *testing.T) {
output := executeTest(t, mainGoroutineIdSource, nil)
func TestMainGoroutineID(t *testing.T) {
output := runTestProg(t, "testprog", "MainGoroutineID")
want := "panic: test\n\ngoroutine 1 [running]:\n"
if !strings.HasPrefix(output, want) {
t.Fatalf("output does not start with %q:\n%s", want, output)
@ -227,7 +249,7 @@ func TestMainGoroutineId(t *testing.T) {
}
func TestNoHelperGoroutines(t *testing.T) {
output := executeTest(t, noHelperGoroutinesSource, nil)
output := runTestProg(t, "testprog", "NoHelperGoroutines")
matches := regexp.MustCompile(`goroutine [0-9]+ \[`).FindAllStringSubmatch(output, -1)
if len(matches) != 1 || matches[0][0] != "goroutine 1 [" {
t.Fatalf("want to see only goroutine 1, see:\n%s", output)
@ -235,7 +257,7 @@ func TestNoHelperGoroutines(t *testing.T) {
}
func TestBreakpoint(t *testing.T) {
output := executeTest(t, breakpointSource, nil)
output := runTestProg(t, "testprog", "Breakpoint")
want := "runtime.Breakpoint()"
if !strings.Contains(output, want) {
t.Fatalf("output:\n%s\n\nwant output containing: %s", output, want)
@ -253,293 +275,34 @@ import (
{{if .Cgo}}
import "C"
{{end}}
func test(name string) {
defer func() {
if x := recover(); x != nil {
fmt.Printf(" recovered")
}
fmt.Printf(" done\n")
}()
fmt.Printf("%s:", name)
var s *string
_ = *s
fmt.Print("SHOULD NOT BE HERE")
}
func testInNewThread(name string) {
c := make(chan bool)
go func() {
runtime.LockOSThread()
test(name)
c <- true
}()
<-c
}
func main() {
runtime.LockOSThread()
test("main")
testInNewThread("new-thread")
testInNewThread("second-new-thread")
test("main-again")
}
`
const simpleDeadlockSource = `
package main
func main() {
select {}
}
`
const initDeadlockSource = `
package main
func init() {
select {}
}
func main() {
}
`
const lockedDeadlockSource = `
package main
import "runtime"
func main() {
runtime.LockOSThread()
select {}
}
`
const lockedDeadlockSource2 = `
package main
import (
"runtime"
"time"
)
func main() {
go func() {
runtime.LockOSThread()
select {}
}()
time.Sleep(time.Millisecond)
select {}
}
`
const goexitDeadlockSource = `
package main
import (
"runtime"
)
func F() {
for i := 0; i < 10; i++ {
}
}
func main() {
go F()
go F()
runtime.Goexit()
}
`
const stackOverflowSource = `
package main
import "runtime/debug"
func main() {
debug.SetMaxStack(4<<20)
f(make([]byte, 10))
}
func f(x []byte) byte {
var buf [64<<10]byte
return x[0] + f(buf[:])
}
`
const threadExhaustionSource = `
package main
import (
"runtime"
"runtime/debug"
)
func main() {
debug.SetMaxThreads(10)
c := make(chan int)
for i := 0; i < 100; i++ {
go func() {
runtime.LockOSThread()
c <- 0
select{}
}()
<-c
}
}
`
const recursivePanicSource = `
package main
import (
"fmt"
)
func main() {
func() {
defer func() {
fmt.Println(recover())
}()
var x [8192]byte
func(x [8192]byte) {
defer func() {
if err := recover(); err != nil {
panic("wrap: " + err.(string))
}
}()
panic("bad")
}(x)
}()
panic("again")
}
`
const goexitExitSource = `
package main
import (
"runtime"
"time"
)
func main() {
go func() {
time.Sleep(time.Millisecond)
}()
i := 0
runtime.SetFinalizer(&i, func(p *int) {})
runtime.GC()
runtime.Goexit()
}
`
const goNilSource = `
package main
func main() {
defer func() {
recover()
}()
var f func()
go f()
select{}
}
`
const mainGoroutineIdSource = `
package main
func main() {
panic("test")
}
`
const noHelperGoroutinesSource = `
package main
import (
"runtime"
"time"
)
func init() {
i := 0
runtime.SetFinalizer(&i, func(p *int) {})
time.AfterFunc(time.Hour, func() {})
panic("oops")
}
func main() {
}
`
const breakpointSource = `
package main
import "runtime"
func main() {
runtime.Breakpoint()
}
`
func TestGoexitInPanic(t *testing.T) {
// see issue 8774: this code used to trigger an infinite recursion
output := executeTest(t, goexitInPanicSource, nil)
output := runTestProg(t, "testprog", "GoexitInPanic")
want := "fatal error: no goroutines (main called runtime.Goexit) - deadlock!"
if !strings.HasPrefix(output, want) {
t.Fatalf("output does not start with %q:\n%s", want, output)
}
}
const goexitInPanicSource = `
package main
import "runtime"
func main() {
go func() {
defer func() {
runtime.Goexit()
}()
panic("hello")
}()
runtime.Goexit()
}
`
func TestPanicAfterGoexit(t *testing.T) {
// an uncaught panic should still work after goexit
output := executeTest(t, panicAfterGoexitSource, nil)
output := runTestProg(t, "testprog", "PanicAfterGoexit")
want := "panic: hello"
if !strings.HasPrefix(output, want) {
t.Fatalf("output does not start with %q:\n%s", want, output)
}
}
const panicAfterGoexitSource = `
package main
import "runtime"
func main() {
defer func() {
panic("hello")
}()
runtime.Goexit()
}
`
func TestRecoveredPanicAfterGoexit(t *testing.T) {
output := executeTest(t, recoveredPanicAfterGoexitSource, nil)
output := runTestProg(t, "testprog", "RecoveredPanicAfterGoexit")
want := "fatal error: no goroutines (main called runtime.Goexit) - deadlock!"
if !strings.HasPrefix(output, want) {
t.Fatalf("output does not start with %q:\n%s", want, output)
}
}
const recoveredPanicAfterGoexitSource = `
package main
import "runtime"
func main() {
defer func() {
defer func() {
r := recover()
if r == nil {
panic("bad recover")
}
}()
panic("hello")
}()
runtime.Goexit()
}
`
func TestRecoverBeforePanicAfterGoexit(t *testing.T) {
// 1. defer a function that recovers
// 2. defer a function that panics
@ -561,7 +324,7 @@ func TestRecoverBeforePanicAfterGoexit(t *testing.T) {
}
func TestNetpollDeadlock(t *testing.T) {
output := executeTest(t, netpollDeadlockSource, nil)
output := runTestProg(t, "testprognet", "NetpollDeadlock")
want := "done\n"
if !strings.HasSuffix(output, want) {
t.Fatalf("output does not start with %q:\n%s", want, output)