Skip to content

Add support for WASI platform #478

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 13, 2024
17 changes: 16 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ project(XCTest LANGUAGES Swift)
option(BUILD_SHARED_LIBS "Build shared libraries" ON)
option(USE_FOUNDATION_FRAMEWORK "Use Foundation.framework on Darwin" NO)

if(NOT CMAKE_SYSTEM_NAME STREQUAL Darwin)
set(USE_SWIFT_CONCURRENCY_WAITER_default NO)

if(CMAKE_SYSTEM_PROCESSOR STREQUAL wasm32)
set(USE_SWIFT_CONCURRENCY_WAITER_default ON)
endif()

option(USE_SWIFT_CONCURRENCY_WAITER "Use Swift Concurrency-based waiter implementation" "${USE_SWIFT_CONCURRENCY_WAITER_default}")
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this option work on other platforms besides WASM/WASI? I wonder if that would be a path to improving test coverage for this new code, by running the existing lit-based tests on Darwin or Linux, with this flag turned on?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it works on other platforms, but the test suite doesn't work as is now because the XCTMain is async with the configuration. So we need to adjust those tests, but it's not too hard. Do you want to include those adjustments in this PR?

Copy link
Contributor

Choose a reason for hiding this comment

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

Cool. It's fine for that to come afterwards, but I would like to see it happen!


