Finding the go command from inside a test case

126 views
Skip to first unread message

Patrick Smith

unread,
Aug 11, 2022, 3:59:43 AM8/11/22
to golang-nuts
I sometimes find that I want to run the go command from inside a test case, for example to build generated code, or to verify that code that should raise an error at compile time does in fact cause that error.

That raises the question, is there a reliable way to find the go command from inside a test case. Including for the case where one gives some code and test cases to someone else, and they are running 'go test' in their own environment? If there is no reliable way, should there be? Or is the answer "don't do that!"?

It's particularly frustrating, because in the normal usage, there is a go command available (one running 'go test', and one used to build the test binary), but I can't see a way to find it. Ideally, I would like to find the go command used to build the test binary.

To date, I've been using a recipe I found some time ago through an Internet search (I've forgotten the source, so can't credit it):

        gocmd := runtime.GOROOT() + "/bin/go"
        if runtime.GOOS == "windows" {
                gocmd += ".exe"
        }

(Of course, this should use filepath.Join instead of string concatenation.)

I recently tried to look into how reliable this is; here are my conclusions so far.

runtime.GOROOT returns the value of the environment variable GOROOT if it is set, and the GOROOT used to compile the (test) binary if not. So if $GOROOT is not set, we should be ok.

With the default compiler gc, if $GOROOT is set, then 'go test' checks that it seems to point to a valid go installation for the same version of go as the one running 'go test', exiting if either check fails. That $GOROOT is passed to the test binary, so will be returned by runtime.GOROOT, but it should be OK to use.

With gccgo, as far as I can tell 'go test' does not check $GOROOT at all, and passes it on to the test binary unchanged:

> cat try_test.go
package main_test

import (
"os"
"runtime"
"testing"
)

func TestFoo(t *testing.T) {
t.Error("version", runtime.Version())
t.Error("$GOROOT", os.Getenv("GOROOT"))
t.Error("runtime.GOROOT", runtime.GOROOT())
}

> go test
--- FAIL: TestFoo (0.00s)
    try_test.go:10: version unknown
    try_test.go:11: $GOROOT
    try_test.go:12: runtime.GOROOT /usr
FAIL
exit status 1
FAIL try 0.068s

> GOROOT=foo go test
--- FAIL: TestFoo (0.00s)
    try_test.go:10: version unknown
    try_test.go:11: $GOROOT foo
    try_test.go:12: runtime.GOROOT foo
FAIL
exit status 1
FAIL try 0.063s

So, when using gccgo, we can't depend on the value of $GOROOT at all. It might be set to something completely unusable, like "foo". Or perhaps the user normally uses gc and has $GOROOT set correspondingly, but one day decides to test that the code works with gccgo, but doesn't unset $GOROOT - in which case the test binary may be built with gccgo, but when the test case looks for the go command, it will find gc, not gccgo.

So perhaps the strategy is, if the test binary was built with gc and $GOROOT is set, just skip the test. Is there a better option?

Finally, there is also the case where the binary is built and saved with 'go test -c', and then run at some later time, possibly on another computer. $GOROOT might be set to anything. Even if it isn't set, the go installation that was used to build the test binary (returned by runtime.GOROOT) might not exist any more, or might have been updated to another version of go. In one sense, I'm not too worried about the 'go test -c' case though, on the theory that people doing this are responsible for correctly setting things up before running the test binary.

Jan Mercl

unread,
Aug 11, 2022, 4:09:35 AM8/11/22
to Patrick Smith, golang-nuts
On Thu, Aug 11, 2022 at 9:59 AM Patrick Smith <pat42...@gmail.com> wrote:

> To date, I've been using a recipe I found some time ago through an Internet search (I've forgotten the source, so can't credit it):
>
> gocmd := runtime.GOROOT() + "/bin/go"
> if runtime.GOOS == "windows" {
> gocmd += ".exe"
> }

I think using https://pkg.go.dev/os/exec#LookPath would be more
correct as it respects the environment of the current process.

ma...@eliasnaur.com

unread,
Aug 11, 2022, 6:18:49 AM8/11/22
to golang-nuts
exec.LookPath is even better in Go 1.19, which implements

"go test and go generate now place GOROOT/bin at the beginning of the PATH used for the subprocess, so tests and generators that execute the go command will resolve it to same GOROOT."[0]

Elias

Patrick Smith

unread,
Aug 11, 2022, 12:53:40 PM8/11/22
to ma...@eliasnaur.com, golang-nuts
On Thu, Aug 11, 2022 at 3:19 AM ma...@eliasnaur.com <ma...@eliasnaur.com> wrote:
exec.LookPath is even better in Go 1.19, which implements

"go test and go generate now place GOROOT/bin at the beginning of the PATH used for the subprocess, so tests and generators that execute the go command will resolve it to same GOROOT."[0]

Elias


Thanks! This change doesn't seem to be included in gccgo 12.1.1, but I presume it will eventually be included in a later version. 
Reply all
Reply to author
Forward
0 new messages