blog.8-p.info

I’ve been using primarily Go at work since last summer and Go’s testing package bit me a few times. Do you know that FailNow() is killing a goroutine? I didn’t even know that killing a goroutine is possible.

In this series, I will explain how Go’s testing works, from go test command to testing package by peeking into the implementations.

Let’s start from “go test”!

In Go, a lot of official toolchain is invoked through the go command and testing is no exception. Let’s start from go test.

src/cmd/go/main.go has the go command’s main function. Unlike Git where all subcommands are different binaries. The go command has all subcommands inside.

func init() {
	base.Go.Commands = []*base.Command{
        ...
        test.CmdTest,
        ...
    }
}

func main() {
	_ = go11tag
	flag.Usage = base.Usage
	flag.Parse()
	log.SetFlags(0)
    ...
BigCmdLoop:
	for bigCmd := base.Go; ; {
		for _, cmd := range bigCmd.Commands {
        ...
        }
    }
}

src/cmd/go/internal/test/test.go has CmdTest and runTest is where interesting things are happening.

func init() {
	CmdTest.Run = runTest
}

const testUsage = "go test [build/test flags] [packages] [build/test flags & test binary flags]"

var CmdTest = &base.Command{
	CustomFlags: true,
	UsageLine:   testUsage,
	Short:       "test packages",
    ...

runTest is building a list of actions – builds, runs and prints – and pass them to work.Builder#Do(). Note that while prints are only passed to root, the actions are having dependencies between which invokes the actions of runs and builds by just passing prints.

func runTest(cmd *base.Command, args []string) {
    ...
	var b work.Builder
	b.Init()
	...
	var builds, runs, prints []*work.Action
	...
	// Prepare build + run + print actions for all packages being tested.
	for _, p := range pkgs {
		// sync/atomic import is inserted by the cover tool. See #18486
		if testCover && testCoverMode == "atomic" {
			ensureImport(p, "sync/atomic")
		}

		buildTest, runTest, printTest, err := builderTest(&b, p)
		if err != nil {
			str := err.Error()
			str = strings.TrimPrefix(str, "\n")
			if p.ImportPath != "" {
				base.Errorf("# %s\n%s", p.ImportPath, str)
			} else {
				base.Errorf("%s", str)
			}
			fmt.Printf("FAIL\t%s [setup failed]\n", p.ImportPath)
			continue
		}
		builds = append(builds, buildTest)
		runs = append(runs, runTest)
		prints = append(prints, printTest)
	}

	// Ultimately the goal is to print the output.
	root := &work.Action{Mode: "go test", Func: printExitStatus, Deps: prints}
	...
    var b work.Builder
	b.Init()
    ...
    b.Do(root)
}

builderTest is using the builder to build actions that invoke the compiler, linker, executing the artifacts and so on.

func builderTest(b *work.Builder, p *load.Package) (buildAction, runAction, printAction *work.Action, err error) {
	...
	// Set compile objdir to testDir we've already created,
	// so that the default file path stripping applies to _testmain.go.
	b.CompileAction(work.ModeBuild, work.ModeBuild, pmain).Objdir = testDir

	a := b.LinkAction(work.ModeBuild, work.ModeBuild, pmain)
	a.Target = testDir + testBinary + cfg.ExeSuffix
	...
	if testC {
		printAction = &work.Action{Mode: "test print (nop)", Package: p, Deps: []*work.Action{runAction}} // nop
		vetRunAction = printAction
	} else {
		// run test
		c := new(runCache)
		runAction = &work.Action{
			Mode:       "test run",
			Func:       c.builderRunTest,
			Deps:       []*work.Action{buildAction},
			Package:    p,
			IgnoreFail: true, // run (prepare output) even if build failed
			TryCache:   c.tryCache,
			Objdir:     testDir,
		}
		vetRunAction = runAction
		cleanAction = &work.Action{
			Mode:       "test clean",
			Func:       builderCleanTest,
			Deps:       []*work.Action{runAction},
			Package:    p,
			IgnoreFail: true, // clean even if test failed
			Objdir:     testDir,
		}
		printAction = &work.Action{
			Mode:       "test print",
			Func:       builderPrintTest,
			Deps:       []*work.Action{cleanAction},
			Package:    p,
			IgnoreFail: true, // print even if test failed
		}
	}
	...
}

Is “work” a good package name?

The work package is a bit odd. It actually has “go install” and “go build”, but it is called “work”. Anyway the Builder is in src/cmd/go/internal/work/action.go.

// A Builder holds global state about a build.
// It does not hold per-package state, because we
// build packages in parallel, and the builder is shared.
type Builder struct {
	WorkDir     string               // the temporary work directory (ends in filepath.Separator)
	actionCache map[cacheKey]*Action // a cache of already-constructed actions
	mkdirCache  map[string]bool      // a cache of created directories
	flagCache   map[[2]string]bool   // a cache of supported compiler flags
	Print       func(args ...interface{}) (int, error)

	IsCmdList           bool // running as part of go list; set p.Stale and additional fields below
	NeedError           bool // list needs p.Error
	NeedExport          bool // list needs p.Export
	NeedCompiledGoFiles bool // list needs p.CompiledGoFIles
    ...

And Do() is in src/cmd/go/internal/work/exec.go. While the function itself does some stuff, it doesn’t do much for testing specifically.

Trivia Time!

Before heading over to the testing package. There are two interesting implementation details I’d like to share.

First, if you explicitly disable Go’s test timeout, it still kills your test after almost one century.

		// An explicit zero disables the test timeout.
		// No timeout is passed to tests.
		// Let it have one century (almost) before we kill it.
		testActualTimeout = -1
		testKillTimeout = 100 * 365 * 24 * time.Hour

Second, this is more likely about Windows, but Windows treats some binaries differently based on solely its names and Go workarounds that.

	if cfg.Goos == "windows" {
		// There are many reserved words on Windows that,
		// if used in the name of an executable, cause Windows
		// to try to ask for extra permissions.
		// The word list includes setup, install, update, and patch,
		// but it does not appear to be defined anywhere.
		// We have run into this trying to run the
		// go.codereview/patch tests.
		// For package names containing those words, use test.test.exe
		// instead of pkgname.test.exe.
		// Note that this file name is only used in the Go command's
		// temporary directory. If the -c or other flags are
		// given, the code below will still use pkgname.test.exe.
		// There are two user-visible effects of this change.
		// First, you can actually run 'go test' in directories that
		// have names that Windows thinks are installer-like,
		// without getting a dialog box asking for more permissions.
		// Second, in the Windows process listing during go test,
		// the test shows up as test.test.exe, not pkgname.test.exe.
		// That second one is a drawback, but it seems a small
		// price to pay for the test running at all.
		// If maintaining the list of bad words is too onerous,
		// we could just do this always on Windows.
		for _, bad := range windowsBadWords {
			if strings.Contains(testBinary, bad) {
				a.Target = testDir + "test.test" + cfg.ExeSuffix
				break
			}
		}
	}