-
Notifications
You must be signed in to change notification settings - Fork 77
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
testscript: expose current subtest in Env #95
Conversation
Is there a particular reason to expose this rather than closing over the |
@myitcv could you expand a bit? Do you mean using the *testing.T being passed to testscript.Run in the setup func? A failure in setting up would make the top level test fail at that point, not the subtest. Also, cleanup functions would be executed only at the end of all tests, not as soon as each subtest completes. |
Sorry, you're quite right, I wasn't thinking about the fact these run as parallel subtests. I would be supportive of this for exactly the reason you state: cleanup. Because right now (and pre the arrival of https://pkg.go.dev/testing?tab=doc#T.Cleanup) I'm having to do this myself. |
cc @rogpeppe |
testscript/testscript.go
Outdated
@@ -69,6 +69,11 @@ func (e *Env) Defer(f func()) { | |||
e.ts.Defer(f) | |||
} | |||
|
|||
// T returns the current subtest. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this is quite right. I think the idea is that you can do:
t := env.T().(*testing.T)
but that won't work because T
isn't implemented by *testing.T
(there's an extra Verbose
method and the signature of the Run
method is different). The doc comment on the T
type is misleading.
I can see a couple of possible ways around this. We could change the signature to this:
// T returns the t argument passed to the current test by the T.Run method.
// This enables a Setup method to acquire the current test value.
// If Cleanup is called on the returned value, the function will run
// after any functions passed to Env.Defer.
func (e *Env) T() interface{}
Alternatively, we could factor out the methods in common between T
and *testing.T
:
// T holds the methods of the *testing.T type that
// are used by testscript along with methods that are
// different because of the requirements of RunT.
type T interface {
TCommon
Run(string, func(T))
// Verbose is usually implemented by the testing package
// directly rather than on the *testing.T type.
Verbose() bool
}
// TCommon holds the methods held in common between
// T and *testing.T.
type TCommon interface {
Skip(...interface{})
Fatal(...interface{})
Parallel()
Log(...interface{})
FailNow()
}
// T returns the test's current T value. This will be either an instance of T
// or an instance of *testing.T depending on whether Run or RunT was
// used to run the tests.
func (e *Env) T() TCommon
Another possibility that springs to mind is that we could expose the tshim
type:
// TShim adapts a *testing.T to make it implement the T
// interface.
type TShim struct {
*testing.T
}
// T returns the value of T for the current script test.
// If the tests were started by calling `Run`, the original
// *testing.T value is wrapped, but can be retrieved with:
//
// env.T().(TShim).T
func (e *Env) T() T
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, all of those would work. For my use case env.T().(testing.TB)
would be good enough, hence this proposal.
If instead we want to allow access to the subtest's *testing.T
, that's different, as you described. But then we exclude testscript.RunT
, because if you had a *testing.T
in the first place, then you would have used Run
. RunT
is (I suppose) intended to be used when you don't actually have a *testing.T
, but your own type implementing testscript.T
.
If that's true, then we could implement something that's very easy to use for the relevant use case, and impossible to use otherwise. Like
// T returns the current subtest, or an error if testcript.RunT was used to run the tests.
// If Cleanup is called on the returned value,
// the function will run after any functions passed to Env.Defer.
func (e *Env) T() (*testing.T, error)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For my use case env.T().(testing.TB) would be good enough, hence this proposal.
That's a reasonable point. Maybe there's not really a good use case for
making the actual concrete *testing.T
value available.
Something like this, perhaps? (the same as your proposal
with an expanded doc comment)
// T returns the t argument passed to the current test by the T.Run method.
// Note that if the tests were started by calling Run,
// the returned value will implement testing.TB. Note that
// despite that, the underlying value will not be of type *testing.T
// because *testing.T does not implement T.
//
// If Cleanup is called on the returned value, the function will run
// after any functions passed to Env.Defer.
func (e *Env) T() T
If at some point, someone does come up with a specific use case that requires
*testing.T
, we can always expose TShim
or similar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, I made that change to the docstring.
64358a2
to
a58f376
Compare
@@ -113,6 +113,9 @@ func TestScripts(t *testing.T) { | |||
if ts.Value("somekey") != 1234 { | |||
ts.Fatalf("test-values did not see expected value") | |||
} | |||
if ts.Value("t").(T) != ts.t { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be worth checking that it actually implements testing.TB
, as documented.
if ts.Value("t").(T) != ts.t { | |
if ts.Value("t").(testing.TB) != ts.t { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, in a slightly different way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM with one final suggestion, thanks!
a58f376
to
cd7b42d
Compare
This way, the setup function could fail in case setting up the test is currently not possible.
Moreover, client code could type assert the returned testscript.T at its own risk, for instance for registering cleanup functions or using test helpers like