Skip to content

TaskLocal test trait#1764

Open
mbrandonw wants to merge 10 commits into
swiftlang:mainfrom
pointfreeco:task-local-test-trait
Open

TaskLocal test trait#1764
mbrandonw wants to merge 10 commits into
swiftlang:mainfrom
pointfreeco:task-local-test-trait

Conversation

@mbrandonw

Copy link
Copy Markdown

Adds a .taskLocal test trait for overriding task locals in tests

Motivation:

Proposal here: swiftlang/swift-evolution#3329

Checklist:

  • Code and documentation should follow the style of the Style Guide.
  • If public symbols are renamed or modified, DocC references should be updated.

Comment thread Sources/Testing/Traits/TaskLocalTrait.swift
Comment thread Sources/Testing/Traits/TaskLocalTrait.swift Outdated
Comment thread Sources/Testing/Traits/TaskLocalTrait.swift Outdated
///
/// To add this trait to a test, use ``Trait/taskLocal(_:_:)``.
public struct TaskLocalTrait<Value: Sendable>: SuiteTrait, TestScoping, TestTrait {
public var isRecursive: Bool { true }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Because task locals are already implicitly applied recursively to subtasks, there's no need for this property's value to be true. Having it true here just causes us to call withValue repeatedly, which does not have a meaningful effect on test code.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is an interesting question to carefully consider, I think! I feel like it'll be common for users to have situations where it's important that the value be reevaluated and re-bound to the task local for every invocation of each test within a suite that a .taskLocal(...) trait is applied to. For example, if the value contains some state which accumulates or is modified during each test, it's very important that be reset to a fresh state for each one.

For that reason I think the safest default for isRecursive is true, as currently written. Per the semantics of Test Scoping Traits, that will mean that whenever you add one of these traits to a suite, value will be reevaluated (by calling its @autoclosure) and (re-)bound to the task local for the invocation of each test within that suite

That said, there could totally be situations when a user doesn't want this work to be redone for each invocation of each test in a suite, they only want the work to happen once per suite. If we want to support that, we could make isRecursive a parameter of the .taskLocal(...) function, with a default value of true, and let it be controlled that way. I'm not certain we need to allow that in the initial version of this API, though; I'd lean towards leaving it as a Future Direction. And either way, I think it's worth discussing this consideration in the proposal.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

We agree that isRecursive: true is the better default and have pushed those changes here b80766b. I also added a test to show the danger of not making it recursive.

Comment thread Sources/Testing/Traits/TaskLocalTrait.swift
Comment thread Sources/Testing/Traits/TaskLocalTrait.swift Outdated
@grynspan

grynspan commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Additional action items:

  • Sources/Testing/CMakeLists.txt needs to be updated to reference the new file.
  • Some test coverage is needed (we can have a dummy task local in the Testing target for this purpose, guarded with #if DEBUG).

@grynspan grynspan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Off to a great start.

@grynspan grynspan added enhancement New feature or request concurrency 🔀 Swift concurrency/sendability issues public-api Affects public API traits Issues and PRs related to the trait subsystem or built-in traits labels Jun 18, 2026
@grynspan grynspan added this to the Swift 6.5.0 (main) milestone Jun 18, 2026
@grynspan grynspan assigned mbrandonw and unassigned mbrandonw Jun 18, 2026
@grynspan

Copy link
Copy Markdown
Contributor

GitHub's multiple assignee feature is broken, I guess.

Comment thread Sources/Testing/Traits/TaskLocalTrait.swift Outdated
@grynspan

Copy link
Copy Markdown
Contributor

Keep in mind async autoclosures are broken.

public func provideScope(
for test: Test,
testCase: Test.Case?,
performing function: @concurrent () async throws -> Void

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We don't have @concurrent on other provideScope() implementations in the library. The other implementations do have @Sendable though; can we get away with only @Sendable ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think this is @concurrent only because it was taken from a library we maintain with this tool, and that library is NonisolatedNonSendingByDefault and Swift Testing is not (I believe). Will remove and bring back @Sendable as that's the TestScoping requirement.

Comment thread Sources/Testing/Traits/TaskLocalTrait.swift Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

concurrency 🔀 Swift concurrency/sendability issues enhancement New feature or request public-api Affects public API traits Issues and PRs related to the trait subsystem or built-in traits

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants