Skip to content

TaskLocal test trait#1764

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

TaskLocal test trait#1764
mbrandonw wants to merge 7 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
/// }
/// ```
///
/// - Note: The task local must be defined outside the test target where the trait is used.

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.

Documentation comments should avoid the passive voice per Apple's and Swift's documentation style guides.

Per our own convention, put discussion including callouts after the brief, parameters, returns, and throws sections.

/// Constructs a trait that overrides a task local value for the duration of a test
/// or suite.
///
/// ```swift

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.

An example like this is probably not necessary, but if you want to include it then it needs to go after a paragraph or two describing the trait in more detail. It won't render well in DocC immediately after the brief.

///
/// 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.

Comment thread Sources/Testing/Traits/TaskLocalTrait.swift
public struct TaskLocalTrait<Value: Sendable>: SuiteTrait, TestScoping, TestTrait {
public var isRecursive: Bool { true }

fileprivate let taskLocal: TaskLocal<Value>

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.

Please provide documentation comments for these two stored properties. As well, because this is a struct, please declare them var rather than let.

@grynspan

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.

/// - Note: You must define the task local outside the test target where the trait is used.
public static func taskLocal<Value: Sendable>(
_ taskLocal: TaskLocal<Value>,
_ value: Value

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.

I recommend making the value parameter an @autoclosure so that its evaluation can be deferred until the point where provideScope() is called, if it gets called at all:

Suggested change
_ value: Value
_ value: @autoclosure @escaping @Sendable () throws -> Value

A couple reasons:

  • If the test is skipped (e.g. it has a .disabled trait) the value expression will never be evaluated. That can be useful if the evaluation has observable side effects you only want to occur if the test actually runs, especially if that work is expensive.
  • The value obtained lazily during provideScope() can be released and discarded once the test finishes, which again can be important if the value occupies significant memory or otherwise needs to be deallocated once it's no longer needed.

Making this a lazily-evaluated stored property on TaskLocalTrait would also align it more closely with ConditionTrait, which stores its condition as a closure which gets evaluated lazily during planning.

If you take this change, I recommend documenting those semantics in the DocC for the .taskLocal() function, and discussing the reasoning for using @autoclosure in the accompanying 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.

If you take this change, I recommend documenting those semantics in the DocC for the .taskLocal() function, and discussing the reasoning for using @autoclosure in the accompanying proposal.

I've made the change for @autoclosure, but on this point I'm not sure what to document. I couldn't find any examples in the Swift ecosystem of autoclosures that are documented. Usually the docs treat the argument as a regular value and do not mention the autoclosure at all, including enabled(if:)/disabled(if:).

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.

The aspect I would encourage you to document is the lifetime guarantees that lazy evaluation via @autoclosure will provide. You could say something like,

/// - Parameters:
///   ...
///   - value: The value to bind to `taskLocal`.
///
///     This value will only be evaluated if the test this
//      trait is applied to runs, and it will be unbound from
///     the task local and released once the trait is finished
///     providing scope for the test.

@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 ?

}

#if DEBUG
@TaskLocal var dummyLocal = false

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.

Could you include a DocC comment about why this is here? (It's a convenient place to put it for use in an accompanying test, because it needs to be in a separate module)

Also, style nit: we generally outdent code within top-level #ifs to column 0

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