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

feat: Add T.Context() #77

Merged
merged 3 commits into from
Feb 20, 2025
Merged

feat: Add T.Context() #77

merged 3 commits into from
Feb 20, 2025

Conversation

abhinav
Copy link
Contributor

@abhinav abhinav commented Feb 16, 2025

Background:
Go 1.24 adds a Context() method to testing.TB that returns
a context bound to the current test's lifetime.
The context becomes invalid before T.Cleanup functions are run.

Details:
This change adds a similar Context() method to rapid.T,
except this context is only valid for the duration of one iteration
of a rapid check.

For rapid.Custom, it's only valid for that call to the custom function.

Implementation notes:
The context is created lazily on the *rapid.T as requested,
using the testing.TB.Context as the base context if available.
A new unexported cleanup method on *rapid.T is called in strategic places
to cancel the context (if any) when its scope ends.

Future ideas:
The cleanup function added here can be expanded in the future
in support of a Cleanup method (#62).

*Context*:
Go 1.24 adds a `Context()` method to `testing.TB` that returns
a context bound to the current test's lifetime.
The context becomes invalid _before_ `T.Cleanup` functions are run.

*Details*:
This change adds a similar `Context()` method to `rapid.T`,
except this context is only valid for the duration of one iteration
of a rapid check.

*Implementation notes*:
This changes `newT` to return a `cancel` function
instead of just adding a `cancel` method to `T`
to ensure that all callers of `newT` remember to call `cancel`.

This also uses IIFEs (immediately-invoked function expressions)
in a couple places to rely on well-timed `defer` calls for cleanup
instead of manually calling `cancel`.

*Future work*:
The logic added in this commit will make it relatively straightforward
to add a `Cleanup` method (flyingmutant#62).
@flyingmutant
Copy link
Owner

I think the implementation can be greatly simplified by both creating the context and deferring the cancel inside checkOnce. It would be both much simpler and obviously correct -- to the extent that I am not sure any tests would be necessary at all.

@abhinav
Copy link
Contributor Author

abhinav commented Feb 17, 2025

That makes sense; I was hoping to use the cancel() machinery to also implement Cleanup later.
I think that might still work without leaking the internals into checkOnce if I do something like this:

func checkOnce(...) {
  // ...
  cleanup := t.prepare()
  defer cleanup()
  prop(t)
  // ...
}

Where prepare and cleanup assign and cancel t.ctx as needed.

Would that work/am I missing anything with it?

@flyingmutant
Copy link
Owner

Yes, I think something like that should work. Additionally, we can make context creation lazy, only after t.Context() was called. This way, we can avoid context creation overhead for the majority of cases where it is unnecessary (and t.prepare() would not be needed, only t.cleanup()).

@abhinav abhinav marked this pull request as draft February 18, 2025 16:53
Per GitHub comment, delete the `cancel` return value from newT,
instead add a single `cleanup` method.

The method is called for cleanup in three places:

- checkOnce: this is per property
- maybeValue: this is per Custom generator function call
- example: this is per Example call

Context is now initialized lazily:
if there isn't one, it is created.
@abhinav abhinav marked this pull request as ready for review February 20, 2025 01:50
@abhinav
Copy link
Contributor Author

abhinav commented Feb 20, 2025

Changed as requested. This is much less disruptive, although defer t.cleanup was needed in three places to get desired behavior out of Custom generators and Example. I've kept the tests around as they verify this behavior.

To clarify, the behavior we get is:

  • rapid.Check(prop): context requested inside the property is only valid for that iteration of prop.
  • rapid.Custom(gen): context requested inside generator function is valid only for that call to gen. Outer property function that called rapid.Custom doesn't affect the lifetime of the context inside gen.
  • Generator.Example(seed): context can only be requested for custom generators (I think), and same rule as rapid.Check(prop) applies

If this is not the desired behavior, we'll need to delete one or more of the t.cleanup calls.

engine.go Outdated
t.mu.RLock()
if t.ctx != nil {
t.mu.RUnlock()
return t.ctx
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is racy: after the unlock t.ctx may already be nil.

Copy link
Contributor Author

@abhinav abhinav Feb 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦 derp. I had this right the first time around and decided to clean up some local variables for unknown reasons.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix pushed

@flyingmutant flyingmutant merged commit c3c5e3c into flyingmutant:master Feb 20, 2025
14 checks passed
@flyingmutant
Copy link
Owner

Thanks, merged!

@abhinav abhinav deleted the t-ctx branch February 20, 2025 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants