Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

testing: `go test` fails if flag.Parse() called in init #31859

Open
vearutop opened this issue May 6, 2019 · 25 comments

Comments

Projects
None yet
6 participants
@vearutop
Copy link
Contributor

commented May 6, 2019

What version of Go are you using (go version)?

$ go version
go version devel +b41eee4 Sun May 5 15:17:52 2019 +0000 darwin/amd64

Does this issue reproduce with the latest release?

Yes

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/viacheslav.poturaev/Library/Caches/go-build"
GOENV="/Users/viacheslav.poturaev/Library/Preferences/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GONOPROXY=""
GONOSUMDB=""
GOOS="darwin"
GOPATH="/Users/viacheslav.poturaev/go"
GOPROXY="direct"
GOROOT="/Users/viacheslav.poturaev/sdk/gotip"
GOSUMDB="off"
GOTMPDIR=""
GOTOOLDIR="/Users/viacheslav.poturaev/sdk/gotip/pkg/tool/darwin_amd64"
GCCGO="gccgo"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD="/Users/viacheslav.poturaev/go/src/github.com/hellofresh/ro-kit/cc/go.mod"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/lb/j0gz8jln36z4frqkrzmkdb0c0000gp/T/go-build279729567=/tmp/go-build -gno-record-gcc-switches -fno-common"

What did you do?

Ran test having custom command line flags, minimal example:

package my_test

import (
	"flag"
	"testing"
)

func init() {
	flag.Parse()
}

func TestSomething(t *testing.T) {}

What did you expect to see?

go1.12 test .
ok  	github.com/bla/cc	0.007s

What did you see instead?

gotip test .
flag provided but not defined: -test.testlogfile
Usage of /var/folders/lb/j0gz8jln36z4frqkrzmkdb0c0000gp/T/go-build200454244/b001/cc.test:
FAIL	 github.com/bla/cc	 0.007s
@bcmills

This comment has been minimized.

Copy link
Member

commented May 6, 2019

What was the behavior with Go 1.12? (Is this a regression?)

@bcmills bcmills changed the title test fails if flag.Parse() called in init cmd/go: `go test` fails if flag.Parse() called in init May 6, 2019

@bcmills bcmills added this to the Go1.13 milestone May 6, 2019

@vearutop

This comment has been minimized.

Copy link
Contributor Author

commented May 6, 2019

I see this issue as a regression, go1.12 did not fail. So I could run tests with additional custom (application relevant) flags.

@vearutop

This comment has been minimized.

Copy link
Contributor Author

commented May 6, 2019

I think this is likely happening due to changed init precedence, that test framework init ran after app-level init.

@bcmills bcmills removed the WaitingForInfo label May 6, 2019

@bcmills bcmills changed the title cmd/go: `go test` fails if flag.Parse() called in init testing: `go test` fails if flag.Parse() called in init May 6, 2019

@bcmills

This comment has been minimized.

Copy link
Member

commented May 6, 2019

Looks like it's probably due to CL 173722, which was intended to address #21051.

@bcmills

This comment has been minimized.

Copy link
Member

commented May 6, 2019

@vearutop

This comment has been minimized.

Copy link
Contributor Author

commented May 6, 2019

OK, so if that is an intended behavior I can fix my code by adding a go1.13 build tagged init() with testing.Init() call inside.

@bcmills

This comment has been minimized.

Copy link
Member

commented May 6, 2019

I don't think that a breaking change was intended. This seems like something that should be fixed before Go 1.13 is released.

@bradfitz

This comment has been minimized.

Copy link
Member

commented May 6, 2019

