Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sources/Testing/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ add_library(Testing
Traits/Tags/Tag+Macro.swift
Traits/Tags/Tag+Predefined.swift
Traits/TimeLimitTrait.swift
Traits/TaskLocalTrait.swift
Traits/Trait.swift)
target_link_libraries(Testing PRIVATE
_TestDiscovery
Expand Down
57 changes: 57 additions & 0 deletions Sources/Testing/Traits/TaskLocalTrait.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

extension Trait {
Comment thread
harlanhaskins marked this conversation as resolved.
/// Constructs a trait that binds a task local value for the duration of a test
/// or suite.
///
/// - Parameters:
/// - taskLocal: The task local to bind the value to.
/// - value: The value to set.
///
/// ```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.

/// @Suite(.taskLocal($myValue, 42))
/// struct MyTests {
/// // ...
/// }
/// ```
///
/// - 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.

) -> Self
where Self == TaskLocalTrait<Value> {
Comment thread
grynspan marked this conversation as resolved.
TaskLocalTrait(taskLocal: taskLocal, value: value)
}
}

/// A type that that binds a task local value for the duration of a test or suite.
///
/// To add this trait to a test, use ``Trait/taskLocal(_:_:)``.
public struct TaskLocalTrait<Value: Sendable>: SuiteTrait, TestTrait, TestScoping {
/// This trait's task local.
fileprivate var taskLocal: TaskLocal<Value>

/// This trait's value.
fileprivate var value: Value

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 ?

) async throws {
try await taskLocal.withValue(value, operation: function)
}
}

#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

#endif
22 changes: 22 additions & 0 deletions Tests/TestingTests/Traits/TaskLocalTraitTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing

@Suite("Task Local Tests", .tags(.traitRelated))
struct TaskLocalTests {
@Test(
".taskLocal trait",
.taskLocal($dummyLocal, true)
)
func taskLocalBinding() throws {
#expect(dummyLocal == true)
}
}
Loading