Skip to content

Commit e7ea525

Browse files
WebAssembly Support
WASI does not have thread spawning method yet, but the existing implementation blocks threads to wait async test cases synchronously. This commit introduced a new waiter method for running async test cases in single-threaded WASI environments, enabled by USE_SWIFT_CONCURRENCY_WAITER flag. With the new waiter, `XCTMain` is async runs the given test suites without blocking the thread by bypassing some synchronous public APIs like `XCTest.perform` and `XCTest.run`. This ignores those APIs even if they are overridden by user-defined subclasses, so it's not 100% compatible with the existing XCTest APIs. This is a trade-off to support async test execution in single-threaded environments, but it should be fine because the APIs are seldom overridden by user code.
1 parent be9cfd4 commit e7ea525

File tree

12 files changed

+384
-47
lines changed

12 files changed

+384
-47
lines changed

CMakeLists.txt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@ project(XCTest LANGUAGES Swift)
88
option(BUILD_SHARED_LIBS "Build shared libraries" ON)
99
option(USE_FOUNDATION_FRAMEWORK "Use Foundation.framework on Darwin" NO)
1010

11-
if(NOT CMAKE_SYSTEM_NAME STREQUAL Darwin)
11+
set(USE_SWIFT_CONCURRENCY_WAITER_default NO)
12+
13+
if(CMAKE_SYSTEM_PROCESSOR STREQUAL wasm32)
14+
set(USE_SWIFT_CONCURRENCY_WAITER_default ON)
15+
endif()
16+
17+
option(USE_SWIFT_CONCURRENCY_WAITER "Use Swift Concurrency-based waiter implementation" "${USE_SWIFT_CONCURRENCY_WAITER_default}")
18+
19+
if(NOT CMAKE_SYSTEM_NAME STREQUAL Darwin AND NOT USE_SWIFT_CONCURRENCY_WAITER)
1220
find_package(dispatch CONFIG REQUIRED)
1321
find_package(Foundation CONFIG REQUIRED)
1422
endif()
@@ -30,6 +38,7 @@ add_library(XCTest
3038
Sources/XCTest/Private/WaiterManager.swift
3139
Sources/XCTest/Private/IgnoredErrors.swift
3240
Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift
41+
Sources/XCTest/Private/DispatchShims.swift
3342
Sources/XCTest/Public/XCTestRun.swift
3443
Sources/XCTest/Public/XCTestMain.swift
3544
Sources/XCTest/Public/XCTestCase.swift
@@ -49,6 +58,12 @@ add_library(XCTest
4958
Sources/XCTest/Public/Asynchronous/XCTWaiter.swift
5059
Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift
5160
Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift)
61+
62+
if(USE_SWIFT_CONCURRENCY_WAITER)
63+
target_compile_definitions(XCTest PRIVATE
64+
USE_SWIFT_CONCURRENCY_WAITER)
65+
endif()
66+
5267
if(USE_FOUNDATION_FRAMEWORK)
5368
target_compile_definitions(XCTest PRIVATE
5469
USE_FOUNDATION_FRAMEWORK)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See http://swift.org/LICENSE.txt for license information
7+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
8+
//
9+
//
10+
// NoThreadDispatchShims.swift
11+
//
12+
13+
// This file is a shim for platforms that don't have libdispatch and do assume a single-threaded environment.
14+
15+
// NOTE: We can't use use `#if canImport(Dispatch)` because Dispatch Clang module is placed directly in the resource
16+
// directory, and not split into target-specific directories. This means that the module is always available, even on
17+
// platforms that don't have libdispatch. Thus, we need to check for the actual platform.
18+
#if os(WASI)
19+
20+
/// No-op shim function
21+
func dispatchPrecondition(condition: DispatchPredicate) {}
22+
23+
struct DispatchPredicate {
24+
static func onQueue<X>(_: X) -> Self {
25+
return DispatchPredicate()
26+
}
27+
28+
static func notOnQueue<X>(_: X) -> Self {
29+
return DispatchPredicate()
30+
}
31+
}
32+
33+
extension XCTWaiter {
34+
/// Single-threaded queue without any actual queueing
35+
struct DispatchQueue {
36+
init(label: String) {}
37+
38+
func sync<T>(_ body: () -> T) -> T {
39+
body()
40+
}
41+
func async(_ body: @escaping () -> Void) {
42+
body()
43+
}
44+
}
45+
}
46+
47+
#endif

Sources/XCTest/Private/WaiterManager.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
//
1010
// WaiterManager.swift
1111
//
12+
#if !USE_SWIFT_CONCURRENCY_WAITER
1213

1314
internal protocol ManageableWaiter: AnyObject, Equatable {
1415
var isFinished: Bool { get }
@@ -143,3 +144,5 @@ internal final class WaiterManager<WaiterType: ManageableWaiter> : NSObject {
143144
}
144145

145146
}
147+
148+
#endif

Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
// XCTNSNotificationExpectation.swift
1111
//
1212

13+
#if !USE_SWIFT_CONCURRENCY_WAITER
14+
1315
/// Expectation subclass for waiting on a condition defined by a Foundation Notification instance.
1416
open class XCTNSNotificationExpectation: XCTestExpectation {
1517

@@ -114,3 +116,5 @@ open class XCTNSNotificationExpectation: XCTestExpectation {
114116
/// - SeeAlso: `XCTNSNotificationExpectation.handler`
115117
@available(*, deprecated, renamed: "XCTNSNotificationExpectation.Handler")
116118
public typealias XCNotificationExpectationHandler = XCTNSNotificationExpectation.Handler
119+
120+
#endif

Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
// XCTNSPredicateExpectation.swift
1111
//
1212

13+
#if !USE_SWIFT_CONCURRENCY_WAITER
14+
1315
/// Expectation subclass for waiting on a condition defined by an NSPredicate and an optional object.
1416
open class XCTNSPredicateExpectation: XCTestExpectation {
1517

@@ -133,3 +135,4 @@ open class XCTNSPredicateExpectation: XCTestExpectation {
133135
/// - SeeAlso: `XCTNSPredicateExpectation.handler`
134136
@available(*, deprecated, renamed: "XCTNSPredicateExpectation.Handler")
135137
public typealias XCPredicateExpectationHandler = XCTNSPredicateExpectation.Handler
138+
#endif

Sources/XCTest/Public/Asynchronous/XCTWaiter.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ open class XCTWaiter {
117117
private var state = State.ready
118118
internal var timeout: TimeInterval = 0
119119
internal var waitSourceLocation: SourceLocation?
120+
#if !USE_SWIFT_CONCURRENCY_WAITER
120121
private weak var manager: WaiterManager<XCTWaiter>?
122+
#endif
121123
private var runLoop: RunLoop?
122124

123125
private weak var _delegate: XCTWaiterDelegate?
@@ -187,9 +189,16 @@ open class XCTWaiter {
187189
/// these environments. To ensure compatibility of tests between
188190
/// swift-corelibs-xctest and Apple XCTest, it is not recommended to pass
189191
/// explicit values for `file` and `line`.
192+
#if USE_SWIFT_CONCURRENCY_WAITER
193+
@available(*, unavailable, message: "Expectation-based waiting is not available when using the Swift concurrency waiter.")
194+
#else
190195
@available(*, noasync, message: "Use await fulfillment(of:timeout:enforceOrder:) instead.")
196+
#endif
191197
@discardableResult
192198
open func wait(for expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) -> Result {
199+
#if USE_SWIFT_CONCURRENCY_WAITER
200+
fatalError("This method is not available when using the Swift concurrency waiter.")
201+
#else
193202
precondition(Set(expectations).count == expectations.count, "API violation - each expectation can appear only once in the 'expectations' parameter.")
194203

195204
self.timeout = timeout
@@ -251,6 +260,7 @@ open class XCTWaiter {
251260
}
252261

253262
return result
263+
#endif
254264
}
255265

256266
/// Wait on an array of expectations for up to the specified timeout, and optionally specify whether they
@@ -276,9 +286,16 @@ open class XCTWaiter {
276286
/// these environments. To ensure compatibility of tests between
277287
/// swift-corelibs-xctest and Apple XCTest, it is not recommended to pass
278288
/// explicit values for `file` and `line`.
289+
#if USE_SWIFT_CONCURRENCY_WAITER
290+
@available(*, unavailable, message: "Expectation-based waiting is not available when using the Swift concurrency waiter.")
291+
#else
279292
@available(macOS 12.0, *)
293+
#endif
280294
@discardableResult
281295
open func fulfillment(of expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) async -> Result {
296+
#if USE_SWIFT_CONCURRENCY_WAITER
297+
fatalError("This method is not available when using the Swift concurrency waiter.")
298+
#else
282299
return await withCheckedContinuation { continuation in
283300
// This function operates by blocking a background thread instead of one owned by libdispatch or by the
284301
// Swift runtime (as used by Swift concurrency.) To ensure we use a thread owned by neither subsystem, use
@@ -288,6 +305,7 @@ open class XCTWaiter {
288305
continuation.resume(returning: result)
289306
}
290307
}
308+
#endif
291309
}
292310

293311
/// Convenience API to create an XCTWaiter which then waits on an array of expectations for up to the specified timeout, and optionally specify whether they
@@ -306,9 +324,17 @@ open class XCTWaiter {
306324
/// expectations are not fulfilled before the given timeout. Default is the line
307325
/// number of the call to this method in the calling file. It is rare to
308326
/// provide this parameter when calling this method.
327+
#if USE_SWIFT_CONCURRENCY_WAITER
328+
@available(*, unavailable, message: "Expectation-based waiting is not available when using the Swift concurrency waiter.")
329+
#else
309330
@available(*, noasync, message: "Use await fulfillment(of:timeout:enforceOrder:) instead.")
331+
#endif
310332
open class func wait(for expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) -> Result {
333+
#if USE_SWIFT_CONCURRENCY_WAITER
334+
fatalError("This method is not available when using the Swift concurrency waiter.")
335+
#else
311336
return XCTWaiter().wait(for: expectations, timeout: timeout, enforceOrder: enforceOrder, file: file, line: line)
337+
#endif
312338
}
313339

314340
/// Convenience API to create an XCTWaiter which then waits on an array of expectations for up to the specified timeout, and optionally specify whether they
@@ -327,9 +353,17 @@ open class XCTWaiter {
327353
/// expectations are not fulfilled before the given timeout. Default is the line
328354
/// number of the call to this method in the calling file. It is rare to
329355
/// provide this parameter when calling this method.
356+
#if USE_SWIFT_CONCURRENCY_WAITER
357+
@available(*, unavailable, message: "Expectation-based waiting is not available when using the Swift concurrency waiter.")
358+
#else
330359
@available(macOS 12.0, *)
360+
#endif
331361
open class func fulfillment(of expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) async -> Result {
362+
#if USE_SWIFT_CONCURRENCY_WAITER
363+
fatalError("This method is not available when using the Swift concurrency waiter.")
364+
#else
332365
return await XCTWaiter().fulfillment(of: expectations, timeout: timeout, enforceOrder: enforceOrder, file: file, line: line)
366+
#endif
333367
}
334368

335369
deinit {
@@ -338,6 +372,7 @@ open class XCTWaiter {
338372
}
339373
}
340374

375+
#if !USE_SWIFT_CONCURRENCY_WAITER
341376
private func queue_configureExpectations(_ expectations: [XCTestExpectation]) {
342377
dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
343378

@@ -413,9 +448,11 @@ open class XCTWaiter {
413448
queue_validateExpectationFulfillment(dueToTimeout: false)
414449
}
415450
}
451+
#endif
416452

417453
}
418454

455+
#if !USE_SWIFT_CONCURRENCY_WAITER
419456
private extension XCTWaiter {
420457
func primitiveWait(using runLoop: RunLoop, duration timeout: TimeInterval) {
421458
// The contract for `primitiveWait(for:)` explicitly allows waiting for a shorter period than requested
@@ -436,6 +473,7 @@ private extension XCTWaiter {
436473
#endif
437474
}
438475
}
476+
#endif
439477

440478
extension XCTWaiter: Equatable {
441479
public static func == (lhs: XCTWaiter, rhs: XCTWaiter) -> Bool {
@@ -453,6 +491,7 @@ extension XCTWaiter: CustomStringConvertible {
453491
}
454492
}
455493

494+
#if !USE_SWIFT_CONCURRENCY_WAITER
456495
extension XCTWaiter: ManageableWaiter {
457496
var isFinished: Bool {
458497
return XCTWaiter.subsystemQueue.sync {
@@ -479,3 +518,5 @@ extension XCTWaiter: ManageableWaiter {
479518
}
480519
}
481520
}
521+
522+
#endif

Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
// Methods on XCTestCase for testing asynchronous operations
1212
//
1313

14+
#if !USE_SWIFT_CONCURRENCY_WAITER
15+
1416
public extension XCTestCase {
1517

1618
/// Creates a point of synchronization in the flow of a test. Only one
@@ -265,3 +267,4 @@ internal extension XCTestCase {
265267
expected: false)
266268
}
267269
}
270+
#endif

Sources/XCTest/Public/XCAbstractTest.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,43 @@ open class XCTest {
3636
/// testRunClass. If the test has not yet been run, this will be nil.
3737
open private(set) var testRun: XCTestRun? = nil
3838

39+
internal var performTask: Task<Void, Never>?
40+
41+
#if USE_SWIFT_CONCURRENCY_WAITER
42+
internal func _performAsync(_ run: XCTestRun) async {
43+
fatalError("Must be overridden by subclasses.")
44+
}
45+
internal func _runAsync() async {
46+
guard let testRunType = testRunClass as? XCTestRun.Type else {
47+
fatalError("XCTest.testRunClass must be a kind of XCTestRun.")
48+
}
49+
testRun = testRunType.init(test: self)
50+
await _performAsync(testRun!)
51+
}
52+
#endif
53+
3954
/// The method through which tests are executed. Must be overridden by
4055
/// subclasses.
56+
#if USE_SWIFT_CONCURRENCY_WAITER
57+
@available(*, unavailable)
58+
#endif
4159
open func perform(_ run: XCTestRun) {
4260
fatalError("Must be overridden by subclasses.")
4361
}
4462

4563
/// Creates an instance of the `testRunClass` and passes it as a parameter
4664
/// to `perform()`.
65+
#if USE_SWIFT_CONCURRENCY_WAITER
66+
@available(*, unavailable)
67+
#endif
4768
open func run() {
69+
#if !USE_SWIFT_CONCURRENCY_WAITER
4870
guard let testRunType = testRunClass as? XCTestRun.Type else {
4971
fatalError("XCTest.testRunClass must be a kind of XCTestRun.")
5072
}
5173
testRun = testRunType.init(test: self)
5274
perform(testRun!)
75+
#endif
5376
}
5477

5578
/// Async setup method called before the invocation of `setUpWithError` for each test method in the class.

0 commit comments

Comments
 (0)