if(NOT CMAKE_SYSTEM_NAME STREQUAL Darwin AND NOT USE_SWIFT_CONCURRENCY_WAITER)
find_package(dispatch CONFIG REQUIRED)
find_package(Foundation CONFIG REQUIRED)
endif()
Expand All @@ -30,6 +38,7 @@ add_library(XCTest
Sources/XCTest/Private/WaiterManager.swift
Sources/XCTest/Private/IgnoredErrors.swift
Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift
Sources/XCTest/Private/DispatchShims.swift
Sources/XCTest/Public/XCTestRun.swift
Sources/XCTest/Public/XCTestMain.swift
Sources/XCTest/Public/XCTestCase.swift
Expand All @@ -49,6 +58,12 @@ add_library(XCTest
Sources/XCTest/Public/Asynchronous/XCTWaiter.swift
Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift
Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift)

if(USE_SWIFT_CONCURRENCY_WAITER)
target_compile_definitions(XCTest PRIVATE
USE_SWIFT_CONCURRENCY_WAITER)
endif()

if(USE_FOUNDATION_FRAMEWORK)
target_compile_definitions(XCTest PRIVATE
USE_FOUNDATION_FRAMEWORK)
Expand Down
47 changes: 47 additions & 0 deletions Sources/XCTest/Private/DispatchShims.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//
// NoThreadDispatchShims.swift
//

// This file is a shim for platforms that don't have libdispatch and do assume a single-threaded environment.

// NOTE: We can't use use `#if canImport(Dispatch)` because Dispatch Clang module is placed directly in the resource
// directory, and not split into target-specific directories. This means that the module is always available, even on
// platforms that don't have libdispatch. Thus, we need to check for the actual platform.
#if os(WASI)

/// No-op shim function
func dispatchPrecondition(condition: DispatchPredicate) {}

struct DispatchPredicate {
static func onQueue<X>(_: X) -> Self {
return DispatchPredicate()
}

static func notOnQueue<X>(_: X) -> Self {
return DispatchPredicate()
}
}

extension XCTWaiter {
/// Single-threaded queue without any actual queueing
struct DispatchQueue {
init(label: String) {}

func sync<T>(_ body: () -> T) -> T {
body()
}
func async(_ body: @escaping () -> Void) {
body()
}
}
}

#endif
3 changes: 3 additions & 0 deletions Sources/XCTest/Private/WaiterManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
//
// WaiterManager.swift
//
#if !USE_SWIFT_CONCURRENCY_WAITER

internal protocol ManageableWaiter: AnyObject, Equatable {
var isFinished: Bool { get }
Expand Down Expand Up @@ -143,3 +144,5 @@ internal final class WaiterManager<WaiterType: ManageableWaiter> : NSObject {
}

}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
// XCTNSNotificationExpectation.swift
//

#if !USE_SWIFT_CONCURRENCY_WAITER

/// Expectation subclass for waiting on a condition defined by a Foundation Notification instance.
open class XCTNSNotificationExpectation: XCTestExpectation {

Expand Down Expand Up @@ -114,3 +116,5 @@ open class XCTNSNotificationExpectation: XCTestExpectation {
/// - SeeAlso: `XCTNSNotificationExpectation.handler`
@available(*, deprecated, renamed: "XCTNSNotificationExpectation.Handler")
public typealias XCNotificationExpectationHandler = XCTNSNotificationExpectation.Handler

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
// XCTNSPredicateExpectation.swift
//

#if !USE_SWIFT_CONCURRENCY_WAITER

/// Expectation subclass for waiting on a condition defined by an NSPredicate and an optional object.
open class XCTNSPredicateExpectation: XCTestExpectation {

Expand Down Expand Up @@ -133,3 +135,4 @@ open class XCTNSPredicateExpectation: XCTestExpectation {
/// - SeeAlso: `XCTNSPredicateExpectation.handler`
@available(*, deprecated, renamed: "XCTNSPredicateExpectation.Handler")
public typealias XCPredicateExpectationHandler = XCTNSPredicateExpectation.Handler
#endif
41 changes: 41 additions & 0 deletions Sources/XCTest/Public/Asynchronous/XCTWaiter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ open class XCTWaiter {
private var state = State.ready
internal var timeout: TimeInterval = 0
internal var waitSourceLocation: SourceLocation?
#if !USE_SWIFT_CONCURRENCY_WAITER
private weak var manager: WaiterManager<XCTWaiter>?
#endif
private var runLoop: RunLoop?

private weak var _delegate: XCTWaiterDelegate?
Expand Down Expand Up @@ -187,9 +189,16 @@ open class XCTWaiter {
/// these environments. To ensure compatibility of tests between
/// swift-corelibs-xctest and Apple XCTest, it is not recommended to pass
/// explicit values for `file` and `line`.
#if USE_SWIFT_CONCURRENCY_WAITER
@available(*, unavailable, message: "Expectation-based waiting is not available when using the Swift concurrency waiter.")
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm a little bit confused about the messaging of the USE_SWIFT_CONCURRENCY_WAITER option, the continued availability of the XCTWaiter class, and these unavailability messages about "expectation-based waiting".

Is there anything left that XCTWaiter can do in this mode? Or should the entire class be removed / made unavailable?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm a little bit confused about the messaging of the USE_SWIFT_CONCURRENCY_WAITER option

Sorry for your confusion, the option conceptually means that no thread blocking should happen with this mode.

and these unavailability messages about "expectation-based waiting".

I used "expectation" word because XCTestExpectation-related APIs block thread. But "Blocking-wait is unavailable" might be more descriptive?

the continued availability of the XCTWaiter class
Is there anything left that XCTWaiter can do in this mode? Or should the entire class be removed / made unavailable?

There are no functionalities XCTWaiter can do with this mode, but I left the class itself available to avoid spreading #if around all XCTWaiter.subsystemQueue uses.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do you think it would be workable to adjust the scope and semantics of this conditional a little bit, perhaps calling it DISABLE_XCTWAITER or something along those lines, and have it guard the entirety of the XCTWaiter and XCTestExpectation classes, along with the everything in XCTestCase+Asynchronous.swift, and the expectation-related state tracking in XCTestCase.swift.

I do see that usage of the XCTWaiter.subsystemQueue has leaked out to non-waiter code in a few places (ThrownErrorWrapper and TeardownBlocksState). Those are using it for just basic locking purposes, and quite independently from the actual waiter subsystem. Perhaps we introduce a XCTestCase.subsystemQueue to use for those instead? Or even a different locking solution altogether? This would be a general-purpose change, not conditionalized by the new USE_SWIFT_CONCURRENCY_WAITER/DISABLE_XCTWAITER option.

Copy link
Member Author

Choose a reason for hiding this comment

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

That totally makes sense to me. I managed to guard out XCTWaiter and XCTestExpectation entirely, and it simplified a lot. A bonus here was I could remove DispatchShim :)

Copy link
Contributor

Choose a reason for hiding this comment

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

If we introduce a second queue, we should be very wary of deadlocks, data races, etc. where the current code assumes a single global lock.

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, I used the new queue in TeardownBlocksState and ThrownErrorWrapper. As far as I checked, they are well independent from the other, and they won't acquire the other lock while acquiring itself lock. But I need more eyes here 🙏

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed, considering the fine-grained scoping of the locking provided by the queue for these, I think we should be ok in that regard.

#else
@available(*, noasync, message: "Use await fulfillment(of:timeout:enforceOrder:) instead.")
#endif
@discardableResult
open func wait(for expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) -> Result {
#if USE_SWIFT_CONCURRENCY_WAITER
fatalError("This method is not available when using the Swift concurrency waiter.")
#else
precondition(Set(expectations).count == expectations.count, "API violation - each expectation can appear only once in the 'expectations' parameter.")

self.timeout = timeout
Expand Down Expand Up @@ -251,6 +260,7 @@ open class XCTWaiter {
}

return result
#endif
}

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

/// 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
Expand All @@ -306,9 +324,17 @@ open class XCTWaiter {
/// expectations are not fulfilled before the given timeout. Default is the line
/// number of the call to this method in the calling file. It is rare to
/// provide this parameter when calling this method.
#if USE_SWIFT_CONCURRENCY_WAITER
@available(*, unavailable, message: "Expectation-based waiting is not available when using the Swift concurrency waiter.")
#else
@available(*, noasync, message: "Use await fulfillment(of:timeout:enforceOrder:) instead.")
#endif
open class func wait(for expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) -> Result {
#if USE_SWIFT_CONCURRENCY_WAITER
fatalError("This method is not available when using the Swift concurrency waiter.")
#else
return XCTWaiter().wait(for: expectations, timeout: timeout, enforceOrder: enforceOrder, file: file, line: line)
#endif
}

/// 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
Expand All @@ -327,9 +353,17 @@ open class XCTWaiter {
/// expectations are not fulfilled before the given timeout. Default is the line
/// number of the call to this method in the calling file. It is rare to
/// provide this parameter when calling this method.
#if USE_SWIFT_CONCURRENCY_WAITER
@available(*, unavailable, message: "Expectation-based waiting is not available when using the Swift concurrency waiter.")
#else
@available(macOS 12.0, *)
#endif
open class func fulfillment(of expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) async -> Result {
#if USE_SWIFT_CONCURRENCY_WAITER
fatalError("This method is not available when using the Swift concurrency waiter.")
#else
return await XCTWaiter().fulfillment(of: expectations, timeout: timeout, enforceOrder: enforceOrder, file: file, line: line)
#endif
}

deinit {
Expand All @@ -338,6 +372,7 @@ open class XCTWaiter {
}
}

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

Expand Down Expand Up @@ -413,9 +448,11 @@ open class XCTWaiter {
queue_validateExpectationFulfillment(dueToTimeout: false)
}
}
#endif

}

#if !USE_SWIFT_CONCURRENCY_WAITER
private extension XCTWaiter {
func primitiveWait(using runLoop: RunLoop, duration timeout: TimeInterval) {
// The contract for `primitiveWait(for:)` explicitly allows waiting for a shorter period than requested
Expand All @@ -436,6 +473,7 @@ private extension XCTWaiter {
#endif
}
}
#endif

extension XCTWaiter: Equatable {
public static func == (lhs: XCTWaiter, rhs: XCTWaiter) -> Bool {
Expand All @@ -453,6 +491,7 @@ extension XCTWaiter: CustomStringConvertible {
}
}

#if !USE_SWIFT_CONCURRENCY_WAITER
extension XCTWaiter: ManageableWaiter {
var isFinished: Bool {
return XCTWaiter.subsystemQueue.sync {
Expand All @@ -479,3 +518,5 @@ extension XCTWaiter: ManageableWaiter {
}
}
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
// Methods on XCTestCase for testing asynchronous operations
//

#if !USE_SWIFT_CONCURRENCY_WAITER

public extension XCTestCase {

/// Creates a point of synchronization in the flow of a test. Only one
Expand Down Expand Up @@ -265,3 +267,4 @@ internal extension XCTestCase {
expected: false)
}
}
#endif
23 changes: 23 additions & 0 deletions Sources/XCTest/Public/XCAbstractTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,43 @@ open class XCTest {
/// testRunClass. If the test has not yet been run, this will be nil.
open private(set) var testRun: XCTestRun? = nil

internal var performTask: Task<Void, Never>?

#if USE_SWIFT_CONCURRENCY_WAITER
internal func _performAsync(_ run: XCTestRun) async {
fatalError("Must be overridden by subclasses.")
}
internal func _runAsync() async {
guard let testRunType = testRunClass as? XCTestRun.Type else {
fatalError("XCTest.testRunClass must be a kind of XCTestRun.")
}
testRun = testRunType.init(test: self)
await _performAsync(testRun!)
}
#endif

/// The method through which tests are executed. Must be overridden by
/// subclasses.
#if USE_SWIFT_CONCURRENCY_WAITER
@available(*, unavailable)
#endif
open func perform(_ run: XCTestRun) {
fatalError("Must be overridden by subclasses.")
}

/// Creates an instance of the `testRunClass` and passes it as a parameter
/// to `perform()`.
#if USE_SWIFT_CONCURRENCY_WAITER
@available(*, unavailable)
#endif
open func run() {
#if !USE_SWIFT_CONCURRENCY_WAITER
guard let testRunType = testRunClass as? XCTestRun.Type else {
fatalError("XCTest.testRunClass must be a kind of XCTestRun.")
}
testRun = testRunType.init(test: self)
perform(testRun!)
#endif
}

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