We knew that fbc6a97 was going to break some people. It's arguably on the fence on whether it's inside the go1compat umbrella (it's half API, half tooling) and we guessed it would affect very few people. (generally more advanced people who could read the docs/release notes & work around)

That said, I haven't looked at this particular issue yet. I'll let @cespare have a look first.

@bcmills

This comment has been minimized.

Copy link
Member

commented May 6, 2019

It seems possible to fix this by putting the call to testing.Init in an earlier init function injected into the test variant of the package, rather than in testing.MainStart.

If I understand correctly, the init functions within a package run in the order in which the source files were presented to the compiler, right?

@cespare

This comment has been minimized.

Copy link
Contributor

commented May 6, 2019

Fun. I'll take a look today.

@cespare

This comment has been minimized.

Copy link
Contributor

commented May 7, 2019

What we (or at least I) intended with #21051 / CL 173722 was:

  • Anyone using go test shouldn't observe any difference or need to explicitly call testing.Init.
  • People using testing outside of go test (notably, testing.Benchmark within a package main) would need to use testing.Init if they want to use test flags.

So @vearutop this means that you shouldn't need to use testing.Init in your example code. In any case, it certainly shouldn't fail with such a mysterious error.

As @bcmills alluded to, the problem is that code which calls flag.Parse during initialization won't get the test flags. The initialization of the user's test package ("ptest"/"pxtest" in the go tool internal parlance) happens before the package main test runner initialization ("testmain") so there's nothing we can do in the generated testmain code to fix this.

The workaround that makes sense to me, which is what I think @bcmills is suggesting, is that we can insert a bit of code into the ptest/pxtest packages before we build them:

var _ = func() bool {
	testing.Init()
	return true
}()

(Putting this in a variable declaration expression means that it happens before flag.Parse is called in a variable initialization as well as an init function. Calling flag.Parse as part of variable declaration is ill-advised, of course, but it does work today.)

I think this is pretty straightforward. I'll send a CL soon.

If I understand correctly, the init functions within a package run in the order in which the source files were presented to the compiler, right?

Yeah, and same for variable declarations, modulo initialization dependencies. I'll ensure that the synthesized file is passed to the compiler first.

@gopherbot

This comment has been minimized.

Copy link

commented May 9, 2019

Change https://golang.org/cl/176098 mentions this issue: cmd/go: move automatic testing.Init call into generated test code

@vearutop

This comment has been minimized.

Copy link
Contributor Author

commented May 15, 2019

@cespare I'm afraid this is still broken.

Given somewhere/init.go:

package somewhere

import (
	"flag"
)

var StopOnFailure bool

func init() {
	flag.BoolVar(&StopOnFailure, "stop-on-failure", false,
		"Stop processing on first failed scenario.")

	flag.Parse()
}

And mytest/my_test.go:

package my_test

import (
	"fmt"
	"testing"

	"path/to/somewhere"
)

func TestSomething(t *testing.T) {
	_ = somewhere.StopOnFailure
}

func BenchmarkSomething(b *testing.B) {
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		_ = fmt.Sprintf("%d", i)
	}
}

gotip (go version devel +1e3ffb0c90 Tue May 14 22:30:48 2019 +0000 darwin/amd64) fails:

gotip test ./mytest/...
flag provided but not defined: -test.testlogfile
Usage of /var/folders/lb/j0gz8jln36z4frqkrzmkdb0c0000gp/T/go-build623057355/b001/mytest.test:
  -stop-on-failure
    	Stop processing on first failed scenario.
FAIL path/to/mytest	0.005s
FAIL
gotip test -bench=. ./mytest/...
flag provided but not defined: -test.bench
Usage of /var/folders/lb/j0gz8jln36z4frqkrzmkdb0c0000gp/T/go-build638505735/b001/mytest.test:
  -stop-on-failure
    	Stop processing on first failed scenario.
exit status 2
FAIL	 path/to/mytest	0.005s
FAIL
@cespare

This comment has been minimized.

Copy link
Contributor

commented May 15, 2019

@vearutop thanks for bringing it to my attention.

It turns out that this particular repro code relies on undefined behavior: it assumes that the testing package is initialized before the somewhere package. There's nothing in the spec that says it will be. And indeed, it so happens that on tip there are some initialization changes that already broke this code before any of my testing changes landed; see https://golang.org/cl/161337, https://golang.org/cl/170318, and related discussion over on #31636. If I run the repro with Go at 8515d9c, which is this commit where CL 170318 went in and before any of my testing changes, I get a failure:

~/apps/godev/bin/go test
flag provided but not defined: -test.timeout
Usage of /tmp/go-build059416027/b001/aaa.test:
  -stop-on-failure
        Stop processing on first failed scenario.
exit status 2
FAIL    aaa     0.001s

(And if I run with -race, it sometimes fails and sometimes succeeds.)

However, this all kind of doesn't matter because you could fix your repro code by making somewhere import testing, perhaps just by adding _ "testing" to the imports. Then you're guaranteed that testing is initialized first, and so it works up until my changes break it. So this is a problem.

