Skip to content

Commit 503c44e

Browse files
committed
Allow arguments to tests to be gathered asynchronously.
This change allows a developer to specify an argument to a test that is the result of an asynchronous function/operation. For example: ```swift @test(arguments: await downloadFilesFromInternet()) func readFile(file: File) { ... } ``` These arguments are resolved at runtime during the test planning stage.
1 parent f759bd4 commit 503c44e

File tree

6 files changed

+88
-75
lines changed

6 files changed

+88
-75
lines changed

Sources/Testing/Test+Macro.swift

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ public func __invokeXCTestCaseMethod<T>(
542542
@_alwaysEmitConformanceMetadata
543543
public protocol __TestContainer {
544544
/// The set of tests contained by this type.
545-
static var __tests: [Test] { get }
545+
static var __tests: [Test] { get async }
546546
}
547547

548548
extension Test {
@@ -554,38 +554,42 @@ extension Test {
554554
///
555555
/// The order of values in this sequence is unspecified.
556556
static var all: some Sequence<Test> {
557-
// Convert the raw sequence of tests to a dictionary keyed by ID.
558-
var result = testsByID(_all)
557+
get async {
558+
// Convert the raw sequence of tests to a dictionary keyed by ID.
559+
var result = await testsByID(_all)
559560

560-
// Ensure test suite types that don't have the @Suite attribute are still
561-
// represented in the result.
562-
_synthesizeSuiteTypes(into: &result)
561+
// Ensure test suite types that don't have the @Suite attribute are still
562+
// represented in the result.
563+
_synthesizeSuiteTypes(into: &result)
563564

564-
return result.values
565+
return result.values
566+
}
565567
}
566568

567569
/// All available ``Test`` instances in the process, according to the runtime.
568570
///
569571
/// The order of values in this sequence is unspecified. This sequence may
570572
/// contain duplicates; callers should use ``all`` instead.
571-
private static var _all: some Sequence<Test> {
572-
var result = [Self]()
573-
574-
withUnsafeMutablePointer(to: &result) { result in
575-
swt_enumerateTypes({ typeName, _ in
576-
// strstr() lets us avoid copying either string before comparing.
577-
Self._testContainerTypeNameMagic.withCString { testContainerTypeNameMagic in
578-
nil != strstr(typeName, testContainerTypeNameMagic)
579-
}
580-
}, /*typeEnumerator:*/ { type, context in
581-
if let context, let type = unsafeBitCast(type, to: Any.Type.self) as? any __TestContainer.Type {
582-
let result = context.assumingMemoryBound(to: Array<Self>.self)
583-
result.pointee.append(contentsOf: type.__tests)
584-
}
585-
}, result)
573+
private static var _all: some Sequence<Self> {
574+
get async {
575+
await withTaskGroup(of: [Self].self) { taskGroup in
576+
swt_enumerateTypes({ typeName, _ in
577+
// strstr() lets us avoid copying either string before comparing.
578+
Self._testContainerTypeNameMagic.withCString { testContainerTypeNameMagic in
579+
nil != strstr(typeName, testContainerTypeNameMagic)
580+
}
581+
}, /*typeEnumerator:*/ { type, context in
582+
if let context, let type = unsafeBitCast(type, to: Any.Type.self) as? any __TestContainer.Type {
583+
let taskGroup = context.assumingMemoryBound(to: ThrowingTaskGroup<[Self], any Error>.self)
584+
taskGroup.pointee.addTask {
585+
return await type.__tests
586+
}
587+
}
588+
}, &taskGroup)
589+
590+
return await taskGroup.reduce(into: [], +=)
591+
}
586592
}
587-
588-
return result
589593
}
590594

591595
/// Create a dictionary mapping the IDs of a sequence of tests to those tests.

Sources/TestingMacros/TestDeclarationMacro.swift

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -480,16 +480,18 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
480480
@available(*, unavailable, message: "This type is an implementation detail of the testing library. It cannot be used directly.")
481481
@available(*, deprecated)
482482
@frozen public enum \(enumName): Testing.__TestContainer {
483-
public static var __tests: [Testing.Test] {[
484-
.__function(
485-
named: \(literal: functionDecl.completeName),
486-
in: \(typealiasExpr),
487-
xcTestCompatibleSelector: \(selectorExpr ?? "nil"),
488-
\(raw: attributeInfo.functionArgumentList(in: context)),
489-
parameters: \(raw: functionDecl.testFunctionParameterList),
490-
testFunction: \(thunkDecl.name)
491-
)
492-
]}
483+
public static var __tests: [Testing.Test] {
484+
get async {[
485+
.__function(
486+
named: \(literal: functionDecl.completeName),
487+
in: \(typealiasExpr),
488+
xcTestCompatibleSelector: \(selectorExpr ?? "nil"),
489+
\(raw: attributeInfo.functionArgumentList(in: context)),
490+
parameters: \(raw: functionDecl.testFunctionParameterList),
491+
testFunction: \(thunkDecl.name)
492+
)
493+
]}
494+
}
493495
}
494496
"""
495497
)

Tests/TestingTests/MiscellaneousTests.swift

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -172,28 +172,35 @@ struct TestsWithStaticMemberAccessBySelfKeyword {
172172
@Test(.hidden, arguments: [0]) func A(🙃: Int) {}
173173
@Test(.hidden, arguments: [0]) func A(🙂: Int) {}
174174

175+
@Suite(.hidden)
176+
struct TestsWithAsyncArguments {
177+
static func asyncCollection() async -> [Int] { [] }
178+
179+
@Test(.hidden, arguments: await asyncCollection()) func f(i: Int) {}
180+
}
181+
175182
@Suite("Miscellaneous tests")
176183
struct MiscellaneousTests {
177184
@Test("Free function's name")
178185
func unnamedFreeFunctionTest() async throws {
179-
let testFunction = try #require(Test.all.first(where: { $0.name.contains("freeSyncFunction") }))
186+
let testFunction = try #require(await Test.all.first(where: { $0.name.contains("freeSyncFunction") }))
180187
#expect(testFunction.name == "freeSyncFunction()")
181188
}
182189

183190
@Test("Test suite type's name")
184191
func unnamedMemberFunctionTest() async throws {
185-
let testType = try #require(test(for: SendableTests.self))
192+
let testType = try #require(await test(for: SendableTests.self))
186193
#expect(testType.name == "SendableTests")
187194
}
188195

189196
@Test("Free function has custom display name")
190197
func namedFreeFunctionTest() async throws {
191-
#expect(Test.all.first { $0.displayName == "Named Free Sync Function" && !$0.isSuite && $0.containingType == nil } != nil)
198+
#expect(await Test.all.first { $0.displayName == "Named Free Sync Function" && !$0.isSuite && $0.containingType == nil } != nil)
192199
}
193200

194201
@Test("Member function has custom display name")
195202
func namedMemberFunctionTest() async throws {
196-
let testType = try #require(test(for: NamedSendableTests.self))
203+
let testType = try #require(await test(for: NamedSendableTests.self))
197204
#expect(testType.displayName == "Named Sendable test type")
198205
}
199206

@@ -301,53 +308,53 @@ struct MiscellaneousTests {
301308
@Test("Test.underestimatedCaseCount property")
302309
func underestimatedCaseCount() async throws {
303310
do {
304-
let test = try #require(testFunction(named: "parameterized(i:)", in: NonSendableTests.self))
311+
let test = try #require(await testFunction(named: "parameterized(i:)", in: NonSendableTests.self))
305312
#expect(test.underestimatedCaseCount == FixtureData.zeroUpTo100.count)
306313
}
307314
do {
308-
let test = try #require(testFunction(named: "parameterized2(i:j:)", in: NonSendableTests.self))
315+
let test = try #require(await testFunction(named: "parameterized2(i:j:)", in: NonSendableTests.self))
309316
#expect(test.underestimatedCaseCount == FixtureData.zeroUpTo100.count * FixtureData.smallStringArray.count)
310317
}
311318
do {
312-
let test = try #require(testFunction(named: "parameterized(i:)", in: SendableTests.self))
319+
let test = try #require(await testFunction(named: "parameterized(i:)", in: SendableTests.self))
313320
#expect(test.underestimatedCaseCount == FixtureData.zeroUpTo100.count)
314321
}
315322
#if !SWT_NO_GLOBAL_ACTORS
316323
do {
317-
let test = try #require(testFunction(named: "parameterized(i:)", in: MainActorIsolatedTests.self))
324+
let test = try #require(await testFunction(named: "parameterized(i:)", in: MainActorIsolatedTests.self))
318325
#expect(test.underestimatedCaseCount == FixtureData.zeroUpTo100.count)
319326
}
320327
do {
321-
let test = try #require(testFunction(named: "parameterizedNonisolated(i:)", in: MainActorIsolatedTests.self))
328+
let test = try #require(await testFunction(named: "parameterizedNonisolated(i:)", in: MainActorIsolatedTests.self))
322329
#expect(test.underestimatedCaseCount == FixtureData.zeroUpTo100.count)
323330
}
324331
#endif
325332

326333
do {
327-
let thisTest = try #require(testFunction(named: "succeeds()", in: SendableTests.self))
334+
let thisTest = try #require(await testFunction(named: "succeeds()", in: SendableTests.self))
328335
#expect(thisTest.underestimatedCaseCount == 1)
329336
}
330337
do {
331-
let thisTest = try #require(test(for: SendableTests.self))
338+
let thisTest = try #require(await test(for: SendableTests.self))
332339
#expect(thisTest.underestimatedCaseCount == nil)
333340
}
334341
}
335342

336343
@Test("Test.parameters property")
337344
func parametersProperty() async throws {
338345
do {
339-
let theTest = try #require(test(for: SendableTests.self))
346+
let theTest = try #require(await test(for: SendableTests.self))
340347
#expect(theTest.parameters == nil)
341348
}
342349

343350
do {
344-
let test = try #require(testFunction(named: "succeeds()", in: SendableTests.self))
351+
let test = try #require(await testFunction(named: "succeeds()", in: SendableTests.self))
345352
let parameters = try #require(test.parameters)
346353
#expect(parameters.isEmpty)
347354
} catch {}
348355

349356
do {
350-
let test = try #require(testFunction(named: "parameterized(i:)", in: NonSendableTests.self))
357+
let test = try #require(await testFunction(named: "parameterized(i:)", in: NonSendableTests.self))
351358
let parameters = try #require(test.parameters)
352359
#expect(parameters.count == 1)
353360
let firstParameter = try #require(parameters.first)
@@ -356,7 +363,7 @@ struct MiscellaneousTests {
356363
} catch {}
357364

358365
do {
359-
let test = try #require(testFunction(named: "parameterized2(i:j:)", in: NonSendableTests.self))
366+
let test = try #require(await testFunction(named: "parameterized2(i:j:)", in: NonSendableTests.self))
360367
let parameters = try #require(test.parameters)
361368
#expect(parameters.count == 2)
362369
let firstParameter = try #require(parameters.first)
@@ -476,7 +483,7 @@ struct MiscellaneousTests {
476483
let line = 12345
477484
let column = 67890
478485
let sourceLocation = SourceLocation(fileID: fileID, filePath: filePath, line: line, column: column)
479-
let testFunction = Test.__function(named: "myTestFunction()", in: nil, xcTestCompatibleSelector: nil, displayName: nil, traits: [], sourceLocation: sourceLocation) {}
486+
let testFunction = await Test.__function(named: "myTestFunction()", in: nil, xcTestCompatibleSelector: nil, displayName: nil, traits: [], sourceLocation: sourceLocation) {}
480487
#expect(String(describing: testFunction.id) == "Module/myTestFunction()/Y.swift:12345:67890")
481488
}
482489

Tests/TestingTests/PlanTests.swift

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
struct PlanTests {
1515
@Test("Selected tests")
1616
func selectedTests() async throws {
17-
let outerTestType = try #require(test(for: SendableTests.self))
18-
let testA = try #require(testFunction(named: "succeeds()", in: SendableTests.self))
19-
let innerTestType = try #require(test(for: SendableTests.NestedSendableTests.self))
20-
let testB = try #require(testFunction(named: "succeeds()", in: SendableTests.NestedSendableTests.self))
17+
let outerTestType = try #require(await test(for: SendableTests.self))
18+
let testA = try #require(await testFunction(named: "succeeds()", in: SendableTests.self))
19+
let innerTestType = try #require(await test(for: SendableTests.NestedSendableTests.self))
20+
let testB = try #require(await testFunction(named: "succeeds()", in: SendableTests.NestedSendableTests.self))
2121

2222
let tests = [
2323
outerTestType,
@@ -38,10 +38,10 @@ struct PlanTests {
3838

3939
@Test("Multiple selected tests")
4040
func multipleSelectedTests() async throws {
41-
let outerTestType = try #require(test(for: SendableTests.self))
42-
let testA = try #require(testFunction(named: "succeeds()", in: SendableTests.self))
43-
let innerTestType = try #require(test(for: SendableTests.NestedSendableTests.self))
44-
let testB = try #require(testFunction(named: "succeeds()", in: SendableTests.NestedSendableTests.self))
41+
let outerTestType = try #require(await test(for: SendableTests.self))
42+
let testA = try #require(await testFunction(named: "succeeds()", in: SendableTests.self))
43+
let innerTestType = try #require(await test(for: SendableTests.NestedSendableTests.self))
44+
let testB = try #require(await testFunction(named: "succeeds()", in: SendableTests.NestedSendableTests.self))
4545

4646
let tests = [
4747
outerTestType,
@@ -63,9 +63,9 @@ struct PlanTests {
6363

6464
@Test("Recursive trait application")
6565
func recursiveTraitApplication() async throws {
66-
let outerTestType = try #require(test(for: OuterTest.self))
66+
let outerTestType = try #require(await test(for: OuterTest.self))
6767
// Intentionally omitting intermediate tests here...
68-
let deeplyNestedTest = try #require(testFunction(named: "example()", in: OuterTest.IntermediateType.InnerTest.self))
68+
let deeplyNestedTest = try #require(await testFunction(named: "example()", in: OuterTest.IntermediateType.InnerTest.self))
6969

7070
let tests = [outerTestType, deeplyNestedTest]
7171

@@ -80,10 +80,10 @@ struct PlanTests {
8080

8181
@Test("Relative order of recursively applied traits")
8282
func recursiveTraitOrder() async throws {
83-
let testSuiteA = try #require(test(for: RelativeTraitOrderingTests.A.self))
84-
let testSuiteB = try #require(test(for: RelativeTraitOrderingTests.A.B.self))
85-
let testSuiteC = try #require(test(for: RelativeTraitOrderingTests.A.B.C.self))
86-
let testFuncX = try #require(testFunction(named: "x()", in: RelativeTraitOrderingTests.A.B.C.self))
83+
let testSuiteA = try #require(await test(for: RelativeTraitOrderingTests.A.self))
84+
let testSuiteB = try #require(await test(for: RelativeTraitOrderingTests.A.B.self))
85+
let testSuiteC = try #require(await test(for: RelativeTraitOrderingTests.A.B.C.self))
86+
let testFuncX = try #require(await testFunction(named: "x()", in: RelativeTraitOrderingTests.A.B.C.self))
8787

8888
let tests = [testSuiteA, testSuiteB, testSuiteC, testFuncX]
8989

Tests/TestingTests/RunnerTests.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,8 @@ final class RunnerTests: XCTestCase {
246246
}
247247

248248
func testConditionTraitsAreEvaluatedOutermostToInnermost() async throws {
249-
let testSuite = try #require(test(for: NeverRunTests.self))
250-
let testFunc = try #require(testFunction(named: "duelingConditions()", in: NeverRunTests.self))
249+
let testSuite = try #require(await test(for: NeverRunTests.self))
250+
let testFunc = try #require(await testFunction(named: "duelingConditions()", in: NeverRunTests.self))
251251

252252
var configuration = Configuration()
253253
configuration.selectedTests = .init(testIDs: [testSuite.id])
@@ -313,11 +313,11 @@ final class RunnerTests: XCTestCase {
313313
}
314314

315315
func testHardCodedPlan() async throws {
316-
let tests = try [
317-
XCTUnwrap(testFunction(named: "succeeds()", in: SendableTests.self)),
318-
XCTUnwrap(testFunction(named: "succeedsAsync()", in: SendableTests.self)),
319-
XCTUnwrap(testFunction(named: "succeeds()", in: SendableTests.NestedSendableTests.self)),
320-
]
316+
let tests = try await [
317+
testFunction(named: "succeeds()", in: SendableTests.self),
318+
testFunction(named: "succeedsAsync()", in: SendableTests.self),
319+
testFunction(named: "succeeds()", in: SendableTests.NestedSendableTests.self),
320+
].map { try XCTUnwrap($0) }
321321
let steps: [Runner.Plan.Step] = tests
322322
.map { .init(test: $0, action: .skip()) }
323323
let plan = Runner.Plan(steps: steps)

Tests/TestingTests/TestSupport/TestingAdditions.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import XCTest
2121
///
2222
/// - Returns: The test instance representing the specified type, or `nil` if
2323
/// none is found.
24-
func test(for containingType: Any.Type) -> Test? {
25-
Test.all.first {
24+
func test(for containingType: Any.Type) async -> Test? {
25+
await Test.all.first {
2626
$0.isSuite && $0.containingType == containingType
2727
}
2828
}
@@ -35,8 +35,8 @@ func test(for containingType: Any.Type) -> Test? {
3535
///
3636
/// - Returns: The test instance representing the specified test function, or
3737
/// `nil` if none is found.
38-
func testFunction(named name: String, in containingType: Any.Type) -> Test? {
39-
Test.all.first {
38+
func testFunction(named name: String, in containingType: Any.Type) async -> Test? {
39+
await Test.all.first {
4040
$0.name == name && !$0.isSuite && $0.containingType == containingType
4141
}
4242
}

0 commit comments

Comments
 (0)