Running the Go Race Detector with -cover

There are two test options that make testing in Go especially powerful: one is -cover, which generates test coverage reports, and the other is -race, which warns of possible race conditions in your code. If you haven’t used them yet, you should. They are very useful tools, and both are covered well (so to speak) by official blog posts: The cover story and Introducing the Race Detector.

A subtle problem arises, however, when you use these two together. Suppose we have this simple function that adds one a hundred times inside separate goroutines and returns the result of this summation:

package coverrace

func add100() int {
    total := 0
    c := make(chan int, 1)
    for i := 0; i < 100; i++ {
        go func(chan int) {
            c <- 1
        }(c)
    }
    for u := 0; u < 100; u++ {
        total += <-c
    }
    return total
}

We can place the function in a file called coverrace.go, and write a test for it in coverrace_test.go. The test might look like this:

package coverrace

import "testing"

func TestCoverRace(t *testing.T) {
    got := add100()
    if got != 100 {
        t.Errorf("got %d, want %d", got, 100)
    }
}

It just asserts that we get a 100 when calling the function, and raises an error if we don’t. So far so good, let’s run the test:

$ go test
PASS
ok      github.com/hermanschaaf/coverrace   0.005s

Test passed! Nice. Let’s check out our coverage:

$ go test -cover
PASS
coverage: 100.0% of statements
ok      github.com/hermanschaaf/coverrace   0.005s

100% test coverage, not bad. Now let’s use the race detector to make sure we didn’t make any silly mistakes:

$ go test -race
PASS
ok      github.com/hermanschaaf/coverrace   1.053s

The test took a wee bit longer to run this time, but it looks like our implementation is OK as far as race conditions are concerned. Time to put it all together and get ready for uploading this to production! Let’s just run everything one more time to be sure:

$ go test -race -cover
==================
WARNING: DATA RACE
Write by goroutine 5:
  github.com/hermanschaaf/coverrace.func·001()
      github.com/hermanschaaf/coverrace/_test/coverrace.go:10 +0x45

Previous write by goroutine 4:
  github.com/hermanschaaf/coverrace.func·001()
      github.com/hermanschaaf/coverrace/_test/coverrace.go:10 +0x45
...
==================
PASS
coverage: 100.0% of statements
Found 1 data race(s)
exit status 66
FAIL    github.com/hermanschaaf/coverrace   1.059s

Wait, what just happened? The code remained the same, but now we have a race condition! How is this possible?

The tests are actually running on a copy of the source code, made by the cover tool to insert counting lines in code blocks. We can see this from the fact that the test errors are coming from a _test folder that we didn’t create. The code, after adding the coverage lines, looks like this:

package test

func add100() int {
    GoCover.Count[0] = 1
    total := 0
    c := make(chan int, 1)
    for i := 0; i < 100; i++ {
        GoCover.Count[3] = 1
        go func(chan int) {
            GoCover.Count[4] = 1
            c <- 1
        }(c)
    }
    GoCover.Count[1] = 1
    for u := 0; u < 100; u++ {
        GoCover.Count[5] = 1
        total += <-c
    }
    GoCover.Count[2] = 1
    return total
}

So cover added some extra lines to check off when any specific block of code is reached. It might be a bit old-school, but it works just fine. The only problem is that it is also setting a global variable inside a goroutine (GoCover.Count[4] = 1), and the race detector doesn’t like that at all. What can we do if we want to run both cover and race detector at the same time?

Luckily for us, there is a simple solution to this problem: we can run the tests using the special -covermode=atomic flag, like this:

go test -race -covermode=atomic

Doing this, the generated code uses the sync/atomic package instead of setting a global variable, and it looks like this:

import _cover_atomic_ "sync/atomic"

func add100() int {
    _cover_atomic_.AddUint32(&GoCover.Count[0], 1)
    total := 0
    c := make(chan int, 1)
    for i := 0; i < 100; i++ {
        _cover_atomic_.AddUint32(&GoCover.Count[3], 1)
        go func(chan int) {
            _cover_atomic_.AddUint32(&GoCover.Count[4], 1)
            c <- 1
        }(c)
    }
    _cover_atomic_.AddUint32(&GoCover.Count[1], 1)
    for u := 0; u < 100; u++ {
        _cover_atomic_.AddUint32(&GoCover.Count[5], 1)
        total += <-c
    }
    _cover_atomic_.AddUint32(&GoCover.Count[2], 1)
    return total
}

var _ = _cover_atomic_.AddUint32

, which is safe from race conditions, as shown by the new output of running the tests with this flag:

$ go test -race -covermode=atomic
PASS
coverage: 100.0% of statements
ok      github.com/hermanschaaf/coverrace   1.055s

Crisis averted!

Protip: when running go test with both -cover and -race, also add the -covermode=atomic flag - it might just save you an hour or two of unnecessary race condition-hunting.

If you’d like to play with the source code of this post, it’s on Github.

Update: Good news, everyone! @shawnps pointed out that from Go 1.3 the atomic mode will be enabled by default when -cover is run with -race, so this will not be an issue for much longer :)

 
94
Kudos
 
94
Kudos

Now read this

Solving Regex Crosswords using Go

I first discovered regular expression crosswords two weeks ago, when RegexCrossword.com appeared on the Hacker News front page. I thoroughly enjoyed the nerdy 30 minutes I spent doing the puzzles, but soon enough my natural inclination... Continue →