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
			}
		}
	}

To improve my working from home situation, I bought Philips Brilliance 27 inch 4k monitor with USB-C docking (272P7VUBNB). This monitor is 4k, USB Type-C and sub-$350. I’m not picky about color. It looks good to me.

4k 60Hz

My home computer is a 13-inch 2017 MacBook Pro. I initially struggled to get 4k 60Hz from the USB Type-C connection, but I managed to do it.

First this monitor supports both USB 2.0 and USB 3.2, but to use USB Type-C, you must use USB 2.0. Second, On the MacBook side, I need to reset NVRAM. Resetting NVRAM is like restarting a computer to fix a problem. I don’t know what was wrong.

Fargate’s platform version 1.4.0 has been released yesterday.

This is the first Fargate release I’ve helped. That’s all I wanted to write about today and kids nowadays may just “tweet” about that instead of blogging. No. I was using Twitter. I was one of the cool kids before.

Anyway, yay!

Work from home

Mar 29, 2020

I work from home for weeks due to COVID-19. I bought a few stuff and learned a bit about how to work from home.

Keyboard: Anne Pro 2

I’ve been using Happy Hacking Keyboard for years. I bought mine when I was in Japan and the premium keyboard market was fairly small at that time.

Probably due to the rise of PC gaming, now there are a lot of compact, mechanical/premium keyboards in the market, often with crazy RGB LEDs. The sad news is, many of them have proprietary configuration software which only works on Windows. While the software only needed for its initial configuration, many keyboards have upgradable firmware and I know how software engineers deal with things when they are upgradable. Let’s address this issue and that issue in the next release! So that ruled out Niz and Kemove.

Anne Pro 2 is the only option that supports Windows, Mac and Linux. It doesn’t have Happy Hacking Keyboard’s premium feeling, but the price difference can justify that.

Note that Anne Pro 2 is different from Anne Pro. Contrary to the subtle “this is just v2” naming, Anne Pro 2 uses Holtek, whereas Anne Pro uses STM32. For example, Rust people may recognize Anne Pro as the one that has open-source Rust firmware, but it doesn’t work with Anne Pro 2.

Regarding Niz, it uses electrostatic capacitive switches like Happy Hacking Keyboard. Niz’s configuration protocol is partially reverse-engineered by cho45. I hope someone could go further or Niz themselves to support non-Windows platforms.

Trackball: Elecom Deft Pro (M-DPT1MRXBK)

I like finger-operated trackballs. Trackballs are niche, and finger-operated ones might be even. Logitech makes some trackballs but they are all thumb-operated.

I’ve been using this Elecom’s one at work and bought the same one. Okay. Not much geeking out.

External Monitor and Chair

I have bought Phillips’ 4k monitor and Herman Miller’s Sayl Chair, but it was late and I haven’t received them yet.

Herman Miller’s is doing Work From Home Sale with a 15% discount. If you’ve been wanted to buy Aaron Chair, it would be a good opportunity.

Music

I don’t listen to music much when I work in an office. My workplace is quiet enough and sometimes there are interesting conversations around.

I did the same at home and I initially thought that listening music at home was irresponsible as a parent. Why don’t I help my wife rather than isolating myself? But the reality is

  1. I have some work stuff to do to pay my bills.
  2. I can step-in, but cannot stick with my wife and kids due to #1.
  3. It is irresponsible to step-in, leave in the middle and don’t see consequences, and say “sorry, I have work”

So I listen to music to isolate myself and avoid micromanaging all of the stuff.

Regarding the music itself, I like Pomplamoose. They are great.

Routines

I’m trying to work from 9 am to 5 pm. So having breakfast and changing clothes (incl. kids) should be happening before that, and having dinner and playing with kids should be happening after that. As all parents know, it doesn’t work all the time, but I’d like to make it consistent.

I make coffee either after breakfast or after lunch and have that in an insulated bottle. I walk a bit after work to disconnect myself from work. These are also not really consistent but helped me when I could.

The answer was both. But since Go 1.14, go test only uses stdout.

From Russ Cox’s commit message:

In past releases, whether test output appears on stdout or stderr has varied depending on exactly how go test was invoked and also (indefensibly) on the number of CPUs available.