-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Do not hang Eventually on failed condition
#1809
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
Conversation
406af1f to
5f135f6
Compare
5f135f6 to
73ecd57
Compare
brackendawson
left a comment
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.
This must be tested, you could do so like this:
func TestEventuallyFailsFast(t *testing.T) {
mockT := &mockTestingT{}
wait := make(chan struct{})
go func() {
Eventually(mockT, func() bool {
// Emulate a call to testing.T.FailNow
runtime.Goexit()
return true
}, time.Hour, time.Millisecond)
close(wait)
}()
select {
case <-wait:
case <-time.After(time.Second):
t.Fatal("Eventually did not fail fast")
}
// assert that eventually adds an error to mockT
True(t, mockT.Failed())
}The documentation of Eventually must be changed to explain the new failure behaviour when runtime.Goexit is called from the condition function.
The same change should be made in the other eventually/never functions where it makes sense.
The above being a review of the code, I'm hesitent about making a change like this. This relies on unsupported behavior in the standard library, namely the behavior of calling t.FailNow from a goroutine other than the one running the test. This behavior isn't guaranteed to remain the same, though with our particular case being synchronous I have no good reason to doubt it would change.
This is also inconsistent when compared with the T versions of this assertion, where a failing require function fails the condition and not the entire assertion. However that is already what happens, just more slowly.
|
@brackendawson I tried to address the issue overall, incl.
Please have a look again at the proposed changes. Thank you! |
|
I pushed a few smaller improvements on the tests docs and updated the PR and GH Issue description. I think The PR now covers |
|
While writing more tests I found my initial I updated the docs and tests for this, please have a look. @brackendawson Note that I changed |
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 what has happened here? We're now handling painics which I expected to be left out of this PR. The code is full of badly written comments and emoji. There are changes in three (!) un-related packages. And the changes in the file I expected to see changed are now so large that they aren't initially shown in GitHub's diff viewer.
At the time of my last review I though this was close to ready to merge, I think it might be better if you return to that state (73ecd57) and implement the requested changes without allowing the scope to creep.
|
Since this got out of hand, I will restart from 73ecd57 as proposed and make new PR, as small as possible, touching as few files as possible. Thanks for taking the time to review. EditI will still address all the comments to not leave them open. If there is value in any of the extensive docs in this PR, we may cherry pick them later; without emojis, as I now understand. The obscurity of
Other than that, changing scripts and unrelated files was really a mistake. Again, sorry for that, and for wasting your time. NextI now opened a new PR. There, I still fix the |
Summary
Do not hang
Eventuallywhen condition does not return normally and fail fast instead.Do not hang
Neverwhen condition does not return normally to prevent false positives.Code Changes
ch <- condition()with adeferfunc inEventuallych <- condition()with adeferfunc inNeverruntime.Goexit()variants to fail accordingly inEventually,Never, andEventuallyWithT"Condition exited unexpectedly"Details/Effect
EventuallyandNeverwill behave more unsurprisingly and fail on exited conditionsNeverwill not report false positives caused by unclean exitsEventuallyWithTcan distinguish betweencollectfailure and outer failure and will fail on an exit if it was not recorded in thecollect. The condition func must callcollect.FailNow()(also indirectly) or return normally to not failrequire.*functions also on the outer/parenttinside conditions to fail the test immediatelyDoc/Internal Changes
Eventually,EventuallyWithT, andNeverwith details on best practicesrequire.*_codegento respect source code links like in// Also see [Eventually] ...and to reuse the last string parameter as error message (updated a few code examples to comply)MockT.FailedtoMockT.Failed()to better comply withtestinginterfaces and for allowing to reuse theCollectTimplementation for testing tests.Motivation
require.Fail, ort.FailNow(), or a similar function inside theEventuallycondition, or when the condition does not return due to any indirect call toruntime.Goexit, thech <- conditionwill block and cause the tick loop to hang until the timeout is reached.Related issues
Eventuallyhangs on failed/abortedcondition#1810Tests
TestPanic=1)Discussion
"Condition exited unexpectedly""Condition never satisfied"as used for the timeout case, to not break existing external test analysis approaches."Condition panicked"to educate developers what actually happened. Out of scope for this change, could be av2feature.Impact
(Our) Go developers tend to write
require.NoErroror similar on the outertinside theEventuallycondition, causing the tests to hang. This regularly creates confusion. With this change the test behavior becomes more explicit. Arequire.Fail(t)or similar inside theEventuallycondition will just behave as if called outside: make the test fail, but without hanging the test. "As a developer" this is how I would expect my test tests to fail.Just looking at our internal codebase, I can see hundreds of tests that will fail fast with this change. Globally, I expect to save thousands of hours of wasted compute caused by the hanging tests.
The more explicit error will better educate developers what is wrong in their code. The fixed
Neverbehavior will reveal hidden issues. And the safe use ofrequire.*in the condition functions will make 1000s of existing test no longer violate the best practices.Risk Assessment
Eventuallyexits faster. This may cause some timing issues, but the core logic stays the same -> low riskEventuallyWithTdetects external exits. Logic on thecollectstays as is. External exits through the outertwill just fail fast. Before,EventuallyWithTwould succeed (wrongly) and only the outer test would fail. Now both will fail. -> low riskNeverreports more failures. It exits with an error now and does not block anymore. Before, it would block and never check the condition anymore, which may be satisfied in the meantime, and then Never would wrongly report a success. This will reveal hidden satisfy bugs -> low riskMockT.Failed(). MockT is not used outside of this repo (GH search + Sourcegraph search) -> low risk