To state it more generally, the "testinginit" workaround I did over in https://golang.org/cl/176098 ensures that testing.Init is called before the package under test is initialized. It does not ensure that testing.Init is called before that package's dependencies are initialized, so if they call flag.Parse during initialization they may not see the testing flags.

I think that a solution is to move the testinginit workaround from the package under test into the testing package. That is, go test would insert this file into the testing package:

package testing
func init() { Init() }

(This would be very simple with a test build tag, but we decided in #21360 not to do that.)

@bcmills thoughts?

@cespare cespare reopened this May 15, 2019

@vearutop

This comment has been minimized.

Copy link
Contributor Author

commented May 15, 2019

Not sure to understand:

And indeed, it so happens that in Go 1.12 there were some initialization changes that already broke this code

With go1.12.5 that example case never fails for me.

@cespare

This comment has been minimized.

Copy link
Contributor

commented May 15, 2019

Sorry, I meant to say that there are some changes on tip (that will be in Go 1.13). I updated my comment.

@vearutop

This comment has been minimized.

Copy link
Contributor Author

commented May 15, 2019

And also I tried adding _ "testing" to the imports:

package somewhere

import (
	"flag"
	_ "testing"
)

var StopOnFailure bool

func init() {
	flag.BoolVar(&StopOnFailure, "stop-on-failure", false,
		"Stop processing on first failed scenario.")

	flag.Parse()
}

but it did not help, same failure on gotip.

@cespare

This comment has been minimized.

Copy link
Contributor

commented May 15, 2019

Right, I'm saying that without the _ "testing" import your repro code is buggy due to depending on undefined behavior, and that bugginess is exposed by the init ordering changes unrelated to my testing changes that are on tip.

By adding the _ "testing" import, your repro code no longer depends on undefined behavior, but it was broken by my change.

So:

  • You need to fix your code if it depends on testing being initialized first without actually importing it
  • We need to fix Go so that any package (not just the package under test) sees that the test flags have been registered when running as a test
@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

commented May 15, 2019

It's hard to understand why code would call flag.Init in an init function that is not in the main package. That will ignore flags defined in other packages that are initialized later. I think I would want to see a clear explanation as to why we need to cater to that case at all, since it seems broken by design.

@bcmills

This comment has been minimized.

Copy link
Member

commented May 15, 2019

I think that a solution is to move the testinginit workaround from the package under test into the testing package.

That might be worth doing anyway, just because it would reduce the number of injected calls to testing.Init().

@vearutop

This comment has been minimized.

Copy link
Contributor Author

commented May 15, 2019

@ianlancetaylor my_test.go is owned by application but uses external library (somewhere) to run suite. Library imposes suite control with command line flags that are configured in library's init() so that app itself does not have to own irrelevant domain.

Alternatively env vars can be used instead of flags, though I'm not sure what is violated when flag.* is called outside main package.

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

commented May 15, 2019

Alternatively env vars can be used instead of flags, though I'm not sure what is violated when flag.* is called outside main package.

To be clear, we are talking only about flag.Parse here, not flag.*. The flag package is designed so that multiple packages can define flags, without those packages being aware of each other. It's essential that flag.Parse be called only after all flags are defined, meaning only after all packages are initialized. The normal practice is that the main package calls flag.Parse, because the main package is the only package that is sure to run after all packages are initialized.

Your program may well be designed so that it is OK to call flag.Parse in an init function of some non-main package. But most programs do not and cannot work that way. So I am questioning how much we should twist the testing package to support that use case. Of course if the change to the testing package is simple, then that is fine. But I would not like to see a lot of complexity added to support a very unusual use case.

@bcmills

This comment has been minimized.

Copy link
Member

commented May 15, 2019

I think it's important that we allow flag.Parse in an init function within a *_test.go file, since (in most cases) that file fills the same role as a main package.

I agree that it's not as important to support flag.Parse in an arbitrary init function outside of main or any *_test.go file.

@vearutop

This comment has been minimized.

Copy link
Contributor Author

commented May 15, 2019

In my humble opinion Go standard library should not race with user code breaking existing code, hopefully that can be achieved without much penalty.

@bradfitz

This comment has been minimized.

Copy link
Member

commented May 15, 2019

I think it's important that we allow flag.Parse in an init function within a *_test.go file, since (in most cases) that file fills the same role as a main package.

Using https://golang.org/pkg/testing/#hdr-Main is a bit cleaner.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.