From a5077655b439533820d6604fc113e28075ac4cf6 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 17 Apr 2020 00:11:15 +0100 Subject: [PATCH 01/26] Add basic WASI ifdefs --- CMakeLists.txt | 14 ++++++++++++-- Sources/XCTest/Public/XCTestMain.swift | 2 +- Sources/XCTest/Public/XCTestObservation.swift | 5 ++++- cmake/modules/SwiftSupport.cmake | 2 ++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index aeefa9be1..c7c7b57e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,13 +8,16 @@ 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) +if(NOT CMAKE_SYSTEM_NAME STREQUAL Darwin AND NOT CMAKE_SYSTEM_PROCESSOR STREQUAL wasm32) find_package(dispatch CONFIG REQUIRED) find_package(Foundation CONFIG REQUIRED) endif() include(SwiftSupport) + +if(NOT CMAKE_SYSTEM_PROCESSOR STREQUAL wasm32) include(GNUInstallDirs) +endif() add_library(XCTest Sources/XCTest/Private/WallClockTimeMetric.swift @@ -52,7 +55,7 @@ if(USE_FOUNDATION_FRAMEWORK) target_compile_definitions(XCTest PRIVATE USE_FOUNDATION_FRAMEWORK) endif() -if(NOT CMAKE_SYSTEM_NAME STREQUAL Darwin) +if(NOT CMAKE_SYSTEM_NAME STREQUAL Darwin AND NOT CMAKE_SYSTEM_PROCESSOR STREQUAL wasm32) target_link_libraries(XCTest PRIVATE dispatch Foundation) @@ -61,6 +64,13 @@ set_target_properties(XCTest PROPERTIES Swift_MODULE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/swift INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_BINARY_DIR}/swift) +if(CMAKE_SYSTEM_PROCESSOR STREQUAL wasm32) + target_compile_options(XCTest + PRIVATE + -sdk ${WASI_SDK_PREFIX}/share/wasi-sysroot + -target wasm32-unknown-wasi + ) +endif() if(ENABLE_TESTING) enable_testing() diff --git a/Sources/XCTest/Public/XCTestMain.swift b/Sources/XCTest/Public/XCTestMain.swift index 4c306512e..4ce0ee89a 100644 --- a/Sources/XCTest/Public/XCTestMain.swift +++ b/Sources/XCTest/Public/XCTestMain.swift @@ -20,7 +20,7 @@ #else @_exported import SwiftFoundation #endif -#else +#elseif !os(WASI) @_exported import Foundation #endif diff --git a/Sources/XCTest/Public/XCTestObservation.swift b/Sources/XCTest/Public/XCTestObservation.swift index 1a53207b9..9e7a463c9 100644 --- a/Sources/XCTest/Public/XCTestObservation.swift +++ b/Sources/XCTest/Public/XCTestObservation.swift @@ -15,11 +15,12 @@ /// test run. /// - seealso: `XCTestObservationCenter` public protocol XCTestObservation: AnyObject { - +#if !os(WASI) /// Sent immediately before tests begin as a hook for any pre-testing setup. /// - Parameter testBundle: The bundle containing the tests that were /// executed. func testBundleWillStart(_ testBundle: Bundle) +#endif /// Sent when a test suite starts executing. /// - Parameter testSuite: The test suite that started. Additional @@ -51,6 +52,7 @@ public protocol XCTestObservation: AnyObject { /// information can be retrieved from the associated XCTestRun. func testSuiteDidFinish(_ testSuite: XCTestSuite) +#if !os(WASI) /// Sent immediately after all tests have finished as a hook for any /// post-testing activity. The test process will generally exit after this /// method returns, so if there is long running and/or asynchronous work to @@ -59,6 +61,7 @@ public protocol XCTestObservation: AnyObject { /// - Parameter testBundle: The bundle containing the tests that were /// executed. func testBundleDidFinish(_ testBundle: Bundle) +#endif } // All `XCTestObservation` methods are optional, so empty default implementations are provided diff --git a/cmake/modules/SwiftSupport.cmake b/cmake/modules/SwiftSupport.cmake index 66ed90f33..9ff7cb048 100644 --- a/cmake/modules/SwiftSupport.cmake +++ b/cmake/modules/SwiftSupport.cmake @@ -30,6 +30,8 @@ function(get_swift_host_arch result_var_name) set("${result_var_name}" "i686" PARENT_SCOPE) elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "i686") set("${result_var_name}" "i686" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "wasm32") + set("${result_var_name}" "wasm32" PARENT_SCOPE) else() message(FATAL_ERROR "Unrecognized architecture on host system: ${CMAKE_SYSTEM_PROCESSOR}") endif() From 7fab8df41e5f4253a0f02b23a34f3266a1a1f098 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 17 Apr 2020 22:59:18 +0100 Subject: [PATCH 02/26] Delete more unused code, fix some errors --- Package.resolved | 25 ++ Package.swift | 7 +- Sources/XCTest/Private/PerformanceMeter.swift | 202 --------- Sources/XCTest/Private/PrintObserver.swift | 13 +- Sources/XCTest/Private/TestListing.swift | 6 +- Sources/XCTest/Private/WaiterManager.swift | 145 ------ .../XCTest/Private/WallClockTimeMetric.swift | 79 ---- .../XCTNSNotificationExpectation.swift | 116 ----- .../XCTNSPredicateExpectation.swift | 135 ------ .../Asynchronous/XCTWaiter+Validation.swift | 89 ---- .../Public/Asynchronous/XCTWaiter.swift | 419 ------------------ .../XCTestCase+Asynchronous.swift | 240 ---------- .../Asynchronous/XCTestExpectation.swift | 329 -------------- .../Public/XCTestCase+Performance.swift | 176 -------- Sources/XCTest/Public/XCTestErrors.swift | 9 + Sources/XCTest/Public/XCTestMain.swift | 4 +- Sources/XCTest/Public/XCTestObservation.swift | 4 +- .../Public/XCTestObservationCenter.swift | 4 + Sources/XCTest/Public/XCTestRun.swift | 4 + Sources/XCTest/Public/XCTestSuiteRun.swift | 4 + 20 files changed, 71 insertions(+), 1939 deletions(-) create mode 100644 Package.resolved delete mode 100644 Sources/XCTest/Private/PerformanceMeter.swift delete mode 100644 Sources/XCTest/Private/WaiterManager.swift delete mode 100644 Sources/XCTest/Private/WallClockTimeMetric.swift delete mode 100644 Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift delete mode 100644 Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift delete mode 100644 Sources/XCTest/Public/Asynchronous/XCTWaiter+Validation.swift delete mode 100644 Sources/XCTest/Public/Asynchronous/XCTWaiter.swift delete mode 100644 Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift delete mode 100644 Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift delete mode 100644 Sources/XCTest/Public/XCTestCase+Performance.swift diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 000000000..47bdc3a72 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,25 @@ +{ + "object": { + "pins": [ + { + "package": "pure-swift-json", + "repositoryURL": "https://github.com/fabianfett/pure-swift-json.git", + "state": { + "branch": null, + "revision": "816db7e4e35d584476ba52965224e4abf3517065", + "version": "0.2.1" + } + }, + { + "package": "WASIFoundation", + "repositoryURL": "https://github.com/MaxDesiatov/WASIFoundation.git", + "state": { + "branch": "master", + "revision": "87665a15b3c396b7182a571dca65fe9e8547b9c1", + "version": null + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift index 71075aedf..527ba02a5 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:4.0 +// swift-tools-version:4.2 // // To build with auto-linking of the .swiftmodule use: // $ swift build -Xswiftc -module-link-name -Xswiftc XCTest @@ -11,13 +11,14 @@ let package = Package( products: [ .library( name: "XCTest", - type: .dynamic, targets: ["XCTest"] ) ], dependencies: [ + .package(url: "https://github.com/fabianfett/pure-swift-json.git", .upToNextMajor(from: "0.2.1")), + .package(url: "https://github.com/MaxDesiatov/WASIFoundation.git", .branch("master")), ], targets: [ - .target(name: "XCTest", dependencies: [], path: "Sources"), + .target(name: "XCTest", dependencies: ["WASIFoundation"], path: "Sources"), ] ) diff --git a/Sources/XCTest/Private/PerformanceMeter.swift b/Sources/XCTest/Private/PerformanceMeter.swift deleted file mode 100644 index afb3e1918..000000000 --- a/Sources/XCTest/Private/PerformanceMeter.swift +++ /dev/null @@ -1,202 +0,0 @@ -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2016 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 -// -// -// PerformanceMeter.swift -// Measures the performance of a block of code and reports the results. -// - -/// Describes a type that is capable of measuring some aspect of code performance -/// over time. -internal protocol PerformanceMetric { - /// Called once per iteration immediately before the tested code is executed. - /// The metric should do whatever work is required to begin a new measurement. - func startMeasuring() - - /// Called once per iteration immediately after the tested code is executed. - /// The metric should do whatever work is required to finalize measurement. - func stopMeasuring() - - /// Called once, after all measurements have been taken, to provide feedback - /// about the collected measurements. - /// - Returns: Measurement results to present to the user. - func calculateResults() -> String - - /// Called once, after all measurements have been taken, to determine whether - /// the measurements should be treated as a test failure or not. - /// - Returns: A diagnostic message if the results indicate failure, else nil. - func failureMessage() -> String? -} - -/// Protocol used by `PerformanceMeter` to report measurement results -internal protocol PerformanceMeterDelegate { - /// Reports a string representation of the gathered performance metrics - /// - Parameter results: The raw measured values, and some derived data such - /// as average, and standard deviation - /// - Parameter file: The source file name where the measurement was invoked - /// - Parameter line: The source line number where the measurement was invoked - func recordMeasurements(results: String, file: StaticString, line: Int) - - /// Reports a test failure from the analysis of performance measurements. - /// This can currently be caused by an unexpectedly large standard deviation - /// calculated over the data. - /// - Parameter description: An explanation of the failure - /// - Parameter file: The source file name where the measurement was invoked - /// - Parameter line: The source line number where the measurement was invoked - func recordFailure(description: String, file: StaticString, line: Int) - - /// Reports a misuse of the `PerformanceMeter` API, such as calling ` - /// startMeasuring` multiple times. - /// - Parameter description: An explanation of the misuse - /// - Parameter file: The source file name where the misuse occurred - /// - Parameter line: The source line number where the misuse occurred - func recordAPIViolation(description: String, file: StaticString, line: Int) -} - -/// - Bug: This class is intended to be `internal` but is public to work around -/// a toolchain bug on Linux. See `XCTestCase._performanceMeter` for more info. -public final class PerformanceMeter { - enum Error: Swift.Error, CustomStringConvertible { - case noMetrics - case unknownMetric(metricName: String) - case startMeasuringAlreadyCalled - case stopMeasuringAlreadyCalled - case startMeasuringNotCalled - case stopBeforeStarting - - var description: String { - switch self { - case .noMetrics: return "At least one metric must be provided to measure." - case .unknownMetric(let name): return "Unknown metric: \(name)" - case .startMeasuringAlreadyCalled: return "Already called startMeasuring() once this iteration." - case .stopMeasuringAlreadyCalled: return "Already called stopMeasuring() once this iteration." - case .startMeasuringNotCalled: return "startMeasuring() must be called during the block." - case .stopBeforeStarting: return "Cannot stop measuring before starting measuring." - } - } - } - - internal var didFinishMeasuring: Bool { - return state == .measurementFinished || state == .measurementAborted - } - - private enum State { - case iterationUnstarted - case iterationStarted - case iterationFinished - case measurementFinished - case measurementAborted - } - private var state: State = .iterationUnstarted - - private let metrics: [PerformanceMetric] - private let delegate: PerformanceMeterDelegate - private let invocationFile: StaticString - private let invocationLine: Int - - private init(metrics: [PerformanceMetric], delegate: PerformanceMeterDelegate, file: StaticString, line: Int) { - self.metrics = metrics - self.delegate = delegate - self.invocationFile = file - self.invocationLine = line - } - - static func measureMetrics(_ metricNames: [String], delegate: PerformanceMeterDelegate, file: StaticString = #file, line: Int = #line, for block: (PerformanceMeter) -> Void) { - do { - let metrics = try self.metrics(forNames: metricNames) - let meter = PerformanceMeter(metrics: metrics, delegate: delegate, file: file, line: line) - meter.measure(block) - } catch let e { - delegate.recordAPIViolation(description: String(describing: e), file: file, line: line) - } - } - - func startMeasuring(file: StaticString = #file, line: Int = #line) { - guard state == .iterationUnstarted else { - return recordAPIViolation(.startMeasuringAlreadyCalled, file: file, line: line) - } - state = .iterationStarted - metrics.forEach { $0.startMeasuring() } - } - - func stopMeasuring(file: StaticString = #file, line: Int = #line) { - guard state != .iterationUnstarted else { - return recordAPIViolation(.stopBeforeStarting, file: file, line: line) - } - - guard state != .iterationFinished else { - return recordAPIViolation(.stopMeasuringAlreadyCalled, file: file, line: line) - } - - state = .iterationFinished - metrics.forEach { $0.stopMeasuring() } - } - - func abortMeasuring() { - state = .measurementAborted - } - - - private static func metrics(forNames names: [String]) throws -> [PerformanceMetric] { - guard !names.isEmpty else { throw Error.noMetrics } - - let metricsMapping = [WallClockTimeMetric.name : WallClockTimeMetric.self] - - return try names.map({ - guard let metricType = metricsMapping[$0] else { throw Error.unknownMetric(metricName: $0) } - return metricType.init() - }) - } - - private var numberOfIterations: Int { - return 10 - } - - private func measure(_ block: (PerformanceMeter) -> Void) { - for _ in (0.. [String] - func dictionaryRepresentation() -> NSDictionary + func dictionaryRepresentation() -> [String: Any] } private func moduleName(value: Any) -> String { @@ -68,7 +68,7 @@ extension XCTestSuite: Listable { return listables.flatMap({ $0.list() }) } - func dictionaryRepresentation() -> NSDictionary { + func dictionaryRepresentation() -> [String: Any] { let listedTests = NSArray(array: tests.compactMap({ ($0 as? Listable)?.dictionaryRepresentation() })) return NSDictionary(objects: [NSString(string: listingName), listedTests], @@ -93,7 +93,7 @@ extension XCTestCase: Listable { return ["\(moduleName(value: self)).\(adjustedName)"] } - func dictionaryRepresentation() -> NSDictionary { + func dictionaryRepresentation() -> [String: Any] { let methodName = String(name.split(separator: ".").last!) return NSDictionary(object: NSString(string: methodName), forKey: NSString(string: "name")) } diff --git a/Sources/XCTest/Private/WaiterManager.swift b/Sources/XCTest/Private/WaiterManager.swift deleted file mode 100644 index f705165fe..000000000 --- a/Sources/XCTest/Private/WaiterManager.swift +++ /dev/null @@ -1,145 +0,0 @@ -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2018 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 -// -// -// WaiterManager.swift -// - -internal protocol ManageableWaiter: AnyObject, Equatable { - var isFinished: Bool { get } - - // Invoked on `XCTWaiter.subsystemQueue` - func queue_handleWatchdogTimeout() - func queue_interrupt(for interruptingWaiter: Self) -} - -private protocol ManageableWaiterWatchdog { - func cancel() -} -extension DispatchWorkItem: ManageableWaiterWatchdog {} - -/// This class manages the XCTWaiter instances which are currently waiting on a particular thread. -/// It facilitates "nested" waiters, allowing an outer waiter to interrupt inner waiters if it times -/// out. -internal final class WaiterManager : NSObject { - - /// The current thread's waiter manager. This is the only supported way to access an instance of - /// this class, since each instance is bound to a particular thread and is only concerned with - /// the XCTWaiters waiting on that thread. - static var current: WaiterManager { - let threadKey = "org.swift.XCTest.WaiterManager" - - if let existing = Thread.current.threadDictionary[threadKey] as? WaiterManager { - return existing - } else { - let manager = WaiterManager() - Thread.current.threadDictionary[threadKey] = manager - return manager - } - } - - private struct ManagedWaiterDetails { - let waiter: WaiterType - let watchdog: ManageableWaiterWatchdog? - } - - private var managedWaiterStack = [ManagedWaiterDetails]() - private weak var thread = Thread.current - private let queue = DispatchQueue(label: "org.swift.XCTest.WaiterManager") - - // Use `WaiterManager.current` to access the thread-specific instance - private override init() {} - - deinit { - assert(managedWaiterStack.isEmpty, "Waiters still registered when WaiterManager is deallocating.") - } - - func startManaging(_ waiter: WaiterType, timeout: TimeInterval) { - guard let thread = thread else { fatalError("\(self) no longer belongs to a thread") } - precondition(thread === Thread.current, "\(#function) called on wrong thread, must be called on \(thread)") - - var alreadyFinishedOuterWaiter: WaiterType? - - queue.sync { - // To start managing `waiter`, first see if any existing, outer waiters have already finished, - // because if one has, then `waiter` will be immediately interrupted before it begins waiting. - alreadyFinishedOuterWaiter = managedWaiterStack.first(where: { $0.waiter.isFinished })?.waiter - - let watchdog: ManageableWaiterWatchdog? - if alreadyFinishedOuterWaiter == nil { - // If there is no already-finished outer waiter, install a watchdog for `waiter`, and store it - // alongside `waiter` so that it may be canceled if `waiter` finishes waiting within its allotted timeout. - watchdog = WaiterManager.installWatchdog(for: waiter, timeout: timeout) - } else { - // If there is an already-finished outer waiter, no watchdog is needed for `waiter` because it will - // be interrupted before it begins waiting. - watchdog = nil - } - - // Add the waiter even if it's going to immediately be interrupted below to simplify the stack management - let details = ManagedWaiterDetails(waiter: waiter, watchdog: watchdog) - managedWaiterStack.append(details) - } - - if let alreadyFinishedOuterWaiter = alreadyFinishedOuterWaiter { - XCTWaiter.subsystemQueue.async { - waiter.queue_interrupt(for: alreadyFinishedOuterWaiter) - } - } - } - - func stopManaging(_ waiter: WaiterType) { - guard let thread = thread else { fatalError("\(self) no longer belongs to a thread") } - precondition(thread === Thread.current, "\(#function) called on wrong thread, must be called on \(thread)") - - queue.sync { - precondition(!managedWaiterStack.isEmpty, "Waiter stack was empty when requesting to stop managing: \(waiter)") - - let expectedIndex = managedWaiterStack.index(before: managedWaiterStack.endIndex) - let waiterDetails = managedWaiterStack[expectedIndex] - guard waiter == waiterDetails.waiter else { - fatalError("Top waiter on stack \(waiterDetails.waiter) is not equal to waiter to stop managing: \(waiter)") - } - - waiterDetails.watchdog?.cancel() - managedWaiterStack.remove(at: expectedIndex) - } - } - - private static func installWatchdog(for waiter: WaiterType, timeout: TimeInterval) -> ManageableWaiterWatchdog { - // Use DispatchWorkItem instead of a basic closure since it can be canceled. - let watchdog = DispatchWorkItem { [weak waiter] in - waiter?.queue_handleWatchdogTimeout() - } - - let outerTimeoutSlop = TimeInterval(0.25) - let deadline = DispatchTime.now() + timeout + outerTimeoutSlop - XCTWaiter.subsystemQueue.asyncAfter(deadline: deadline, execute: watchdog) - - return watchdog - } - - func queue_handleWatchdogTimeout(of waiter: WaiterType) { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - - var waitersToInterrupt = [WaiterType]() - - queue.sync { - guard let indexOfWaiter = managedWaiterStack.firstIndex(where: { $0.waiter == waiter }) else { - preconditionFailure("Waiter \(waiter) reported timed out but is not in the waiter stack \(managedWaiterStack)") - } - - waitersToInterrupt += managedWaiterStack[managedWaiterStack.index(after: indexOfWaiter)...].map { $0.waiter } - } - - for waiterToInterrupt in waitersToInterrupt.reversed() { - waiterToInterrupt.queue_interrupt(for: waiter) - } - } - -} diff --git a/Sources/XCTest/Private/WallClockTimeMetric.swift b/Sources/XCTest/Private/WallClockTimeMetric.swift deleted file mode 100644 index 2fe945a34..000000000 --- a/Sources/XCTest/Private/WallClockTimeMetric.swift +++ /dev/null @@ -1,79 +0,0 @@ -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2016 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 -// -// -// WallClockTimeMetric.swift -// Performance metric measuring how long it takes code to execute -// - -/// This metric uses the system uptime to keep track of how much time passes -/// between starting and stopping measuring. -internal final class WallClockTimeMetric: PerformanceMetric { - static let name = "org.swift.XCTPerformanceMetric_WallClockTime" - - typealias Measurement = TimeInterval - private var startTime: TimeInterval? - var measurements: [Measurement] = [] - - func startMeasuring() { - startTime = currentTime() - } - - func stopMeasuring() { - guard let startTime = startTime else { fatalError("Must start measuring before stopping measuring") } - let stopTime = currentTime() - measurements.append(stopTime-startTime) - } - - private let maxRelativeStandardDeviation = 10.0 - private let standardDeviationNegligibilityThreshold = 0.1 - - func calculateResults() -> String { - let results = [ - String(format: "average: %.3f", measurements.average), - String(format: "relative standard deviation: %.3f%%", measurements.relativeStandardDeviation), - "values: [\(measurements.map({ String(format: "%.6f", $0) }).joined(separator: ", "))]", - "performanceMetricID:\(type(of: self).name)", - String(format: "maxPercentRelativeStandardDeviation: %.3f%%", maxRelativeStandardDeviation), - String(format: "maxStandardDeviation: %.3f", standardDeviationNegligibilityThreshold), - ] - return "[Time, seconds] \(results.joined(separator: ", "))" - } - - func failureMessage() -> String? { - let relativeStandardDeviation = measurements.relativeStandardDeviation - if (relativeStandardDeviation > maxRelativeStandardDeviation && - measurements.standardDeviation > standardDeviationNegligibilityThreshold) { - return String(format: "The relative standard deviation of the measurements is %.3f%% which is higher than the max allowed of %.3f%%.", relativeStandardDeviation, maxRelativeStandardDeviation) - } - - return nil - } - - private func currentTime() -> TimeInterval { - return ProcessInfo.processInfo.systemUptime - } -} - - -private extension Collection where Index: ExpressibleByIntegerLiteral, Iterator.Element == WallClockTimeMetric.Measurement { - var average: WallClockTimeMetric.Measurement { - return self.reduce(0, +) / Double(Int(count)) - } - - var standardDeviation: WallClockTimeMetric.Measurement { - let average = self.average - let squaredDifferences = self.map({ pow($0 - average, 2.0) }) - let variance = squaredDifferences.reduce(0, +) / Double(Int(count-1)) - return sqrt(variance) - } - - var relativeStandardDeviation: Double { - return (standardDeviation*100) / average - } -} diff --git a/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift b/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift deleted file mode 100644 index 573c6c270..000000000 --- a/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift +++ /dev/null @@ -1,116 +0,0 @@ -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2018 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 -// -// -// XCTNSNotificationExpectation.swift -// - -/// Expectation subclass for waiting on a condition defined by a Foundation Notification instance. -open class XCTNSNotificationExpectation: XCTestExpectation { - - /// A closure to be invoked when a notification specified by the expectation is observed. - /// - /// - Parameter notification: The notification object which was observed. - /// - Returns: `true` if the expectation should be fulfilled, `false` if it should not. - /// - /// - SeeAlso: `XCTNSNotificationExpectation.handler` - public typealias Handler = (Notification) -> Bool - - private let queue = DispatchQueue(label: "org.swift.XCTest.XCTNSNotificationExpectation") - - /// The name of the notification being waited on. - open private(set) var notificationName: Notification.Name - - /// The specific object that will post the notification, if any. - /// If nil, any object may post the notification. Default is nil. - open private(set) var observedObject: Any? - - /// The specific notification center that the notification will be posted to. - open private(set) var notificationCenter: NotificationCenter - - private var observer: AnyObject? - - private var _handler: Handler? - - /// Allows the caller to install a special handler to do custom evaluation of received notifications - /// matching the specified object and notification center. - /// - /// - SeeAlso: `XCTNSNotificationExpectation.Handler` - open var handler: Handler? { - get { - return queue.sync { _handler } - } - set { - dispatchPrecondition(condition: .notOnQueue(queue)) - queue.async { self._handler = newValue } - } - } - - /// Initializes an expectation that waits for a Foundation Notification to be posted by an optional `object` to a specific NotificationCenter. - /// - /// - Parameter notificationName: The name of the notification to wait on. - /// - Parameter object: The object that will post the notification, if any. Default is nil. - /// - Parameter notificationCenter: The specific notification center that the notification will be posted to. - /// - Parameter file: The file name to use in the error message if - /// expectations are not met before the wait timeout. Default is the file - /// containing the call to this method. It is rare to provide this - /// parameter when calling this method. - /// - Parameter line: The line number to use in the error message if the - /// expectations are not met before the wait 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. - public init(name notificationName: Notification.Name, object: Any? = nil, notificationCenter: NotificationCenter = .default, file: StaticString = #file, line: Int = #line) { - self.notificationName = notificationName - self.observedObject = object - self.notificationCenter = notificationCenter - let description = "Expect notification '\(notificationName.rawValue)' from " + (object.map { "\($0)" } ?? "any object") - - super.init(description: description, file: file, line: line) - - beginObserving(with: notificationCenter) - } - - deinit { - assert(observer == nil, "observer should be nil, indicates failure to call cleanUp() internally") - } - - private func beginObserving(with notificationCenter: NotificationCenter) { - observer = notificationCenter.addObserver(forName: notificationName, object: observedObject, queue: nil) { [weak self] notification in - guard let strongSelf = self else { return } - - let shouldFulfill: Bool - - // If the handler is invoked, the test will only pass if true is returned. - if let handler = strongSelf.handler { - shouldFulfill = handler(notification) - } else { - shouldFulfill = true - } - - if shouldFulfill { - strongSelf.fulfill() - } - } - } - - override func cleanUp() { - queue.sync { - if let observer = observer { - notificationCenter.removeObserver(observer) - self.observer = nil - } - } - } - -} - -/// A closure to be invoked when a notification specified by the expectation is observed. -/// -/// - SeeAlso: `XCTNSNotificationExpectation.handler` -@available(*, deprecated, renamed: "XCTNSNotificationExpectation.Handler") -public typealias XCNotificationExpectationHandler = XCTNSNotificationExpectation.Handler diff --git a/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift b/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift deleted file mode 100644 index 08d0cf26b..000000000 --- a/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift +++ /dev/null @@ -1,135 +0,0 @@ -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2018 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 -// -// -// XCTNSPredicateExpectation.swift -// - -/// Expectation subclass for waiting on a condition defined by an NSPredicate and an optional object. -open class XCTNSPredicateExpectation: XCTestExpectation { - - /// A closure to be invoked whenever evaluating the predicate against the object returns true. - /// - /// - Returns: `true` if the expectation should be fulfilled, `false` if it should not. - /// - /// - SeeAlso: `XCTNSPredicateExpectation.handler` - public typealias Handler = () -> Bool - - private let queue = DispatchQueue(label: "org.swift.XCTest.XCTNSPredicateExpectation") - - /// The predicate used by the expectation. - open private(set) var predicate: NSPredicate - - /// The object against which the predicate is evaluated, if any. Default is nil. - open private(set) var object: Any? - - private var _handler: Handler? - - /// Handler called when evaluating the predicate against the object returns true. If the handler is not - /// provided, the first successful evaluation will fulfill the expectation. If the handler provided, the - /// handler will be queried each time the notification is received to determine whether the expectation - /// should be fulfilled or not. - open var handler: Handler? { - get { - return queue.sync { _handler } - } - set { - dispatchPrecondition(condition: .notOnQueue(queue)) - queue.async { self._handler = newValue } - } - } - - private let runLoop = RunLoop.current - private var timer: Timer? - private let evaluationInterval = 0.01 - - /// Initializes an expectation that waits for a predicate to evaluate as true with an optionally specified object. - /// - /// - Parameter predicate: The predicate to evaluate. - /// - Parameter object: An optional object to evaluate `predicate` with. Default is nil. - /// - Parameter file: The file name to use in the error message if - /// expectations are not met before the wait timeout. Default is the file - /// containing the call to this method. It is rare to provide this - /// parameter when calling this method. - /// - Parameter line: The line number to use in the error message if the - /// expectations are not met before the wait 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. - public init(predicate: NSPredicate, object: Any? = nil, file: StaticString = #file, line: Int = #line) { - self.predicate = predicate - self.object = object - let description = "Expect predicate `\(predicate)`" + (object.map { " for object \($0)" } ?? "") - - super.init(description: description, file: file, line: line) - } - - deinit { - assert(timer == nil, "timer should be nil, indicates failure to call cleanUp() internally") - } - - override func didBeginWaiting() { - runLoop.perform { - if self.shouldFulfill() { - self.fulfill() - } else { - self.startPolling() - } - } - } - - private func startPolling() { - let timer = Timer(timeInterval: evaluationInterval, repeats: true) { [weak self] timer in - guard let self = self else { - timer.invalidate() - return - } - - if self.shouldFulfill() { - self.fulfill() - timer.invalidate() - } - } - - runLoop.add(timer, forMode: .default) - queue.async { - self.timer = timer - } - } - - private func shouldFulfill() -> Bool { - if predicate.evaluate(with: object) { - if let handler = handler { - if handler() { - return true - } - // We do not fulfill or invalidate the timer if the handler returns - // false. The object is still re-evaluated until timeout. - } else { - return true - } - } - - return false - } - - override func cleanUp() { - queue.sync { - if let timer = timer { - timer.invalidate() - self.timer = nil - } - } - } - -} - -/// A closure to be invoked whenever evaluating the predicate against the object returns true. -/// -/// - SeeAlso: `XCTNSPredicateExpectation.handler` -@available(*, deprecated, renamed: "XCTNSPredicateExpectation.Handler") -public typealias XCPredicateExpectationHandler = XCTNSPredicateExpectation.Handler diff --git a/Sources/XCTest/Public/Asynchronous/XCTWaiter+Validation.swift b/Sources/XCTest/Public/Asynchronous/XCTWaiter+Validation.swift deleted file mode 100644 index 5ff4643c7..000000000 --- a/Sources/XCTest/Public/Asynchronous/XCTWaiter+Validation.swift +++ /dev/null @@ -1,89 +0,0 @@ -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2018 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 -// -// -// XCTWaiter+Validation.swift -// - -protocol XCTWaiterValidatableExpectation: Equatable { - var isFulfilled: Bool { get } - var fulfillmentToken: UInt64 { get } - var isInverted: Bool { get } -} - -extension XCTWaiter { - struct ValidatableXCTestExpectation: XCTWaiterValidatableExpectation { - let expectation: XCTestExpectation - - var isFulfilled: Bool { - return expectation.queue_isFulfilled - } - - var fulfillmentToken: UInt64 { - return expectation.queue_fulfillmentToken - } - - var isInverted: Bool { - return expectation.queue_isInverted - } - } -} - -extension XCTWaiter { - enum ValidationResult { - case complete - case fulfilledInvertedExpectation(invertedExpectation: ExpectationType) - case violatedOrderingConstraints(expectation: ExpectationType, requiredExpectation: ExpectationType) - case timedOut(unfulfilledExpectations: [ExpectationType]) - case incomplete - } - - static func validateExpectations(_ expectations: [ExpectationType], dueToTimeout didTimeOut: Bool, enforceOrder: Bool) -> ValidationResult { - var unfulfilledExpectations = [ExpectationType]() - var fulfilledExpectations = [ExpectationType]() - - for expectation in expectations { - if expectation.isFulfilled { - // Check for any fulfilled inverse expectations. If they were fulfilled before wait was called, - // this is where we'd catch that. - if expectation.isInverted { - return .fulfilledInvertedExpectation(invertedExpectation: expectation) - } else { - fulfilledExpectations.append(expectation) - } - } else { - unfulfilledExpectations.append(expectation) - } - } - - if enforceOrder { - fulfilledExpectations.sort { $0.fulfillmentToken < $1.fulfillmentToken } - let nonInvertedExpectations = expectations.filter { !$0.isInverted } - - assert(fulfilledExpectations.count <= nonInvertedExpectations.count, "Internal error: number of fulfilledExpectations (\(fulfilledExpectations.count)) must not exceed number of non-inverted expectations (\(nonInvertedExpectations.count))") - - for (fulfilledExpectation, nonInvertedExpectation) in zip(fulfilledExpectations, nonInvertedExpectations) where fulfilledExpectation != nonInvertedExpectation { - return .violatedOrderingConstraints(expectation: fulfilledExpectation, requiredExpectation: nonInvertedExpectation) - } - } - - if unfulfilledExpectations.isEmpty { - return .complete - } else if didTimeOut { - // If we've timed out, our new state is just based on whether or not we have any remaining unfulfilled, non-inverted expectations. - let nonInvertedUnfilledExpectations = unfulfilledExpectations.filter { !$0.isInverted } - if nonInvertedUnfilledExpectations.isEmpty { - return .complete - } else { - return .timedOut(unfulfilledExpectations: nonInvertedUnfilledExpectations) - } - } - - return .incomplete - } -} diff --git a/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift b/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift deleted file mode 100644 index 13a232f33..000000000 --- a/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift +++ /dev/null @@ -1,419 +0,0 @@ -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2018 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 -// -// -// XCTWaiter.swift -// - -import CoreFoundation - -/// Events are reported to the waiter's delegate via these methods. XCTestCase conforms to this -/// protocol and will automatically report timeouts and other unexpected events as test failures. -/// -/// - Note: These methods are invoked on an arbitrary queue. -public protocol XCTWaiterDelegate: AnyObject { - - /// Invoked when not all waited on expectations are fulfilled during the timeout period. If the delegate - /// is an XCTestCase instance, this will be reported as a test failure. - /// - /// - Parameter waiter: The waiter which timed out. - /// - Parameter unfulfilledExpectations: The expectations which were unfulfilled when `waiter` timed out. - func waiter(_ waiter: XCTWaiter, didTimeoutWithUnfulfilledExpectations unfulfilledExpectations: [XCTestExpectation]) - - /// Invoked when the wait specified that fulfillment order should be enforced and an expectation - /// has been fulfilled in the wrong order. If the delegate is an XCTestCase instance, this will be reported - /// as a test failure. - /// - /// - Parameter waiter: The waiter which had an ordering violation. - /// - Parameter expectation: The expectation which was fulfilled instead of the required expectation. - /// - Parameter requiredExpectation: The expectation which was fulfilled instead of the required expectation. - func waiter(_ waiter: XCTWaiter, fulfillmentDidViolateOrderingConstraintsFor expectation: XCTestExpectation, requiredExpectation: XCTestExpectation) - - /// Invoked when an expectation marked as inverted is fulfilled. If the delegate is an XCTestCase instance, - /// this will be reported as a test failure. - /// - /// - Parameter waiter: The waiter which had an inverted expectation fulfilled. - /// - Parameter expectation: The inverted expectation which was fulfilled. - /// - /// - SeeAlso: `XCTestExpectation.isInverted` - func waiter(_ waiter: XCTWaiter, didFulfillInvertedExpectation expectation: XCTestExpectation) - - /// Invoked when the waiter is interrupted prior to its expectations being fulfilled or timing out. - /// This occurs when an "outer" waiter times out, resulting in any waiters nested inside it being - /// interrupted to allow the call stack to quickly unwind. - /// - /// - Parameter waiter: The waiter which was interrupted. - /// - Parameter outerWaiter: The "outer" waiter which interrupted `waiter`. - func nestedWaiter(_ waiter: XCTWaiter, wasInterruptedByTimedOutWaiter outerWaiter: XCTWaiter) - -} - -// All `XCTWaiterDelegate` methods are optional, so empty default implementations are provided -public extension XCTWaiterDelegate { - func waiter(_ waiter: XCTWaiter, didTimeoutWithUnfulfilledExpectations unfulfilledExpectations: [XCTestExpectation]) {} - func waiter(_ waiter: XCTWaiter, fulfillmentDidViolateOrderingConstraintsFor expectation: XCTestExpectation, requiredExpectation: XCTestExpectation) {} - func waiter(_ waiter: XCTWaiter, didFulfillInvertedExpectation expectation: XCTestExpectation) {} - func nestedWaiter(_ waiter: XCTWaiter, wasInterruptedByTimedOutWaiter outerWaiter: XCTWaiter) {} -} - -/// Manages waiting - pausing the current execution context - for an array of XCTestExpectations. Waiters -/// can be used with or without a delegate to respond to events such as completion, timeout, or invalid -/// expectation fulfillment. XCTestCase conforms to the delegate protocol and will automatically report -/// timeouts and other unexpected events as test failures. -/// -/// Waiters can be used without a delegate or any association with a test case instance. This allows test -/// support libraries to provide convenience methods for waiting without having to pass test cases through -/// those APIs. -open class XCTWaiter { - - /// Values returned by a waiter when it completes, times out, or is interrupted due to another waiter - /// higher in the call stack timing out. - public enum Result: Int { - case completed = 1 - case timedOut - case incorrectOrder - case invertedFulfillment - case interrupted - } - - private enum State: Equatable { - case ready - case waiting(state: Waiting) - case finished(state: Finished) - - struct Waiting: Equatable { - var enforceOrder: Bool - var expectations: [XCTestExpectation] - var fulfilledExpectations: [XCTestExpectation] - } - - struct Finished: Equatable { - let result: Result - let fulfilledExpectations: [XCTestExpectation] - let unfulfilledExpectations: [XCTestExpectation] - } - - var allExpectations: [XCTestExpectation] { - switch self { - case .ready: - return [] - case let .waiting(waitingState): - return waitingState.expectations - case let .finished(finishedState): - return finishedState.fulfilledExpectations + finishedState.unfulfilledExpectations - } - } - } - - internal static let subsystemQueue = DispatchQueue(label: "org.swift.XCTest.XCTWaiter") - - private var state = State.ready - internal var timeout: TimeInterval = 0 - internal var waitSourceLocation: SourceLocation? - private weak var manager: WaiterManager? - private var runLoop: RunLoop? - - private weak var _delegate: XCTWaiterDelegate? - private let delegateQueue = DispatchQueue(label: "org.swift.XCTest.XCTWaiter.delegate") - - /// The waiter delegate will be called with various events described in the `XCTWaiterDelegate` protocol documentation. - /// - /// - SeeAlso: `XCTWaiterDelegate` - open var delegate: XCTWaiterDelegate? { - get { - return XCTWaiter.subsystemQueue.sync { _delegate } - } - set { - dispatchPrecondition(condition: .notOnQueue(XCTWaiter.subsystemQueue)) - XCTWaiter.subsystemQueue.async { self._delegate = newValue } - } - } - - /// Returns an array containing the expectations that were fulfilled, in that order, up until the waiter - /// stopped waiting. Expectations fulfilled after the waiter stopped waiting will not be in the array. - /// The array will be empty until the waiter has started waiting, even if expectations have already been - /// fulfilled. - open var fulfilledExpectations: [XCTestExpectation] { - return XCTWaiter.subsystemQueue.sync { - let fulfilledExpectations: [XCTestExpectation] - - switch state { - case .ready: - fulfilledExpectations = [] - case let .waiting(waitingState): - fulfilledExpectations = waitingState.fulfilledExpectations - case let .finished(finishedState): - fulfilledExpectations = finishedState.fulfilledExpectations - } - - // Sort by fulfillment token before returning, since it is the true fulfillment order. - // The waiter being notified by the expectation isn't guaranteed to happen in the same order. - return fulfilledExpectations.sorted { $0.queue_fulfillmentToken < $1.queue_fulfillmentToken } - } - } - - /// Initializes a waiter with an optional delegate. - public init(delegate: XCTWaiterDelegate? = nil) { - _delegate = delegate - } - - /// Wait on an array of expectations for up to the specified timeout, and optionally specify whether they - /// must be fulfilled in the given order. May return early based on fulfillment of the waited on expectations. - /// - /// - Parameter expectations: The expectations to wait on. - /// - Parameter timeout: The maximum total time duration to wait on all expectations. - /// - Parameter enforceOrder: Specifies whether the expectations must be fulfilled in the order - /// they are specified in the `expectations` Array. Default is false. - /// - Parameter file: The file name to use in the error message if - /// expectations are not fulfilled before the given timeout. Default is the file - /// containing the call to this method. It is rare to provide this - /// parameter when calling this method. - /// - Parameter line: The line number to use in the error message if the - /// 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. - /// - /// - Note: Whereas Objective-C XCTest determines the file and line - /// number of the "wait" call using symbolication, this implementation - /// opts to take `file` and `line` as parameters instead. As a result, - /// the interface to these methods are not exactly identical between - /// 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`. - @discardableResult - open func wait(for expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) -> Result { - precondition(Set(expectations).count == expectations.count, "API violation - each expectation can appear only once in the 'expectations' parameter.") - - self.timeout = timeout - waitSourceLocation = SourceLocation(file: file, line: line) - let runLoop = RunLoop.current - - XCTWaiter.subsystemQueue.sync { - precondition(state == .ready, "API violation - wait(...) has already been called on this waiter.") - - let previouslyWaitedOnExpectations = expectations.filter { $0.queue_hasBeenWaitedOn } - let previouslyWaitedOnExpectationDescriptions = previouslyWaitedOnExpectations.map { $0.queue_expectationDescription }.joined(separator: "`, `") - precondition(previouslyWaitedOnExpectations.isEmpty, "API violation - expectations can only be waited on once, `\(previouslyWaitedOnExpectationDescriptions)` have already been waited on.") - - let waitingState = State.Waiting( - enforceOrder: enforceOrder, - expectations: expectations, - fulfilledExpectations: expectations.filter { $0.queue_isFulfilled } - ) - queue_configureExpectations(expectations) - state = .waiting(state: waitingState) - self.runLoop = runLoop - - queue_validateExpectationFulfillment(dueToTimeout: false) - } - - let manager = WaiterManager.current - manager.startManaging(self, timeout: timeout) - self.manager = manager - - // Begin the core wait loop. - let timeoutTimestamp = Date.timeIntervalSinceReferenceDate + timeout - while !isFinished { - let remaining = timeoutTimestamp - Date.timeIntervalSinceReferenceDate - if remaining <= 0 { - break - } - primitiveWait(using: runLoop, duration: remaining) - } - - manager.stopManaging(self) - self.manager = nil - - let result: Result = XCTWaiter.subsystemQueue.sync { - queue_validateExpectationFulfillment(dueToTimeout: true) - - for expectation in expectations { - expectation.cleanUp() - expectation.queue_didFulfillHandler = nil - } - - guard case let .finished(finishedState) = state else { fatalError("Unexpected state: \(state)") } - return finishedState.result - } - - delegateQueue.sync { - // DO NOT REMOVE ME - // This empty block, executed synchronously, ensures that inflight delegate callbacks from the - // internal queue have been processed before wait returns. - } - - return result - } - - /// 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 - /// must be fulfilled in the given order. May return early based on fulfillment of the waited on expectations. The waiter - /// is discarded when the wait completes. - /// - /// - Parameter expectations: The expectations to wait on. - /// - Parameter timeout: The maximum total time duration to wait on all expectations. - /// - Parameter enforceOrder: Specifies whether the expectations must be fulfilled in the order - /// they are specified in the `expectations` Array. Default is false. - /// - Parameter file: The file name to use in the error message if - /// expectations are not fulfilled before the given timeout. Default is the file - /// containing the call to this method. It is rare to provide this - /// parameter when calling this method. - /// - Parameter line: The line number to use in the error message if the - /// 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. - open class func wait(for expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) -> Result { - return XCTWaiter().wait(for: expectations, timeout: timeout, enforceOrder: enforceOrder, file: file, line: line) - } - - deinit { - for expectation in state.allExpectations { - expectation.cleanUp() - } - } - - private func queue_configureExpectations(_ expectations: [XCTestExpectation]) { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - - for expectation in expectations { - expectation.queue_didFulfillHandler = { [weak self, unowned expectation] in - self?.expectationWasFulfilled(expectation) - } - expectation.queue_hasBeenWaitedOn = true - } - } - - private func queue_validateExpectationFulfillment(dueToTimeout: Bool) { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - guard case let .waiting(waitingState) = state else { return } - - let validatableExpectations = waitingState.expectations.map { ValidatableXCTestExpectation(expectation: $0) } - let validationResult = XCTWaiter.validateExpectations(validatableExpectations, dueToTimeout: dueToTimeout, enforceOrder: waitingState.enforceOrder) - - switch validationResult { - case .complete: - queue_finish(result: .completed, cancelPrimitiveWait: !dueToTimeout) - - case .fulfilledInvertedExpectation(let invertedValidationExpectation): - queue_finish(result: .invertedFulfillment, cancelPrimitiveWait: true) { delegate in - delegate.waiter(self, didFulfillInvertedExpectation: invertedValidationExpectation.expectation) - } - - case .violatedOrderingConstraints(let validationExpectation, let requiredValidationExpectation): - queue_finish(result: .incorrectOrder, cancelPrimitiveWait: true) { delegate in - delegate.waiter(self, fulfillmentDidViolateOrderingConstraintsFor: validationExpectation.expectation, requiredExpectation: requiredValidationExpectation.expectation) - } - - case .timedOut(let unfulfilledValidationExpectations): - queue_finish(result: .timedOut, cancelPrimitiveWait: false) { delegate in - delegate.waiter(self, didTimeoutWithUnfulfilledExpectations: unfulfilledValidationExpectations.map { $0.expectation }) - } - - case .incomplete: - break - - } - } - - private func queue_finish(result: Result, cancelPrimitiveWait: Bool, delegateBlock: ((XCTWaiterDelegate) -> Void)? = nil) { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - guard case let .waiting(waitingState) = state else { preconditionFailure("Unexpected state: \(state)") } - - let unfulfilledExpectations = waitingState.expectations.filter { !waitingState.fulfilledExpectations.contains($0) } - - state = .finished(state: State.Finished( - result: result, - fulfilledExpectations: waitingState.fulfilledExpectations, - unfulfilledExpectations: unfulfilledExpectations - )) - - if cancelPrimitiveWait { - self.cancelPrimitiveWait() - } - - if let delegateBlock = delegateBlock, let delegate = _delegate { - delegateQueue.async { - delegateBlock(delegate) - } - } - } - - private func expectationWasFulfilled(_ expectation: XCTestExpectation) { - XCTWaiter.subsystemQueue.sync { - // If already finished, do nothing - guard case var .waiting(waitingState) = state else { return } - - waitingState.fulfilledExpectations.append(expectation) - queue_validateExpectationFulfillment(dueToTimeout: false) - } - } - -} - -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 - // by the `timeout` argument. Only run for a short time in case `cancelPrimitiveWait()` was called and - // issued `CFRunLoopStop` just before we reach this point. - let timeIntervalToRun = min(0.1, timeout) - - // RunLoop.run(mode:before:) should have @discardableResult - _ = runLoop.run(mode: .default, before: Date(timeIntervalSinceNow: timeIntervalToRun)) - } - - func cancelPrimitiveWait() { - guard let runLoop = runLoop else { return } -#if os(Windows) - runLoop._stop() -#else - CFRunLoopStop(runLoop.getCFRunLoop()) -#endif - } -} - -extension XCTWaiter: Equatable { - public static func == (lhs: XCTWaiter, rhs: XCTWaiter) -> Bool { - return lhs === rhs - } -} - -extension XCTWaiter: CustomStringConvertible { - public var description: String { - return XCTWaiter.subsystemQueue.sync { - let expectationsString = state.allExpectations.map { "'\($0.queue_expectationDescription)'" }.joined(separator: ", ") - - return "" - } - } -} - -extension XCTWaiter: ManageableWaiter { - var isFinished: Bool { - return XCTWaiter.subsystemQueue.sync { - switch state { - case .ready, .waiting: return false - case .finished: return true - } - } - } - - func queue_handleWatchdogTimeout() { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - - queue_validateExpectationFulfillment(dueToTimeout: true) - manager!.queue_handleWatchdogTimeout(of: self) - cancelPrimitiveWait() - } - - func queue_interrupt(for interruptingWaiter: XCTWaiter) { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - - queue_finish(result: .interrupted, cancelPrimitiveWait: true) { delegate in - delegate.nestedWaiter(self, wasInterruptedByTimedOutWaiter: interruptingWaiter) - } - } -} diff --git a/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift b/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift deleted file mode 100644 index f1b032b34..000000000 --- a/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift +++ /dev/null @@ -1,240 +0,0 @@ -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2018 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 -// -// -// XCTestCase+Asynchronous.swift -// Methods on XCTestCase for testing asynchronous operations -// - -public extension XCTestCase { - - /// Creates a point of synchronization in the flow of a test. Only one - /// "wait" can be active at any given time, but multiple discrete sequences - /// of { expectations -> wait } can be chained together. The related - /// XCTWaiter API allows multiple "nested" waits if that is required. - /// - /// - Parameter timeout: The amount of time within which all expectation - /// must be fulfilled. - /// - Parameter file: The file name to use in the error message if - /// expectations are not met before the given timeout. Default is the file - /// containing the call to this method. It is rare to provide this - /// parameter when calling this method. - /// - Parameter line: The line number to use in the error message if the - /// expectations are not met 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. - /// - Parameter handler: If provided, the handler will be invoked both on - /// timeout or fulfillment of all expectations. Timeout is always treated - /// as a test failure. - /// - /// - SeeAlso: XCTWaiter - /// - /// - Note: Whereas Objective-C XCTest determines the file and line - /// number of the "wait" call using symbolication, this implementation - /// opts to take `file` and `line` as parameters instead. As a result, - /// the interface to these methods are not exactly identical between - /// 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`. - func waitForExpectations(timeout: TimeInterval, file: StaticString = #file, line: Int = #line, handler: XCWaitCompletionHandler? = nil) { - precondition(Thread.isMainThread, "\(#function) must be called on the main thread") - if currentWaiter != nil { - return recordFailure(description: "API violation - calling wait on test case while already waiting.", at: SourceLocation(file: file, line: line), expected: false) - } - let expectations = self.expectations - if expectations.isEmpty { - return recordFailure(description: "API violation - call made to wait without any expectations having been set.", at: SourceLocation(file: file, line: line), expected: false) - } - - let waiter = XCTWaiter(delegate: self) - currentWaiter = waiter - - let waiterResult = waiter.wait(for: expectations, timeout: timeout, file: file, line: line) - - currentWaiter = nil - - cleanUpExpectations() - - // The handler is invoked regardless of whether the test passed. - if let handler = handler { - let error = (waiterResult == .completed) ? nil : XCTestError(.timeoutWhileWaiting) - handler(error) - } - } - - /// Wait on an array of expectations for up to the specified timeout, and optionally specify whether they - /// must be fulfilled in the given order. May return early based on fulfillment of the waited on expectations. - /// - /// - Parameter expectations: The expectations to wait on. - /// - Parameter timeout: The maximum total time duration to wait on all expectations. - /// - Parameter enforceOrder: Specifies whether the expectations must be fulfilled in the order - /// they are specified in the `expectations` Array. Default is false. - /// - Parameter file: The file name to use in the error message if - /// expectations are not fulfilled before the given timeout. Default is the file - /// containing the call to this method. It is rare to provide this - /// parameter when calling this method. - /// - Parameter line: The line number to use in the error message if the - /// 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. - /// - /// - SeeAlso: XCTWaiter - func wait(for expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) { - let waiter = XCTWaiter(delegate: self) - waiter.wait(for: expectations, timeout: timeout, enforceOrder: enforceOrder, file: file, line: line) - - cleanUpExpectations(expectations) - } - - /// Creates and returns an expectation associated with the test case. - /// - /// - Parameter description: This string will be displayed in the test log - /// to help diagnose failures. - /// - Parameter file: The file name to use in the error message if - /// this expectation is not waited for. Default is the file - /// containing the call to this method. It is rare to provide this - /// parameter when calling this method. - /// - Parameter line: The line number to use in the error message if the - /// this expectation is not waited for. 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. - /// - /// - Note: Whereas Objective-C XCTest determines the file and line - /// number of expectations that are created by using symbolication, this - /// implementation opts to take `file` and `line` as parameters instead. - /// As a result, the interface to these methods are not exactly identical - /// between 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`. - @discardableResult func expectation(description: String, file: StaticString = #file, line: Int = #line) -> XCTestExpectation { - let expectation = XCTestExpectation(description: description, file: file, line: line) - addExpectation(expectation) - return expectation - } - - /// Creates and returns an expectation for a notification. - /// - /// - Parameter notificationName: The name of the notification the - /// expectation observes. - /// - Parameter object: The object whose notifications the expectation will - /// receive; that is, only notifications with this object are observed by - /// the test case. If you pass nil, the expectation doesn't use - /// a notification's object to decide whether it is fulfilled. - /// - Parameter notificationCenter: The specific notification center that - /// the notification will be posted to. - /// - Parameter handler: If provided, the handler will be invoked when the - /// notification is observed. It will not be invoked on timeout. Use the - /// handler to further investigate if the notification fulfills the - /// expectation. - @discardableResult func expectation(forNotification notificationName: Notification.Name, object: Any? = nil, notificationCenter: NotificationCenter = .default, file: StaticString = #file, line: Int = #line, handler: XCTNSNotificationExpectation.Handler? = nil) -> XCTestExpectation { - let expectation = XCTNSNotificationExpectation(name: notificationName, object: object, notificationCenter: notificationCenter, file: file, line: line) - expectation.handler = handler - addExpectation(expectation) - return expectation - } - - /// Creates and returns an expectation for a notification. - /// - /// - Parameter notificationName: The name of the notification the - /// expectation observes. - /// - Parameter object: The object whose notifications the expectation will - /// receive; that is, only notifications with this object are observed by - /// the test case. If you pass nil, the expectation doesn't use - /// a notification's object to decide whether it is fulfilled. - /// - Parameter notificationCenter: The specific notification center that - /// the notification will be posted to. - /// - Parameter handler: If provided, the handler will be invoked when the - /// notification is observed. It will not be invoked on timeout. Use the - /// handler to further investigate if the notification fulfills the - /// expectation. - @discardableResult func expectation(forNotification notificationName: String, object: Any? = nil, notificationCenter: NotificationCenter = .default, file: StaticString = #file, line: Int = #line, handler: XCTNSNotificationExpectation.Handler? = nil) -> XCTestExpectation { - return expectation(forNotification: Notification.Name(rawValue: notificationName), object: object, notificationCenter: notificationCenter, file: file, line: line, handler: handler) - } - - /// Creates and returns an expectation that is fulfilled if the predicate - /// returns true when evaluated with the given object. The expectation - /// periodically evaluates the predicate and also may use notifications or - /// other events to optimistically re-evaluate. - /// - /// - Parameter predicate: The predicate that will be used to evaluate the - /// object. - /// - Parameter object: The object that is evaluated against the conditions - /// specified by the predicate, if any. Default is nil. - /// - Parameter file: The file name to use in the error message if - /// this expectation is not waited for. Default is the file - /// containing the call to this method. It is rare to provide this - /// parameter when calling this method. - /// - Parameter line: The line number to use in the error message if the - /// this expectation is not waited for. 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. - /// - Parameter handler: A block to be invoked when evaluating the predicate - /// against the object returns true. If the block is not provided the - /// first successful evaluation will fulfill the expectation. If provided, - /// the handler can override that behavior which leaves the caller - /// responsible for fulfilling the expectation. - @discardableResult func expectation(for predicate: NSPredicate, evaluatedWith object: Any? = nil, file: StaticString = #file, line: Int = #line, handler: XCTNSPredicateExpectation.Handler? = nil) -> XCTestExpectation { - let expectation = XCTNSPredicateExpectation(predicate: predicate, object: object, file: file, line: line) - expectation.handler = handler - addExpectation(expectation) - return expectation - } - -} - -/// A block to be invoked when a call to wait times out or has had all -/// associated expectations fulfilled. -/// -/// - Parameter error: If the wait timed out or a failure was raised while -/// waiting, the error's code will specify the type of failure. Otherwise -/// error will be nil. -public typealias XCWaitCompletionHandler = (Error?) -> () - -extension XCTestCase: XCTWaiterDelegate { - - public func waiter(_ waiter: XCTWaiter, didTimeoutWithUnfulfilledExpectations unfulfilledExpectations: [XCTestExpectation]) { - let expectationDescription = unfulfilledExpectations.map { $0.expectationDescription }.joined(separator: ", ") - let failureDescription = "Asynchronous wait failed - Exceeded timeout of \(waiter.timeout) seconds, with unfulfilled expectations: \(expectationDescription)" - recordFailure(description: failureDescription, at: waiter.waitSourceLocation ?? .unknown, expected: true) - } - - public func waiter(_ waiter: XCTWaiter, fulfillmentDidViolateOrderingConstraintsFor expectation: XCTestExpectation, requiredExpectation: XCTestExpectation) { - let failureDescription = "Failed due to expectation fulfilled in incorrect order: requires '\(requiredExpectation.expectationDescription)', actually fulfilled '\(expectation.expectationDescription)'" - recordFailure(description: failureDescription, at: expectation.fulfillmentSourceLocation ?? .unknown, expected: true) - } - - public func waiter(_ waiter: XCTWaiter, didFulfillInvertedExpectation expectation: XCTestExpectation) { - let failureDescription = "Asynchronous wait failed - Fulfilled inverted expectation '\(expectation.expectationDescription)'" - recordFailure(description: failureDescription, at: expectation.fulfillmentSourceLocation ?? .unknown, expected: true) - } - - public func nestedWaiter(_ waiter: XCTWaiter, wasInterruptedByTimedOutWaiter outerWaiter: XCTWaiter) { - let failureDescription = "Asynchronous waiter \(waiter) failed - Interrupted by timeout of containing waiter \(outerWaiter)" - recordFailure(description: failureDescription, at: waiter.waitSourceLocation ?? .unknown, expected: true) - } - -} - -internal extension XCTestCase { - // It is an API violation to create expectations but not wait for them to - // be completed. Notify the user of a mistake via a test failure. - func failIfExpectationsNotWaitedFor(_ expectations: [XCTestExpectation]) { - let orderedUnwaitedExpectations = expectations.filter { !$0.hasBeenWaitedOn }.sorted { $0.creationToken < $1.creationToken } - guard let expectationForFileLineReporting = orderedUnwaitedExpectations.first else { - return - } - - let expectationDescriptions = orderedUnwaitedExpectations.map { "'\($0.expectationDescription)'" }.joined(separator: ", ") - let failureDescription = "Failed due to unwaited expectation\(orderedUnwaitedExpectations.count > 1 ? "s" : "") \(expectationDescriptions)" - - recordFailure( - description: failureDescription, - at: expectationForFileLineReporting.creationSourceLocation, - expected: false) - } -} diff --git a/Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift b/Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift deleted file mode 100644 index afe435617..000000000 --- a/Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift +++ /dev/null @@ -1,329 +0,0 @@ -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2018 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 -// -// -// XCTestExpectation.swift -// - -/// Expectations represent specific conditions in asynchronous testing. -open class XCTestExpectation { - - private static var currentMonotonicallyIncreasingToken: UInt64 = 0 - private static func queue_nextMonotonicallyIncreasingToken() -> UInt64 { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - currentMonotonicallyIncreasingToken += 1 - return currentMonotonicallyIncreasingToken - } - private static func nextMonotonicallyIncreasingToken() -> UInt64 { - return XCTWaiter.subsystemQueue.sync { queue_nextMonotonicallyIncreasingToken() } - } - - /* - Rules for properties - ==================== - - XCTestExpectation has many properties, many of which require synchronization on `XCTWaiter.subsystemQueue`. - When adding properties, use the following rules for consistency. The naming guidelines aim to allow - property names to be as short & simple as possible, while maintaining the necessary synchronization. - - - If property is constant (`let`), it is immutable so there is no synchronization concern. - - No underscore prefix on name - - No matching `queue_` property - - If it is only used within this file: - - `private` access - - If is is used outside this file but not outside the module: - - `internal` access - - If it is used outside the module: - - `public` or `open` access, depending on desired overridability - - - If property is variable (`var`), it is mutable so access to it must be synchronized. - - `private` access - - If it is only used within this file: - - No underscore prefix on name - - No matching `queue_` property - - If is is used outside this file: - - If access outside this file is always on-queue: - - No underscore prefix on name - - Matching internal `queue_` property with `.onQueue` dispatchPreconditions - - If access outside this file is sometimes off-queue - - Underscore prefix on name - - Matching `internal` property with `queue_` prefix and `XCTWaiter.subsystemQueue` dispatchPreconditions - - Matching `internal` or `public` property without underscore prefix but with `XCTWaiter.subsystemQueue` synchronization - */ - - private var _expectationDescription: String - - internal let creationToken: UInt64 - internal let creationSourceLocation: SourceLocation - - private var isFulfilled = false - private var fulfillmentToken: UInt64 = 0 - private var _fulfillmentSourceLocation: SourceLocation? - - private var _expectedFulfillmentCount = 1 - private var numberOfFulfillments = 0 - - private var _isInverted = false - - private var _assertForOverFulfill = false - - private var _hasBeenWaitedOn = false - - private var _didFulfillHandler: (() -> Void)? - - /// A human-readable string used to describe the expectation in log output and test reports. - open var expectationDescription: String { - get { - return XCTWaiter.subsystemQueue.sync { queue_expectationDescription } - } - set { - XCTWaiter.subsystemQueue.sync { queue_expectationDescription = newValue } - } - } - - /// The number of times `fulfill()` must be called on the expectation in order for it - /// to report complete fulfillment to its waiter. Default is 1. - /// This value must be greater than 0 and is not meaningful if combined with `isInverted`. - open var expectedFulfillmentCount: Int { - get { - return XCTWaiter.subsystemQueue.sync { queue_expectedFulfillmentCount } - } - set { - precondition(newValue > 0, "API violation - fulfillment count must be greater than 0.") - - XCTWaiter.subsystemQueue.sync { - precondition(!queue_hasBeenWaitedOn, "API violation - cannot set expectedFulfillmentCount on '\(queue_expectationDescription)' after already waiting on it.") - queue_expectedFulfillmentCount = newValue - } - } - } - - /// If an expectation is set to be inverted, then fulfilling it will have a similar effect as - /// failing to fulfill a conventional expectation has, as handled by the waiter and its delegate. - /// Furthermore, waiters that wait on an inverted expectation will allow the full timeout to elapse - /// and not report timeout to the delegate if it is not fulfilled. - open var isInverted: Bool { - get { - return XCTWaiter.subsystemQueue.sync { queue_isInverted } - } - set { - XCTWaiter.subsystemQueue.sync { - precondition(!queue_hasBeenWaitedOn, "API violation - cannot set isInverted on '\(queue_expectationDescription)' after already waiting on it.") - queue_isInverted = newValue - } - } - } - - /// If set, calls to fulfill() after the expectation has already been fulfilled - exceeding the fulfillment - /// count - will cause a fatal error and halt process execution. Default is false (disabled). - /// - /// - Note: This is the legacy behavior of expectations created through APIs on the ObjC version of XCTestCase - /// because that version raises ObjC exceptions (which may be caught) instead of causing a fatal error. - /// In this version of XCTest, no expectation ever has this property set to true (enabled) by default, it - /// must be opted-in to explicitly. - open var assertForOverFulfill: Bool { - get { - return XCTWaiter.subsystemQueue.sync { _assertForOverFulfill } - } - set { - XCTWaiter.subsystemQueue.sync { - precondition(!queue_hasBeenWaitedOn, "API violation - cannot set assertForOverFulfill on '\(queue_expectationDescription)' after already waiting on it.") - _assertForOverFulfill = newValue - } - } - } - - internal var fulfillmentSourceLocation: SourceLocation? { - return XCTWaiter.subsystemQueue.sync { _fulfillmentSourceLocation } - } - - internal var hasBeenWaitedOn: Bool { - return XCTWaiter.subsystemQueue.sync { queue_hasBeenWaitedOn } - } - - internal var queue_expectationDescription: String { - get { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - return _expectationDescription - } - set { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - _expectationDescription = newValue - } - } - internal var queue_isFulfilled: Bool { - get { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - return isFulfilled - } - set { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - isFulfilled = newValue - } - } - internal var queue_fulfillmentToken: UInt64 { - get { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - return fulfillmentToken - } - set { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - fulfillmentToken = newValue - } - } - internal var queue_expectedFulfillmentCount: Int { - get { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - return _expectedFulfillmentCount - } - set { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - _expectedFulfillmentCount = newValue - } - } - internal var queue_isInverted: Bool { - get { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - return _isInverted - } - set { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - _isInverted = newValue - } - } - internal var queue_hasBeenWaitedOn: Bool { - get { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - return _hasBeenWaitedOn - } - set { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - _hasBeenWaitedOn = newValue - - if _hasBeenWaitedOn { - didBeginWaiting() - } - } - } - internal var queue_didFulfillHandler: (() -> Void)? { - get { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - return _didFulfillHandler - } - set { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - _didFulfillHandler = newValue - } - } - - /// Initializes a new expectation with a description of the condition it is checking. - /// - /// - Parameter description: A human-readable string used to describe the condition the expectation is checking. - public init(description: String = "no description provided", file: StaticString = #file, line: Int = #line) { - _expectationDescription = description - creationToken = XCTestExpectation.nextMonotonicallyIncreasingToken() - creationSourceLocation = SourceLocation(file: file, line: line) - } - - /// Marks an expectation as having been met. It's an error to call this - /// method on an expectation that has already been fulfilled, or when the - /// test case that vended the expectation has already completed. - /// - /// - Parameter file: The file name to use in the error message if - /// expectations are not met before the given timeout. Default is the file - /// containing the call to this method. It is rare to provide this - /// parameter when calling this method. - /// - Parameter line: The line number to use in the error message if the - /// expectations are not met 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. - /// - /// - Note: Whereas Objective-C XCTest determines the file and line - /// number the expectation was fulfilled using symbolication, this - /// implementation opts to take `file` and `line` as parameters instead. - /// As a result, the interface to these methods are not exactly identical - /// between 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`. - open func fulfill(_ file: StaticString = #file, line: Int = #line) { - let sourceLocation = SourceLocation(file: file, line: line) - - let didFulfillHandler: (() -> Void)? = XCTWaiter.subsystemQueue.sync { - // FIXME: Objective-C XCTest emits failures when expectations are - // fulfilled after the test cases that generated those - // expectations have completed. Similarly, this should cause an - // error as well. - - if queue_isFulfilled { - // FIXME: No regression tests exist for this feature. We may break it - // without ever realizing (similar to `continueAfterFailure`). - if _assertForOverFulfill { - fatalError("API violation - multiple calls made to fulfill() for \(queue_expectationDescription)") - } - - if let testCase = XCTCurrentTestCase { - testCase.recordFailure( - description: "API violation - multiple calls made to XCTestExpectation.fulfill() for \(queue_expectationDescription).", - at: sourceLocation, - expected: false) - } - return nil - } - - if queue_fulfill(sourceLocation: sourceLocation) { - return queue_didFulfillHandler - } else { - return nil - } - } - - didFulfillHandler?() - } - - private func queue_fulfill(sourceLocation: SourceLocation) -> Bool { - dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) - - numberOfFulfillments += 1 - - if numberOfFulfillments == queue_expectedFulfillmentCount { - queue_isFulfilled = true - _fulfillmentSourceLocation = sourceLocation - queue_fulfillmentToken = XCTestExpectation.queue_nextMonotonicallyIncreasingToken() - return true - } else { - return false - } - } - - internal func didBeginWaiting() { - // Override point for subclasses - } - - internal func cleanUp() { - // Override point for subclasses - } - -} - -extension XCTestExpectation: Equatable { - public static func == (lhs: XCTestExpectation, rhs: XCTestExpectation) -> Bool { - return lhs === rhs - } -} - -extension XCTestExpectation: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(ObjectIdentifier(self)) - } -} - -extension XCTestExpectation: CustomStringConvertible { - public var description: String { - return expectationDescription - } -} diff --git a/Sources/XCTest/Public/XCTestCase+Performance.swift b/Sources/XCTest/Public/XCTestCase+Performance.swift deleted file mode 100644 index 401fb5e9c..000000000 --- a/Sources/XCTest/Public/XCTestCase+Performance.swift +++ /dev/null @@ -1,176 +0,0 @@ -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2016 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 -// -// -// XCTestCase+Performance.swift -// Methods on XCTestCase for testing the performance of code blocks. -// - -public struct XCTPerformanceMetric : RawRepresentable, Equatable, Hashable { - public let rawValue: String - - public init(_ rawValue: String) { - self.rawValue = rawValue - } - - public init(rawValue: String) { - self.rawValue = rawValue - } -} - -public extension XCTPerformanceMetric { - /// Records wall clock time in seconds between `startMeasuring`/`stopMeasuring`. - static let wallClockTime = XCTPerformanceMetric(rawValue: WallClockTimeMetric.name) -} - -/// The following methods are called from within a test method to carry out -/// performance testing on blocks of code. -public extension XCTestCase { - - /// The names of the performance metrics to measure when invoking `measure(block:)`. - /// Returns `XCTPerformanceMetric_WallClockTime` by default. Subclasses can - /// override this to change the behavior of `measure(block:)` - class var defaultPerformanceMetrics: [XCTPerformanceMetric] { - return [.wallClockTime] - } - - /// Call from a test method to measure resources (`defaultPerformanceMetrics`) - /// used by the block in the current process. - /// - /// func testPerformanceOfMyFunction() { - /// measure { - /// // Do that thing you want to measure. - /// MyFunction(); - /// } - /// } - /// - /// - Parameter block: A block whose performance to measure. - /// - Bug: The `block` param should have no external label, but there seems - /// to be a swiftc bug that causes issues when such a parameter comes - /// after a defaulted arg. See https://bugs.swift.org/browse/SR-1483 This - /// API incompatibility with Apple XCTest can be worked around in practice - /// by using trailing closure syntax when calling this method. - /// - Note: Whereas Apple XCTest determines the file and line number of - /// measurements by using symbolication, this implementation opts to take - /// `file` and `line` as parameters instead. As a result, the interface to - /// these methods are not exactly identical between 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`. - func measure(file: StaticString = #file, line: Int = #line, block: () -> Void) { - measureMetrics(type(of: self).defaultPerformanceMetrics, - automaticallyStartMeasuring: true, - file: file, - line: line, - for: block) - } - - /// Call from a test method to measure resources (XCTPerformanceMetrics) used - /// by the block in the current process. Each metric will be measured across - /// calls to the block. The number of times the block will be called is undefined - /// and may change in the future. For one example of why, as long as the requested - /// performance metrics do not interfere with each other the API will measure - /// all metrics across the same calls to the block. If the performance metrics - /// may interfere the API will measure them separately. - /// - /// func testMyFunction2_WallClockTime() { - /// measureMetrics(type(of: self).defaultPerformanceMetrics, automaticallyStartMeasuring: false) { - /// - /// // Do setup work that needs to be done for every iteration but - /// // you don't want to measure before the call to `startMeasuring()` - /// SetupSomething(); - /// self.startMeasuring() - /// - /// // Do that thing you want to measure. - /// MyFunction() - /// self.stopMeasuring() - /// - /// // Do teardown work that needs to be done for every iteration - /// // but you don't want to measure after the call to `stopMeasuring()` - /// TeardownSomething() - /// } - /// } - /// - /// Caveats: - /// * If `true` was passed for `automaticallyStartMeasuring` and `startMeasuring()` - /// is called anyway, the test will fail. - /// * If `false` was passed for `automaticallyStartMeasuring` then `startMeasuring()` - /// must be called once and only once before the end of the block or the test will fail. - /// * If `stopMeasuring()` is called multiple times during the block the test will fail. - /// - /// - Parameter metrics: An array of Strings (XCTPerformanceMetrics) to measure. - /// Providing an unrecognized string is a test failure. - /// - Parameter automaticallyStartMeasuring: If `false`, `XCTestCase` will - /// not take any measurements until -startMeasuring is called. - /// - Parameter block: A block whose performance to measure. - /// - Note: Whereas Apple XCTest determines the file and line number of - /// measurements by using symbolication, this implementation opts to take - /// `file` and `line` as parameters instead. As a result, the interface to - /// these methods are not exactly identical between 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`. - func measureMetrics(_ metrics: [XCTPerformanceMetric], automaticallyStartMeasuring: Bool, file: StaticString = #file, line: Int = #line, for block: () -> Void) { - guard _performanceMeter == nil else { - return recordAPIViolation(description: "Can only record one set of metrics per test method.", file: file, line: line) - } - - PerformanceMeter.measureMetrics(metrics.map({ $0.rawValue }), delegate: self, file: file, line: line) { meter in - self._performanceMeter = meter - if automaticallyStartMeasuring { - meter.startMeasuring(file: file, line: line) - } - block() - } - } - - /// Call this from within a measure block to set the beginning of the critical - /// section. Measurement of metrics will start at this point. - /// - Note: Whereas Apple XCTest determines the file and line number of - /// measurements by using symbolication, this implementation opts to take - /// `file` and `line` as parameters instead. As a result, the interface to - /// these methods are not exactly identical between 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`. - func startMeasuring(file: StaticString = #file, line: Int = #line) { - guard let performanceMeter = _performanceMeter, !performanceMeter.didFinishMeasuring else { - return recordAPIViolation(description: "Cannot start measuring. startMeasuring() is only supported from a block passed to measureMetrics(...).", file: file, line: line) - } - performanceMeter.startMeasuring(file: file, line: line) - } - - /// Call this from within a measure block to set the ending of the critical - /// section. Measurement of metrics will stop at this point. - /// - Note: Whereas Apple XCTest determines the file and line number of - /// measurements by using symbolication, this implementation opts to take - /// `file` and `line` as parameters instead. As a result, the interface to - /// these methods are not exactly identical between 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`. - func stopMeasuring(file: StaticString = #file, line: Int = #line) { - guard let performanceMeter = _performanceMeter, !performanceMeter.didFinishMeasuring else { - return recordAPIViolation(description: "Cannot stop measuring. stopMeasuring() is only supported from a block passed to measureMetrics(...).", file: file, line: line) - } - performanceMeter.stopMeasuring(file: file, line: line) - } -} - -extension XCTestCase: PerformanceMeterDelegate { - internal func recordAPIViolation(description: String, file: StaticString, line: Int) { - recordFailure(withDescription: "API violation - \(description)", - inFile: String(describing: file), - atLine: line, - expected: false) - } - - internal func recordMeasurements(results: String, file: StaticString, line: Int) { - XCTestObservationCenter.shared.testCase(self, didMeasurePerformanceResults: results, file: file, line: line) - } - - internal func recordFailure(description: String, file: StaticString, line: Int) { - recordFailure(withDescription: "failed: " + description, inFile: String(describing: file), atLine: line, expected: true) - } -} diff --git a/Sources/XCTest/Public/XCTestErrors.swift b/Sources/XCTest/Public/XCTestErrors.swift index a18af0afb..00fc3811c 100644 --- a/Sources/XCTest/Public/XCTestErrors.swift +++ b/Sources/XCTest/Public/XCTestErrors.swift @@ -11,6 +11,14 @@ // Constants used in errors produced by the XCTest library. // +#if os(WASI) +public struct XCTestError { + public enum Code : Int { + case timeoutWhileWaiting + case failureWhileWaiting + } +} +#else /// The domain used by errors produced by the XCTest library. public let XCTestErrorDomain = "org.swift.XCTestErrorDomain" @@ -32,6 +40,7 @@ public struct XCTestError : _BridgedStoredNSError { case failureWhileWaiting } } +#endif public extension XCTestError { /// Indicates that one or more expectations failed to be fulfilled in time diff --git a/Sources/XCTest/Public/XCTestMain.swift b/Sources/XCTest/Public/XCTestMain.swift index 4ce0ee89a..58f32b0f2 100644 --- a/Sources/XCTest/Public/XCTestMain.swift +++ b/Sources/XCTest/Public/XCTestMain.swift @@ -20,7 +20,9 @@ #else @_exported import SwiftFoundation #endif -#elseif !os(WASI) +#elseif os(WASI) + import WASIFoundation +#else @_exported import Foundation #endif diff --git a/Sources/XCTest/Public/XCTestObservation.swift b/Sources/XCTest/Public/XCTestObservation.swift index 9e7a463c9..7a1849cfa 100644 --- a/Sources/XCTest/Public/XCTestObservation.swift +++ b/Sources/XCTest/Public/XCTestObservation.swift @@ -66,11 +66,13 @@ public protocol XCTestObservation: AnyObject { // All `XCTestObservation` methods are optional, so empty default implementations are provided public extension XCTestObservation { + #if !os(WASI) func testBundleWillStart(_ testBundle: Bundle) {} + func testBundleDidFinish(_ testBundle: Bundle) {} + #endif func testSuiteWillStart(_ testSuite: XCTestSuite) {} func testCaseWillStart(_ testCase: XCTestCase) {} func testCase(_ testCase: XCTestCase, didFailWithDescription description: String, inFile filePath: String?, atLine lineNumber: Int) {} func testCaseDidFinish(_ testCase: XCTestCase) {} func testSuiteDidFinish(_ testSuite: XCTestSuite) {} - func testBundleDidFinish(_ testBundle: Bundle) {} } diff --git a/Sources/XCTest/Public/XCTestObservationCenter.swift b/Sources/XCTest/Public/XCTestObservationCenter.swift index 540956dd5..7e86b85ff 100644 --- a/Sources/XCTest/Public/XCTestObservationCenter.swift +++ b/Sources/XCTest/Public/XCTestObservationCenter.swift @@ -38,9 +38,11 @@ public class XCTestObservationCenter { observers.remove(testObserver.wrapper) } +#if !os(WASI) internal func testBundleWillStart(_ testBundle: Bundle) { forEachObserver { $0.testBundleWillStart(testBundle) } } +#endif internal func testSuiteWillStart(_ testSuite: XCTestSuite) { forEachObserver { $0.testSuiteWillStart(testSuite) } @@ -66,9 +68,11 @@ public class XCTestObservationCenter { forEachObserver { $0.testSuiteDidFinish(testSuite) } } +#if !os(WASI) internal func testBundleDidFinish(_ testBundle: Bundle) { forEachObserver { $0.testBundleDidFinish(testBundle) } } +#endif internal func testCase(_ testCase: XCTestCase, didMeasurePerformanceResults results: String, file: StaticString, line: Int) { forEachInternalObserver { $0.testCase(testCase, didMeasurePerformanceResults: results, file: file, line: line) } diff --git a/Sources/XCTest/Public/XCTestRun.swift b/Sources/XCTest/Public/XCTestRun.swift index 4ca345268..f3a7f2fb1 100644 --- a/Sources/XCTest/Public/XCTestRun.swift +++ b/Sources/XCTest/Public/XCTestRun.swift @@ -11,6 +11,10 @@ // A test run collects information about the execution of a test. // +#if canImport(WASIFoundation) +import WASIFoundation +#endif + /// A test run collects information about the execution of a test. Failures in /// explicit test assertions are classified as "expected", while failures from /// unrelated or uncaught exceptions are classified as "unexpected". diff --git a/Sources/XCTest/Public/XCTestSuiteRun.swift b/Sources/XCTest/Public/XCTestSuiteRun.swift index 666a3b069..f2295ab29 100644 --- a/Sources/XCTest/Public/XCTestSuiteRun.swift +++ b/Sources/XCTest/Public/XCTestSuiteRun.swift @@ -11,6 +11,10 @@ // A test run for an `XCTestSuite`. // +#if canImport(WASIFoundation) +import WASIFoundation +#endif + /// A test run for an `XCTestSuite`. open class XCTestSuiteRun: XCTestRun { /// The combined `testDuration` of each test case run in the suite. From 011030c98c7dbe24386e23e3aac401984583b3ed Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sat, 18 Apr 2020 22:55:31 +0100 Subject: [PATCH 03/26] Fix compilation for WASI --- Package.resolved | 11 +++++++++- Package.swift | 3 ++- Sources/XCTest/Private/PrintObserver.swift | 2 ++ Sources/XCTest/Private/TestListing.swift | 24 ++++++++++++---------- Sources/XCTest/Public/XCTestCase.swift | 19 ++++++++++++++++- Sources/XCTest/Public/XCTestMain.swift | 10 +++++++++ 6 files changed, 55 insertions(+), 14 deletions(-) diff --git a/Package.resolved b/Package.resolved index 47bdc3a72..f9d96ef11 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "AnyCodable", + "repositoryURL": "https://github.com/MaxDesiatov/AnyCodable", + "state": { + "branch": "master", + "revision": "6324b004d13f203a56ac27d650932e0e14286adc", + "version": null + } + }, { "package": "pure-swift-json", "repositoryURL": "https://github.com/fabianfett/pure-swift-json.git", @@ -15,7 +24,7 @@ "repositoryURL": "https://github.com/MaxDesiatov/WASIFoundation.git", "state": { "branch": "master", - "revision": "87665a15b3c396b7182a571dca65fe9e8547b9c1", + "revision": "a533d932063fcc83c23a8673cb8141c7ee1b1503", "version": null } } diff --git a/Package.swift b/Package.swift index 527ba02a5..d256fafc0 100644 --- a/Package.swift +++ b/Package.swift @@ -17,8 +17,9 @@ let package = Package( dependencies: [ .package(url: "https://github.com/fabianfett/pure-swift-json.git", .upToNextMajor(from: "0.2.1")), .package(url: "https://github.com/MaxDesiatov/WASIFoundation.git", .branch("master")), + .package(url: "https://github.com/MaxDesiatov/AnyCodable", .branch("master")), ], targets: [ - .target(name: "XCTest", dependencies: ["WASIFoundation"], path: "Sources"), + .target(name: "XCTest", dependencies: ["AnyCodable", "WASIFoundation", "PureSwiftJSONCoding"], path: "Sources"), ] ) diff --git a/Sources/XCTest/Private/PrintObserver.swift b/Sources/XCTest/Private/PrintObserver.swift index a9b893779..bed4700e0 100644 --- a/Sources/XCTest/Private/PrintObserver.swift +++ b/Sources/XCTest/Private/PrintObserver.swift @@ -80,7 +80,9 @@ internal class PrintObserver: XCTestObservation { private lazy var dateFormatter: DateFormatter = { let formatter = DateFormatter() +#if !os(WASI) formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" +#endif return formatter }() diff --git a/Sources/XCTest/Private/TestListing.swift b/Sources/XCTest/Private/TestListing.swift index 0f9bb5737..63ea15b92 100644 --- a/Sources/XCTest/Private/TestListing.swift +++ b/Sources/XCTest/Private/TestListing.swift @@ -11,6 +11,10 @@ // Implementation of the mode for printing the list of tests. // +import AnyCodable +import PureSwiftJSONCoding +import WASIFoundation + internal struct TestListing { private let testSuite: XCTestSuite @@ -35,14 +39,14 @@ internal struct TestListing { /// tree representation of test suites and test cases. This output is intended /// to be consumed by other tools. func printTestJSON() { - let json = try! JSONSerialization.data(withJSONObject: testSuite.dictionaryRepresentation()) + let json = Data(try! JSONEncoder().encode(testSuite.dictionaryRepresentation())) print(String(data: json, encoding: .utf8)!) } } protocol Listable { func list() -> [String] - func dictionaryRepresentation() -> [String: Any] + func dictionaryRepresentation() -> [String: AnyEncodable] } private func moduleName(value: Any) -> String { @@ -68,12 +72,11 @@ extension XCTestSuite: Listable { return listables.flatMap({ $0.list() }) } - func dictionaryRepresentation() -> [String: Any] { - let listedTests = NSArray(array: tests.compactMap({ ($0 as? Listable)?.dictionaryRepresentation() })) - return NSDictionary(objects: [NSString(string: listingName), - listedTests], - forKeys: [NSString(string: "name"), - NSString(string: "tests")]) + func dictionaryRepresentation() -> [String: AnyEncodable] { + [ + "name": AnyEncodable(listingName), + "tests": AnyEncodable(tests.compactMap { ($0 as? Listable)?.dictionaryRepresentation() }) + ] } func findBundleTestSuite() -> XCTestSuite? { @@ -93,8 +96,7 @@ extension XCTestCase: Listable { return ["\(moduleName(value: self)).\(adjustedName)"] } - func dictionaryRepresentation() -> [String: Any] { - let methodName = String(name.split(separator: ".").last!) - return NSDictionary(object: NSString(string: methodName), forKey: NSString(string: "name")) + func dictionaryRepresentation() -> [String: AnyEncodable] { + ["name": AnyEncodable(String(name.split(separator: ".").last!))] } } diff --git a/Sources/XCTest/Public/XCTestCase.swift b/Sources/XCTest/Public/XCTestCase.swift index 947a35db7..0819fbf3a 100644 --- a/Sources/XCTest/Public/XCTestCase.swift +++ b/Sources/XCTest/Public/XCTestCase.swift @@ -48,6 +48,7 @@ open class XCTestCase: XCTest { return 1 } +#if !os(WASI) internal var currentWaiter: XCTWaiter? /// The set of expectations made upon this test case. @@ -86,6 +87,7 @@ open class XCTestCase: XCTest { /// An internal object implementing performance measurements. internal var _performanceMeter: PerformanceMeter? +#endif open override var testRunClass: AnyClass? { return XCTestCaseRun.self @@ -99,7 +101,9 @@ open class XCTestCase: XCTest { XCTCurrentTestCase = self testRun.start() invokeTest() + #if !os(WASI) failIfExpectationsNotWaitedFor(_allExpectations) + #endif testRun.stop() XCTCurrentTestCase = nil } @@ -160,7 +164,9 @@ open class XCTestCase: XCTest { atLine: lineNumber, expected: expected) +#if !os(WASI) _performanceMeter?.abortMeasuring() +#endif // FIXME: Apple XCTest does not throw a fatal error and crash the test // process, it merely prevents the remainder of a testClosure @@ -197,15 +203,21 @@ open class XCTestCase: XCTest { private var teardownBlocks: [() -> Void] = [] private var teardownBlocksDequeued: Bool = false + #if !os(WASI) private let teardownBlocksQueue: DispatchQueue = DispatchQueue(label: "org.swift.XCTest.XCTestCase.teardownBlocks") + #endif /// Registers a block of teardown code to be run after the current test /// method ends. open func addTeardownBlock(_ block: @escaping () -> Void) { + #if os(WASI) + teardownBlocks.append(block) + #else teardownBlocksQueue.sync { precondition(!self.teardownBlocksDequeued, "API violation -- attempting to add a teardown block after teardown blocks have been dequeued") self.teardownBlocks.append(block) } + #endif } private func performSetUpSequence() { @@ -243,12 +255,17 @@ open class XCTestCase: XCTest { } private func runTeardownBlocks() { - let blocks = teardownBlocksQueue.sync { () -> [() -> Void] in + let closure = { () -> [() -> Void] in self.teardownBlocksDequeued = true let blocks = self.teardownBlocks self.teardownBlocks = [] return blocks } + #if os(WASI) + let blocks = closure() + #else + let blocks = teardownBlocksQueue.sync(closure) + #endif for block in blocks.reversed() { block() diff --git a/Sources/XCTest/Public/XCTestMain.swift b/Sources/XCTest/Public/XCTestMain.swift index 58f32b0f2..2f3f4525c 100644 --- a/Sources/XCTest/Public/XCTestMain.swift +++ b/Sources/XCTest/Public/XCTestMain.swift @@ -65,7 +65,9 @@ public func XCTMain(_ testCases: [XCTestCaseEntry]) -> Never { XCTMain(testCases, arguments: CommandLine.arguments) } public func XCTMain(_ testCases: [XCTestCaseEntry], arguments: [String]) -> Never { + #if !os(WASI) let testBundle = Bundle.main + #endif let executionMode = ArgumentParser(arguments: arguments).executionMode @@ -77,7 +79,11 @@ public func XCTMain(_ testCases: [XCTestCaseEntry], arguments: [String]) -> Neve let currentTestSuite: XCTestSuite if executionMode.selectedTestNames == nil { rootTestSuite = XCTestSuite(name: "All tests") + #if !os(WASI) currentTestSuite = XCTestSuite(name: "\(testBundle.bundleURL.lastPathComponent).xctest") + #else + currentTestSuite = XCTestSuite(name: "testBundle.xctest") + #endif rootTestSuite.addTest(currentTestSuite) } else { rootTestSuite = XCTestSuite(name: "Selected tests") @@ -132,9 +138,13 @@ public func XCTMain(_ testCases: [XCTestCaseEntry], arguments: [String]) -> Neve let observationCenter = XCTestObservationCenter.shared observationCenter.addTestObserver(PrintObserver()) +#if !os(WASI) observationCenter.testBundleWillStart(testBundle) +#endif rootTestSuite.run() +#if !os(WASI) observationCenter.testBundleDidFinish(testBundle) +#endif exit(rootTestSuite.testRun!.totalFailureCount == 0 ? EXIT_SUCCESS : EXIT_FAILURE) } From 2520c4bbb4c340597ca4c672071cdce53a953c51 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 3 Jun 2020 17:52:51 +0100 Subject: [PATCH 04/26] Switch back to Foundation from WASIFoundation --- Package.resolved | 34 ------------------------ Package.swift | 5 +--- Sources/XCTest/Private/TestListing.swift | 24 ++++++++--------- Sources/XCTest/Public/XCTestMain.swift | 2 -- 4 files changed, 13 insertions(+), 52 deletions(-) delete mode 100644 Package.resolved diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index f9d96ef11..000000000 --- a/Package.resolved +++ /dev/null @@ -1,34 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "AnyCodable", - "repositoryURL": "https://github.com/MaxDesiatov/AnyCodable", - "state": { - "branch": "master", - "revision": "6324b004d13f203a56ac27d650932e0e14286adc", - "version": null - } - }, - { - "package": "pure-swift-json", - "repositoryURL": "https://github.com/fabianfett/pure-swift-json.git", - "state": { - "branch": null, - "revision": "816db7e4e35d584476ba52965224e4abf3517065", - "version": "0.2.1" - } - }, - { - "package": "WASIFoundation", - "repositoryURL": "https://github.com/MaxDesiatov/WASIFoundation.git", - "state": { - "branch": "master", - "revision": "a533d932063fcc83c23a8673cb8141c7ee1b1503", - "version": null - } - } - ] - }, - "version": 1 -} diff --git a/Package.swift b/Package.swift index d256fafc0..857e846ed 100644 --- a/Package.swift +++ b/Package.swift @@ -15,11 +15,8 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/fabianfett/pure-swift-json.git", .upToNextMajor(from: "0.2.1")), - .package(url: "https://github.com/MaxDesiatov/WASIFoundation.git", .branch("master")), - .package(url: "https://github.com/MaxDesiatov/AnyCodable", .branch("master")), ], targets: [ - .target(name: "XCTest", dependencies: ["AnyCodable", "WASIFoundation", "PureSwiftJSONCoding"], path: "Sources"), + .target(name: "XCTest", dependencies: [], path: "Sources"), ] ) diff --git a/Sources/XCTest/Private/TestListing.swift b/Sources/XCTest/Private/TestListing.swift index 63ea15b92..021c36bc9 100644 --- a/Sources/XCTest/Private/TestListing.swift +++ b/Sources/XCTest/Private/TestListing.swift @@ -11,9 +11,7 @@ // Implementation of the mode for printing the list of tests. // -import AnyCodable -import PureSwiftJSONCoding -import WASIFoundation +import Foundation internal struct TestListing { private let testSuite: XCTestSuite @@ -39,14 +37,14 @@ internal struct TestListing { /// tree representation of test suites and test cases. This output is intended /// to be consumed by other tools. func printTestJSON() { - let json = Data(try! JSONEncoder().encode(testSuite.dictionaryRepresentation())) + let json = try! JSONSerialization.data(withJSONObject: testSuite.dictionaryRepresentation()) print(String(data: json, encoding: .utf8)!) } } protocol Listable { func list() -> [String] - func dictionaryRepresentation() -> [String: AnyEncodable] + func dictionaryRepresentation() -> NSDictionary } private func moduleName(value: Any) -> String { @@ -72,11 +70,12 @@ extension XCTestSuite: Listable { return listables.flatMap({ $0.list() }) } - func dictionaryRepresentation() -> [String: AnyEncodable] { - [ - "name": AnyEncodable(listingName), - "tests": AnyEncodable(tests.compactMap { ($0 as? Listable)?.dictionaryRepresentation() }) - ] + func dictionaryRepresentation() -> NSDictionary { + let listedTests = NSArray(array: tests.compactMap({ ($0 as? Listable)?.dictionaryRepresentation() })) + return NSDictionary(objects: [NSString(string: listingName), + listedTests], + forKeys: [NSString(string: "name"), + NSString(string: "tests")]) } func findBundleTestSuite() -> XCTestSuite? { @@ -96,7 +95,8 @@ extension XCTestCase: Listable { return ["\(moduleName(value: self)).\(adjustedName)"] } - func dictionaryRepresentation() -> [String: AnyEncodable] { - ["name": AnyEncodable(String(name.split(separator: ".").last!))] + func dictionaryRepresentation() -> NSDictionary { + let methodName = String(name.split(separator: ".").last!) + return NSDictionary(object: NSString(string: methodName), forKey: NSString(string: "name")) } } diff --git a/Sources/XCTest/Public/XCTestMain.swift b/Sources/XCTest/Public/XCTestMain.swift index 2f3f4525c..909a8a8f9 100644 --- a/Sources/XCTest/Public/XCTestMain.swift +++ b/Sources/XCTest/Public/XCTestMain.swift @@ -20,8 +20,6 @@ #else @_exported import SwiftFoundation #endif -#elseif os(WASI) - import WASIFoundation #else @_exported import Foundation #endif From 99f1c825a92d7b8bcc5804955e8be4b23e39a7d8 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 3 Jun 2020 18:21:28 +0100 Subject: [PATCH 05/26] Add back removed files, adjust CMakeLists.txt --- CMakeLists.txt | 32 +- Sources/XCTest/Private/PerformanceMeter.swift | 202 +++++++++ Sources/XCTest/Private/WaiterManager.swift | 145 ++++++ .../XCTest/Private/WallClockTimeMetric.swift | 79 ++++ .../XCTNSNotificationExpectation.swift | 116 +++++ .../XCTNSPredicateExpectation.swift | 135 ++++++ .../Asynchronous/XCTWaiter+Validation.swift | 89 ++++ .../Public/Asynchronous/XCTWaiter.swift | 419 ++++++++++++++++++ .../XCTestCase+Asynchronous.swift | 240 ++++++++++ .../Asynchronous/XCTestExpectation.swift | 329 ++++++++++++++ .../Public/XCTestCase+Performance.swift | 176 ++++++++ 11 files changed, 1950 insertions(+), 12 deletions(-) create mode 100644 Sources/XCTest/Private/PerformanceMeter.swift create mode 100644 Sources/XCTest/Private/WaiterManager.swift create mode 100644 Sources/XCTest/Private/WallClockTimeMetric.swift create mode 100644 Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift create mode 100644 Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift create mode 100644 Sources/XCTest/Public/Asynchronous/XCTWaiter+Validation.swift create mode 100644 Sources/XCTest/Public/Asynchronous/XCTWaiter.swift create mode 100644 Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift create mode 100644 Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift create mode 100644 Sources/XCTest/Public/XCTestCase+Performance.swift diff --git a/CMakeLists.txt b/CMakeLists.txt index c7c7b57e9..a54770e63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,18 +19,30 @@ if(NOT CMAKE_SYSTEM_PROCESSOR STREQUAL wasm32) include(GNUInstallDirs) endif() -add_library(XCTest +set(XCTEST_WASI_UNAVAILABLE_SOURCES) +if(NOT CMAKE_SYSTEM_NAME STREQUAL WASI) +list(APPEND XCTEST_WASI_UNAVAILABLE_SOURCES Sources/XCTest/Private/WallClockTimeMetric.swift + Sources/XCTest/Private/PerformanceMeter.swift + Sources/XCTest/Private/WaiterManager.swift + Sources/XCTest/Public/XCTestCase+Performance.swift + Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift + Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift + Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift + Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift + Sources/XCTest/Public/Asynchronous/XCTWaiter.swift + Sources/XCTest/Public/Asynchronous/XCTWaiter+Validation.swift) +endif() + +add_library(XCTest Sources/XCTest/Private/TestListing.swift Sources/XCTest/Private/XCTestCaseSuite.swift Sources/XCTest/Private/TestFiltering.swift Sources/XCTest/Private/XCTestInternalObservation.swift Sources/XCTest/Private/ObjectWrapper.swift - Sources/XCTest/Private/PerformanceMeter.swift Sources/XCTest/Private/PrintObserver.swift Sources/XCTest/Private/ArgumentParser.swift Sources/XCTest/Private/SourceLocation.swift - Sources/XCTest/Private/WaiterManager.swift Sources/XCTest/Private/IgnoredErrors.swift Sources/XCTest/Public/XCTestRun.swift Sources/XCTest/Public/XCTestMain.swift @@ -42,15 +54,10 @@ add_library(XCTest Sources/XCTest/Public/XCTestCaseRun.swift Sources/XCTest/Public/XCAbstractTest.swift Sources/XCTest/Public/XCTestObservationCenter.swift - Sources/XCTest/Public/XCTestCase+Performance.swift Sources/XCTest/Public/XCTAssert.swift Sources/XCTest/Public/XCTSkip.swift - Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift - Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift - Sources/XCTest/Public/Asynchronous/XCTWaiter+Validation.swift - Sources/XCTest/Public/Asynchronous/XCTWaiter.swift - Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift - Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift) + + ${XCTEST_WASI_UNAVAILABLE_SOURCES}) if(USE_FOUNDATION_FRAMEWORK) target_compile_definitions(XCTest PRIVATE USE_FOUNDATION_FRAMEWORK) @@ -64,11 +71,12 @@ set_target_properties(XCTest PROPERTIES Swift_MODULE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/swift INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_BINARY_DIR}/swift) -if(CMAKE_SYSTEM_PROCESSOR STREQUAL wasm32) +if(CMAKE_SYSTEM_NAME STREQUAL WASI) target_compile_options(XCTest PRIVATE - -sdk ${WASI_SDK_PREFIX}/share/wasi-sysroot + -sdk ${CMAKE_SYSROOT} -target wasm32-unknown-wasi + -I ${SWIFT_FOUNDATION_PATH} ) endif() diff --git a/Sources/XCTest/Private/PerformanceMeter.swift b/Sources/XCTest/Private/PerformanceMeter.swift new file mode 100644 index 000000000..afb3e1918 --- /dev/null +++ b/Sources/XCTest/Private/PerformanceMeter.swift @@ -0,0 +1,202 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2016 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 +// +// +// PerformanceMeter.swift +// Measures the performance of a block of code and reports the results. +// + +/// Describes a type that is capable of measuring some aspect of code performance +/// over time. +internal protocol PerformanceMetric { + /// Called once per iteration immediately before the tested code is executed. + /// The metric should do whatever work is required to begin a new measurement. + func startMeasuring() + + /// Called once per iteration immediately after the tested code is executed. + /// The metric should do whatever work is required to finalize measurement. + func stopMeasuring() + + /// Called once, after all measurements have been taken, to provide feedback + /// about the collected measurements. + /// - Returns: Measurement results to present to the user. + func calculateResults() -> String + + /// Called once, after all measurements have been taken, to determine whether + /// the measurements should be treated as a test failure or not. + /// - Returns: A diagnostic message if the results indicate failure, else nil. + func failureMessage() -> String? +} + +/// Protocol used by `PerformanceMeter` to report measurement results +internal protocol PerformanceMeterDelegate { + /// Reports a string representation of the gathered performance metrics + /// - Parameter results: The raw measured values, and some derived data such + /// as average, and standard deviation + /// - Parameter file: The source file name where the measurement was invoked + /// - Parameter line: The source line number where the measurement was invoked + func recordMeasurements(results: String, file: StaticString, line: Int) + + /// Reports a test failure from the analysis of performance measurements. + /// This can currently be caused by an unexpectedly large standard deviation + /// calculated over the data. + /// - Parameter description: An explanation of the failure + /// - Parameter file: The source file name where the measurement was invoked + /// - Parameter line: The source line number where the measurement was invoked + func recordFailure(description: String, file: StaticString, line: Int) + + /// Reports a misuse of the `PerformanceMeter` API, such as calling ` + /// startMeasuring` multiple times. + /// - Parameter description: An explanation of the misuse + /// - Parameter file: The source file name where the misuse occurred + /// - Parameter line: The source line number where the misuse occurred + func recordAPIViolation(description: String, file: StaticString, line: Int) +} + +/// - Bug: This class is intended to be `internal` but is public to work around +/// a toolchain bug on Linux. See `XCTestCase._performanceMeter` for more info. +public final class PerformanceMeter { + enum Error: Swift.Error, CustomStringConvertible { + case noMetrics + case unknownMetric(metricName: String) + case startMeasuringAlreadyCalled + case stopMeasuringAlreadyCalled + case startMeasuringNotCalled + case stopBeforeStarting + + var description: String { + switch self { + case .noMetrics: return "At least one metric must be provided to measure." + case .unknownMetric(let name): return "Unknown metric: \(name)" + case .startMeasuringAlreadyCalled: return "Already called startMeasuring() once this iteration." + case .stopMeasuringAlreadyCalled: return "Already called stopMeasuring() once this iteration." + case .startMeasuringNotCalled: return "startMeasuring() must be called during the block." + case .stopBeforeStarting: return "Cannot stop measuring before starting measuring." + } + } + } + + internal var didFinishMeasuring: Bool { + return state == .measurementFinished || state == .measurementAborted + } + + private enum State { + case iterationUnstarted + case iterationStarted + case iterationFinished + case measurementFinished + case measurementAborted + } + private var state: State = .iterationUnstarted + + private let metrics: [PerformanceMetric] + private let delegate: PerformanceMeterDelegate + private let invocationFile: StaticString + private let invocationLine: Int + + private init(metrics: [PerformanceMetric], delegate: PerformanceMeterDelegate, file: StaticString, line: Int) { + self.metrics = metrics + self.delegate = delegate + self.invocationFile = file + self.invocationLine = line + } + + static func measureMetrics(_ metricNames: [String], delegate: PerformanceMeterDelegate, file: StaticString = #file, line: Int = #line, for block: (PerformanceMeter) -> Void) { + do { + let metrics = try self.metrics(forNames: metricNames) + let meter = PerformanceMeter(metrics: metrics, delegate: delegate, file: file, line: line) + meter.measure(block) + } catch let e { + delegate.recordAPIViolation(description: String(describing: e), file: file, line: line) + } + } + + func startMeasuring(file: StaticString = #file, line: Int = #line) { + guard state == .iterationUnstarted else { + return recordAPIViolation(.startMeasuringAlreadyCalled, file: file, line: line) + } + state = .iterationStarted + metrics.forEach { $0.startMeasuring() } + } + + func stopMeasuring(file: StaticString = #file, line: Int = #line) { + guard state != .iterationUnstarted else { + return recordAPIViolation(.stopBeforeStarting, file: file, line: line) + } + + guard state != .iterationFinished else { + return recordAPIViolation(.stopMeasuringAlreadyCalled, file: file, line: line) + } + + state = .iterationFinished + metrics.forEach { $0.stopMeasuring() } + } + + func abortMeasuring() { + state = .measurementAborted + } + + + private static func metrics(forNames names: [String]) throws -> [PerformanceMetric] { + guard !names.isEmpty else { throw Error.noMetrics } + + let metricsMapping = [WallClockTimeMetric.name : WallClockTimeMetric.self] + + return try names.map({ + guard let metricType = metricsMapping[$0] else { throw Error.unknownMetric(metricName: $0) } + return metricType.init() + }) + } + + private var numberOfIterations: Int { + return 10 + } + + private func measure(_ block: (PerformanceMeter) -> Void) { + for _ in (0.. : NSObject { + + /// The current thread's waiter manager. This is the only supported way to access an instance of + /// this class, since each instance is bound to a particular thread and is only concerned with + /// the XCTWaiters waiting on that thread. + static var current: WaiterManager { + let threadKey = "org.swift.XCTest.WaiterManager" + + if let existing = Thread.current.threadDictionary[threadKey] as? WaiterManager { + return existing + } else { + let manager = WaiterManager() + Thread.current.threadDictionary[threadKey] = manager + return manager + } + } + + private struct ManagedWaiterDetails { + let waiter: WaiterType + let watchdog: ManageableWaiterWatchdog? + } + + private var managedWaiterStack = [ManagedWaiterDetails]() + private weak var thread = Thread.current + private let queue = DispatchQueue(label: "org.swift.XCTest.WaiterManager") + + // Use `WaiterManager.current` to access the thread-specific instance + private override init() {} + + deinit { + assert(managedWaiterStack.isEmpty, "Waiters still registered when WaiterManager is deallocating.") + } + + func startManaging(_ waiter: WaiterType, timeout: TimeInterval) { + guard let thread = thread else { fatalError("\(self) no longer belongs to a thread") } + precondition(thread === Thread.current, "\(#function) called on wrong thread, must be called on \(thread)") + + var alreadyFinishedOuterWaiter: WaiterType? + + queue.sync { + // To start managing `waiter`, first see if any existing, outer waiters have already finished, + // because if one has, then `waiter` will be immediately interrupted before it begins waiting. + alreadyFinishedOuterWaiter = managedWaiterStack.first(where: { $0.waiter.isFinished })?.waiter + + let watchdog: ManageableWaiterWatchdog? + if alreadyFinishedOuterWaiter == nil { + // If there is no already-finished outer waiter, install a watchdog for `waiter`, and store it + // alongside `waiter` so that it may be canceled if `waiter` finishes waiting within its allotted timeout. + watchdog = WaiterManager.installWatchdog(for: waiter, timeout: timeout) + } else { + // If there is an already-finished outer waiter, no watchdog is needed for `waiter` because it will + // be interrupted before it begins waiting. + watchdog = nil + } + + // Add the waiter even if it's going to immediately be interrupted below to simplify the stack management + let details = ManagedWaiterDetails(waiter: waiter, watchdog: watchdog) + managedWaiterStack.append(details) + } + + if let alreadyFinishedOuterWaiter = alreadyFinishedOuterWaiter { + XCTWaiter.subsystemQueue.async { + waiter.queue_interrupt(for: alreadyFinishedOuterWaiter) + } + } + } + + func stopManaging(_ waiter: WaiterType) { + guard let thread = thread else { fatalError("\(self) no longer belongs to a thread") } + precondition(thread === Thread.current, "\(#function) called on wrong thread, must be called on \(thread)") + + queue.sync { + precondition(!managedWaiterStack.isEmpty, "Waiter stack was empty when requesting to stop managing: \(waiter)") + + let expectedIndex = managedWaiterStack.index(before: managedWaiterStack.endIndex) + let waiterDetails = managedWaiterStack[expectedIndex] + guard waiter == waiterDetails.waiter else { + fatalError("Top waiter on stack \(waiterDetails.waiter) is not equal to waiter to stop managing: \(waiter)") + } + + waiterDetails.watchdog?.cancel() + managedWaiterStack.remove(at: expectedIndex) + } + } + + private static func installWatchdog(for waiter: WaiterType, timeout: TimeInterval) -> ManageableWaiterWatchdog { + // Use DispatchWorkItem instead of a basic closure since it can be canceled. + let watchdog = DispatchWorkItem { [weak waiter] in + waiter?.queue_handleWatchdogTimeout() + } + + let outerTimeoutSlop = TimeInterval(0.25) + let deadline = DispatchTime.now() + timeout + outerTimeoutSlop + XCTWaiter.subsystemQueue.asyncAfter(deadline: deadline, execute: watchdog) + + return watchdog + } + + func queue_handleWatchdogTimeout(of waiter: WaiterType) { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + + var waitersToInterrupt = [WaiterType]() + + queue.sync { + guard let indexOfWaiter = managedWaiterStack.firstIndex(where: { $0.waiter == waiter }) else { + preconditionFailure("Waiter \(waiter) reported timed out but is not in the waiter stack \(managedWaiterStack)") + } + + waitersToInterrupt += managedWaiterStack[managedWaiterStack.index(after: indexOfWaiter)...].map { $0.waiter } + } + + for waiterToInterrupt in waitersToInterrupt.reversed() { + waiterToInterrupt.queue_interrupt(for: waiter) + } + } + +} diff --git a/Sources/XCTest/Private/WallClockTimeMetric.swift b/Sources/XCTest/Private/WallClockTimeMetric.swift new file mode 100644 index 000000000..2fe945a34 --- /dev/null +++ b/Sources/XCTest/Private/WallClockTimeMetric.swift @@ -0,0 +1,79 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2016 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 +// +// +// WallClockTimeMetric.swift +// Performance metric measuring how long it takes code to execute +// + +/// This metric uses the system uptime to keep track of how much time passes +/// between starting and stopping measuring. +internal final class WallClockTimeMetric: PerformanceMetric { + static let name = "org.swift.XCTPerformanceMetric_WallClockTime" + + typealias Measurement = TimeInterval + private var startTime: TimeInterval? + var measurements: [Measurement] = [] + + func startMeasuring() { + startTime = currentTime() + } + + func stopMeasuring() { + guard let startTime = startTime else { fatalError("Must start measuring before stopping measuring") } + let stopTime = currentTime() + measurements.append(stopTime-startTime) + } + + private let maxRelativeStandardDeviation = 10.0 + private let standardDeviationNegligibilityThreshold = 0.1 + + func calculateResults() -> String { + let results = [ + String(format: "average: %.3f", measurements.average), + String(format: "relative standard deviation: %.3f%%", measurements.relativeStandardDeviation), + "values: [\(measurements.map({ String(format: "%.6f", $0) }).joined(separator: ", "))]", + "performanceMetricID:\(type(of: self).name)", + String(format: "maxPercentRelativeStandardDeviation: %.3f%%", maxRelativeStandardDeviation), + String(format: "maxStandardDeviation: %.3f", standardDeviationNegligibilityThreshold), + ] + return "[Time, seconds] \(results.joined(separator: ", "))" + } + + func failureMessage() -> String? { + let relativeStandardDeviation = measurements.relativeStandardDeviation + if (relativeStandardDeviation > maxRelativeStandardDeviation && + measurements.standardDeviation > standardDeviationNegligibilityThreshold) { + return String(format: "The relative standard deviation of the measurements is %.3f%% which is higher than the max allowed of %.3f%%.", relativeStandardDeviation, maxRelativeStandardDeviation) + } + + return nil + } + + private func currentTime() -> TimeInterval { + return ProcessInfo.processInfo.systemUptime + } +} + + +private extension Collection where Index: ExpressibleByIntegerLiteral, Iterator.Element == WallClockTimeMetric.Measurement { + var average: WallClockTimeMetric.Measurement { + return self.reduce(0, +) / Double(Int(count)) + } + + var standardDeviation: WallClockTimeMetric.Measurement { + let average = self.average + let squaredDifferences = self.map({ pow($0 - average, 2.0) }) + let variance = squaredDifferences.reduce(0, +) / Double(Int(count-1)) + return sqrt(variance) + } + + var relativeStandardDeviation: Double { + return (standardDeviation*100) / average + } +} diff --git a/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift b/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift new file mode 100644 index 000000000..573c6c270 --- /dev/null +++ b/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift @@ -0,0 +1,116 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2018 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 +// +// +// XCTNSNotificationExpectation.swift +// + +/// Expectation subclass for waiting on a condition defined by a Foundation Notification instance. +open class XCTNSNotificationExpectation: XCTestExpectation { + + /// A closure to be invoked when a notification specified by the expectation is observed. + /// + /// - Parameter notification: The notification object which was observed. + /// - Returns: `true` if the expectation should be fulfilled, `false` if it should not. + /// + /// - SeeAlso: `XCTNSNotificationExpectation.handler` + public typealias Handler = (Notification) -> Bool + + private let queue = DispatchQueue(label: "org.swift.XCTest.XCTNSNotificationExpectation") + + /// The name of the notification being waited on. + open private(set) var notificationName: Notification.Name + + /// The specific object that will post the notification, if any. + /// If nil, any object may post the notification. Default is nil. + open private(set) var observedObject: Any? + + /// The specific notification center that the notification will be posted to. + open private(set) var notificationCenter: NotificationCenter + + private var observer: AnyObject? + + private var _handler: Handler? + + /// Allows the caller to install a special handler to do custom evaluation of received notifications + /// matching the specified object and notification center. + /// + /// - SeeAlso: `XCTNSNotificationExpectation.Handler` + open var handler: Handler? { + get { + return queue.sync { _handler } + } + set { + dispatchPrecondition(condition: .notOnQueue(queue)) + queue.async { self._handler = newValue } + } + } + + /// Initializes an expectation that waits for a Foundation Notification to be posted by an optional `object` to a specific NotificationCenter. + /// + /// - Parameter notificationName: The name of the notification to wait on. + /// - Parameter object: The object that will post the notification, if any. Default is nil. + /// - Parameter notificationCenter: The specific notification center that the notification will be posted to. + /// - Parameter file: The file name to use in the error message if + /// expectations are not met before the wait timeout. Default is the file + /// containing the call to this method. It is rare to provide this + /// parameter when calling this method. + /// - Parameter line: The line number to use in the error message if the + /// expectations are not met before the wait 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. + public init(name notificationName: Notification.Name, object: Any? = nil, notificationCenter: NotificationCenter = .default, file: StaticString = #file, line: Int = #line) { + self.notificationName = notificationName + self.observedObject = object + self.notificationCenter = notificationCenter + let description = "Expect notification '\(notificationName.rawValue)' from " + (object.map { "\($0)" } ?? "any object") + + super.init(description: description, file: file, line: line) + + beginObserving(with: notificationCenter) + } + + deinit { + assert(observer == nil, "observer should be nil, indicates failure to call cleanUp() internally") + } + + private func beginObserving(with notificationCenter: NotificationCenter) { + observer = notificationCenter.addObserver(forName: notificationName, object: observedObject, queue: nil) { [weak self] notification in + guard let strongSelf = self else { return } + + let shouldFulfill: Bool + + // If the handler is invoked, the test will only pass if true is returned. + if let handler = strongSelf.handler { + shouldFulfill = handler(notification) + } else { + shouldFulfill = true + } + + if shouldFulfill { + strongSelf.fulfill() + } + } + } + + override func cleanUp() { + queue.sync { + if let observer = observer { + notificationCenter.removeObserver(observer) + self.observer = nil + } + } + } + +} + +/// A closure to be invoked when a notification specified by the expectation is observed. +/// +/// - SeeAlso: `XCTNSNotificationExpectation.handler` +@available(*, deprecated, renamed: "XCTNSNotificationExpectation.Handler") +public typealias XCNotificationExpectationHandler = XCTNSNotificationExpectation.Handler diff --git a/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift b/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift new file mode 100644 index 000000000..08d0cf26b --- /dev/null +++ b/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift @@ -0,0 +1,135 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2018 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 +// +// +// XCTNSPredicateExpectation.swift +// + +/// Expectation subclass for waiting on a condition defined by an NSPredicate and an optional object. +open class XCTNSPredicateExpectation: XCTestExpectation { + + /// A closure to be invoked whenever evaluating the predicate against the object returns true. + /// + /// - Returns: `true` if the expectation should be fulfilled, `false` if it should not. + /// + /// - SeeAlso: `XCTNSPredicateExpectation.handler` + public typealias Handler = () -> Bool + + private let queue = DispatchQueue(label: "org.swift.XCTest.XCTNSPredicateExpectation") + + /// The predicate used by the expectation. + open private(set) var predicate: NSPredicate + + /// The object against which the predicate is evaluated, if any. Default is nil. + open private(set) var object: Any? + + private var _handler: Handler? + + /// Handler called when evaluating the predicate against the object returns true. If the handler is not + /// provided, the first successful evaluation will fulfill the expectation. If the handler provided, the + /// handler will be queried each time the notification is received to determine whether the expectation + /// should be fulfilled or not. + open var handler: Handler? { + get { + return queue.sync { _handler } + } + set { + dispatchPrecondition(condition: .notOnQueue(queue)) + queue.async { self._handler = newValue } + } + } + + private let runLoop = RunLoop.current + private var timer: Timer? + private let evaluationInterval = 0.01 + + /// Initializes an expectation that waits for a predicate to evaluate as true with an optionally specified object. + /// + /// - Parameter predicate: The predicate to evaluate. + /// - Parameter object: An optional object to evaluate `predicate` with. Default is nil. + /// - Parameter file: The file name to use in the error message if + /// expectations are not met before the wait timeout. Default is the file + /// containing the call to this method. It is rare to provide this + /// parameter when calling this method. + /// - Parameter line: The line number to use in the error message if the + /// expectations are not met before the wait 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. + public init(predicate: NSPredicate, object: Any? = nil, file: StaticString = #file, line: Int = #line) { + self.predicate = predicate + self.object = object + let description = "Expect predicate `\(predicate)`" + (object.map { " for object \($0)" } ?? "") + + super.init(description: description, file: file, line: line) + } + + deinit { + assert(timer == nil, "timer should be nil, indicates failure to call cleanUp() internally") + } + + override func didBeginWaiting() { + runLoop.perform { + if self.shouldFulfill() { + self.fulfill() + } else { + self.startPolling() + } + } + } + + private func startPolling() { + let timer = Timer(timeInterval: evaluationInterval, repeats: true) { [weak self] timer in + guard let self = self else { + timer.invalidate() + return + } + + if self.shouldFulfill() { + self.fulfill() + timer.invalidate() + } + } + + runLoop.add(timer, forMode: .default) + queue.async { + self.timer = timer + } + } + + private func shouldFulfill() -> Bool { + if predicate.evaluate(with: object) { + if let handler = handler { + if handler() { + return true + } + // We do not fulfill or invalidate the timer if the handler returns + // false. The object is still re-evaluated until timeout. + } else { + return true + } + } + + return false + } + + override func cleanUp() { + queue.sync { + if let timer = timer { + timer.invalidate() + self.timer = nil + } + } + } + +} + +/// A closure to be invoked whenever evaluating the predicate against the object returns true. +/// +/// - SeeAlso: `XCTNSPredicateExpectation.handler` +@available(*, deprecated, renamed: "XCTNSPredicateExpectation.Handler") +public typealias XCPredicateExpectationHandler = XCTNSPredicateExpectation.Handler diff --git a/Sources/XCTest/Public/Asynchronous/XCTWaiter+Validation.swift b/Sources/XCTest/Public/Asynchronous/XCTWaiter+Validation.swift new file mode 100644 index 000000000..5ff4643c7 --- /dev/null +++ b/Sources/XCTest/Public/Asynchronous/XCTWaiter+Validation.swift @@ -0,0 +1,89 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2018 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 +// +// +// XCTWaiter+Validation.swift +// + +protocol XCTWaiterValidatableExpectation: Equatable { + var isFulfilled: Bool { get } + var fulfillmentToken: UInt64 { get } + var isInverted: Bool { get } +} + +extension XCTWaiter { + struct ValidatableXCTestExpectation: XCTWaiterValidatableExpectation { + let expectation: XCTestExpectation + + var isFulfilled: Bool { + return expectation.queue_isFulfilled + } + + var fulfillmentToken: UInt64 { + return expectation.queue_fulfillmentToken + } + + var isInverted: Bool { + return expectation.queue_isInverted + } + } +} + +extension XCTWaiter { + enum ValidationResult { + case complete + case fulfilledInvertedExpectation(invertedExpectation: ExpectationType) + case violatedOrderingConstraints(expectation: ExpectationType, requiredExpectation: ExpectationType) + case timedOut(unfulfilledExpectations: [ExpectationType]) + case incomplete + } + + static func validateExpectations(_ expectations: [ExpectationType], dueToTimeout didTimeOut: Bool, enforceOrder: Bool) -> ValidationResult { + var unfulfilledExpectations = [ExpectationType]() + var fulfilledExpectations = [ExpectationType]() + + for expectation in expectations { + if expectation.isFulfilled { + // Check for any fulfilled inverse expectations. If they were fulfilled before wait was called, + // this is where we'd catch that. + if expectation.isInverted { + return .fulfilledInvertedExpectation(invertedExpectation: expectation) + } else { + fulfilledExpectations.append(expectation) + } + } else { + unfulfilledExpectations.append(expectation) + } + } + + if enforceOrder { + fulfilledExpectations.sort { $0.fulfillmentToken < $1.fulfillmentToken } + let nonInvertedExpectations = expectations.filter { !$0.isInverted } + + assert(fulfilledExpectations.count <= nonInvertedExpectations.count, "Internal error: number of fulfilledExpectations (\(fulfilledExpectations.count)) must not exceed number of non-inverted expectations (\(nonInvertedExpectations.count))") + + for (fulfilledExpectation, nonInvertedExpectation) in zip(fulfilledExpectations, nonInvertedExpectations) where fulfilledExpectation != nonInvertedExpectation { + return .violatedOrderingConstraints(expectation: fulfilledExpectation, requiredExpectation: nonInvertedExpectation) + } + } + + if unfulfilledExpectations.isEmpty { + return .complete + } else if didTimeOut { + // If we've timed out, our new state is just based on whether or not we have any remaining unfulfilled, non-inverted expectations. + let nonInvertedUnfilledExpectations = unfulfilledExpectations.filter { !$0.isInverted } + if nonInvertedUnfilledExpectations.isEmpty { + return .complete + } else { + return .timedOut(unfulfilledExpectations: nonInvertedUnfilledExpectations) + } + } + + return .incomplete + } +} diff --git a/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift b/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift new file mode 100644 index 000000000..13a232f33 --- /dev/null +++ b/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift @@ -0,0 +1,419 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2018 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 +// +// +// XCTWaiter.swift +// + +import CoreFoundation + +/// Events are reported to the waiter's delegate via these methods. XCTestCase conforms to this +/// protocol and will automatically report timeouts and other unexpected events as test failures. +/// +/// - Note: These methods are invoked on an arbitrary queue. +public protocol XCTWaiterDelegate: AnyObject { + + /// Invoked when not all waited on expectations are fulfilled during the timeout period. If the delegate + /// is an XCTestCase instance, this will be reported as a test failure. + /// + /// - Parameter waiter: The waiter which timed out. + /// - Parameter unfulfilledExpectations: The expectations which were unfulfilled when `waiter` timed out. + func waiter(_ waiter: XCTWaiter, didTimeoutWithUnfulfilledExpectations unfulfilledExpectations: [XCTestExpectation]) + + /// Invoked when the wait specified that fulfillment order should be enforced and an expectation + /// has been fulfilled in the wrong order. If the delegate is an XCTestCase instance, this will be reported + /// as a test failure. + /// + /// - Parameter waiter: The waiter which had an ordering violation. + /// - Parameter expectation: The expectation which was fulfilled instead of the required expectation. + /// - Parameter requiredExpectation: The expectation which was fulfilled instead of the required expectation. + func waiter(_ waiter: XCTWaiter, fulfillmentDidViolateOrderingConstraintsFor expectation: XCTestExpectation, requiredExpectation: XCTestExpectation) + + /// Invoked when an expectation marked as inverted is fulfilled. If the delegate is an XCTestCase instance, + /// this will be reported as a test failure. + /// + /// - Parameter waiter: The waiter which had an inverted expectation fulfilled. + /// - Parameter expectation: The inverted expectation which was fulfilled. + /// + /// - SeeAlso: `XCTestExpectation.isInverted` + func waiter(_ waiter: XCTWaiter, didFulfillInvertedExpectation expectation: XCTestExpectation) + + /// Invoked when the waiter is interrupted prior to its expectations being fulfilled or timing out. + /// This occurs when an "outer" waiter times out, resulting in any waiters nested inside it being + /// interrupted to allow the call stack to quickly unwind. + /// + /// - Parameter waiter: The waiter which was interrupted. + /// - Parameter outerWaiter: The "outer" waiter which interrupted `waiter`. + func nestedWaiter(_ waiter: XCTWaiter, wasInterruptedByTimedOutWaiter outerWaiter: XCTWaiter) + +} + +// All `XCTWaiterDelegate` methods are optional, so empty default implementations are provided +public extension XCTWaiterDelegate { + func waiter(_ waiter: XCTWaiter, didTimeoutWithUnfulfilledExpectations unfulfilledExpectations: [XCTestExpectation]) {} + func waiter(_ waiter: XCTWaiter, fulfillmentDidViolateOrderingConstraintsFor expectation: XCTestExpectation, requiredExpectation: XCTestExpectation) {} + func waiter(_ waiter: XCTWaiter, didFulfillInvertedExpectation expectation: XCTestExpectation) {} + func nestedWaiter(_ waiter: XCTWaiter, wasInterruptedByTimedOutWaiter outerWaiter: XCTWaiter) {} +} + +/// Manages waiting - pausing the current execution context - for an array of XCTestExpectations. Waiters +/// can be used with or without a delegate to respond to events such as completion, timeout, or invalid +/// expectation fulfillment. XCTestCase conforms to the delegate protocol and will automatically report +/// timeouts and other unexpected events as test failures. +/// +/// Waiters can be used without a delegate or any association with a test case instance. This allows test +/// support libraries to provide convenience methods for waiting without having to pass test cases through +/// those APIs. +open class XCTWaiter { + + /// Values returned by a waiter when it completes, times out, or is interrupted due to another waiter + /// higher in the call stack timing out. + public enum Result: Int { + case completed = 1 + case timedOut + case incorrectOrder + case invertedFulfillment + case interrupted + } + + private enum State: Equatable { + case ready + case waiting(state: Waiting) + case finished(state: Finished) + + struct Waiting: Equatable { + var enforceOrder: Bool + var expectations: [XCTestExpectation] + var fulfilledExpectations: [XCTestExpectation] + } + + struct Finished: Equatable { + let result: Result + let fulfilledExpectations: [XCTestExpectation] + let unfulfilledExpectations: [XCTestExpectation] + } + + var allExpectations: [XCTestExpectation] { + switch self { + case .ready: + return [] + case let .waiting(waitingState): + return waitingState.expectations + case let .finished(finishedState): + return finishedState.fulfilledExpectations + finishedState.unfulfilledExpectations + } + } + } + + internal static let subsystemQueue = DispatchQueue(label: "org.swift.XCTest.XCTWaiter") + + private var state = State.ready + internal var timeout: TimeInterval = 0 + internal var waitSourceLocation: SourceLocation? + private weak var manager: WaiterManager? + private var runLoop: RunLoop? + + private weak var _delegate: XCTWaiterDelegate? + private let delegateQueue = DispatchQueue(label: "org.swift.XCTest.XCTWaiter.delegate") + + /// The waiter delegate will be called with various events described in the `XCTWaiterDelegate` protocol documentation. + /// + /// - SeeAlso: `XCTWaiterDelegate` + open var delegate: XCTWaiterDelegate? { + get { + return XCTWaiter.subsystemQueue.sync { _delegate } + } + set { + dispatchPrecondition(condition: .notOnQueue(XCTWaiter.subsystemQueue)) + XCTWaiter.subsystemQueue.async { self._delegate = newValue } + } + } + + /// Returns an array containing the expectations that were fulfilled, in that order, up until the waiter + /// stopped waiting. Expectations fulfilled after the waiter stopped waiting will not be in the array. + /// The array will be empty until the waiter has started waiting, even if expectations have already been + /// fulfilled. + open var fulfilledExpectations: [XCTestExpectation] { + return XCTWaiter.subsystemQueue.sync { + let fulfilledExpectations: [XCTestExpectation] + + switch state { + case .ready: + fulfilledExpectations = [] + case let .waiting(waitingState): + fulfilledExpectations = waitingState.fulfilledExpectations + case let .finished(finishedState): + fulfilledExpectations = finishedState.fulfilledExpectations + } + + // Sort by fulfillment token before returning, since it is the true fulfillment order. + // The waiter being notified by the expectation isn't guaranteed to happen in the same order. + return fulfilledExpectations.sorted { $0.queue_fulfillmentToken < $1.queue_fulfillmentToken } + } + } + + /// Initializes a waiter with an optional delegate. + public init(delegate: XCTWaiterDelegate? = nil) { + _delegate = delegate + } + + /// Wait on an array of expectations for up to the specified timeout, and optionally specify whether they + /// must be fulfilled in the given order. May return early based on fulfillment of the waited on expectations. + /// + /// - Parameter expectations: The expectations to wait on. + /// - Parameter timeout: The maximum total time duration to wait on all expectations. + /// - Parameter enforceOrder: Specifies whether the expectations must be fulfilled in the order + /// they are specified in the `expectations` Array. Default is false. + /// - Parameter file: The file name to use in the error message if + /// expectations are not fulfilled before the given timeout. Default is the file + /// containing the call to this method. It is rare to provide this + /// parameter when calling this method. + /// - Parameter line: The line number to use in the error message if the + /// 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. + /// + /// - Note: Whereas Objective-C XCTest determines the file and line + /// number of the "wait" call using symbolication, this implementation + /// opts to take `file` and `line` as parameters instead. As a result, + /// the interface to these methods are not exactly identical between + /// 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`. + @discardableResult + open func wait(for expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) -> Result { + precondition(Set(expectations).count == expectations.count, "API violation - each expectation can appear only once in the 'expectations' parameter.") + + self.timeout = timeout + waitSourceLocation = SourceLocation(file: file, line: line) + let runLoop = RunLoop.current + + XCTWaiter.subsystemQueue.sync { + precondition(state == .ready, "API violation - wait(...) has already been called on this waiter.") + + let previouslyWaitedOnExpectations = expectations.filter { $0.queue_hasBeenWaitedOn } + let previouslyWaitedOnExpectationDescriptions = previouslyWaitedOnExpectations.map { $0.queue_expectationDescription }.joined(separator: "`, `") + precondition(previouslyWaitedOnExpectations.isEmpty, "API violation - expectations can only be waited on once, `\(previouslyWaitedOnExpectationDescriptions)` have already been waited on.") + + let waitingState = State.Waiting( + enforceOrder: enforceOrder, + expectations: expectations, + fulfilledExpectations: expectations.filter { $0.queue_isFulfilled } + ) + queue_configureExpectations(expectations) + state = .waiting(state: waitingState) + self.runLoop = runLoop + + queue_validateExpectationFulfillment(dueToTimeout: false) + } + + let manager = WaiterManager.current + manager.startManaging(self, timeout: timeout) + self.manager = manager + + // Begin the core wait loop. + let timeoutTimestamp = Date.timeIntervalSinceReferenceDate + timeout + while !isFinished { + let remaining = timeoutTimestamp - Date.timeIntervalSinceReferenceDate + if remaining <= 0 { + break + } + primitiveWait(using: runLoop, duration: remaining) + } + + manager.stopManaging(self) + self.manager = nil + + let result: Result = XCTWaiter.subsystemQueue.sync { + queue_validateExpectationFulfillment(dueToTimeout: true) + + for expectation in expectations { + expectation.cleanUp() + expectation.queue_didFulfillHandler = nil + } + + guard case let .finished(finishedState) = state else { fatalError("Unexpected state: \(state)") } + return finishedState.result + } + + delegateQueue.sync { + // DO NOT REMOVE ME + // This empty block, executed synchronously, ensures that inflight delegate callbacks from the + // internal queue have been processed before wait returns. + } + + return result + } + + /// 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 + /// must be fulfilled in the given order. May return early based on fulfillment of the waited on expectations. The waiter + /// is discarded when the wait completes. + /// + /// - Parameter expectations: The expectations to wait on. + /// - Parameter timeout: The maximum total time duration to wait on all expectations. + /// - Parameter enforceOrder: Specifies whether the expectations must be fulfilled in the order + /// they are specified in the `expectations` Array. Default is false. + /// - Parameter file: The file name to use in the error message if + /// expectations are not fulfilled before the given timeout. Default is the file + /// containing the call to this method. It is rare to provide this + /// parameter when calling this method. + /// - Parameter line: The line number to use in the error message if the + /// 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. + open class func wait(for expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) -> Result { + return XCTWaiter().wait(for: expectations, timeout: timeout, enforceOrder: enforceOrder, file: file, line: line) + } + + deinit { + for expectation in state.allExpectations { + expectation.cleanUp() + } + } + + private func queue_configureExpectations(_ expectations: [XCTestExpectation]) { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + + for expectation in expectations { + expectation.queue_didFulfillHandler = { [weak self, unowned expectation] in + self?.expectationWasFulfilled(expectation) + } + expectation.queue_hasBeenWaitedOn = true + } + } + + private func queue_validateExpectationFulfillment(dueToTimeout: Bool) { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + guard case let .waiting(waitingState) = state else { return } + + let validatableExpectations = waitingState.expectations.map { ValidatableXCTestExpectation(expectation: $0) } + let validationResult = XCTWaiter.validateExpectations(validatableExpectations, dueToTimeout: dueToTimeout, enforceOrder: waitingState.enforceOrder) + + switch validationResult { + case .complete: + queue_finish(result: .completed, cancelPrimitiveWait: !dueToTimeout) + + case .fulfilledInvertedExpectation(let invertedValidationExpectation): + queue_finish(result: .invertedFulfillment, cancelPrimitiveWait: true) { delegate in + delegate.waiter(self, didFulfillInvertedExpectation: invertedValidationExpectation.expectation) + } + + case .violatedOrderingConstraints(let validationExpectation, let requiredValidationExpectation): + queue_finish(result: .incorrectOrder, cancelPrimitiveWait: true) { delegate in + delegate.waiter(self, fulfillmentDidViolateOrderingConstraintsFor: validationExpectation.expectation, requiredExpectation: requiredValidationExpectation.expectation) + } + + case .timedOut(let unfulfilledValidationExpectations): + queue_finish(result: .timedOut, cancelPrimitiveWait: false) { delegate in + delegate.waiter(self, didTimeoutWithUnfulfilledExpectations: unfulfilledValidationExpectations.map { $0.expectation }) + } + + case .incomplete: + break + + } + } + + private func queue_finish(result: Result, cancelPrimitiveWait: Bool, delegateBlock: ((XCTWaiterDelegate) -> Void)? = nil) { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + guard case let .waiting(waitingState) = state else { preconditionFailure("Unexpected state: \(state)") } + + let unfulfilledExpectations = waitingState.expectations.filter { !waitingState.fulfilledExpectations.contains($0) } + + state = .finished(state: State.Finished( + result: result, + fulfilledExpectations: waitingState.fulfilledExpectations, + unfulfilledExpectations: unfulfilledExpectations + )) + + if cancelPrimitiveWait { + self.cancelPrimitiveWait() + } + + if let delegateBlock = delegateBlock, let delegate = _delegate { + delegateQueue.async { + delegateBlock(delegate) + } + } + } + + private func expectationWasFulfilled(_ expectation: XCTestExpectation) { + XCTWaiter.subsystemQueue.sync { + // If already finished, do nothing + guard case var .waiting(waitingState) = state else { return } + + waitingState.fulfilledExpectations.append(expectation) + queue_validateExpectationFulfillment(dueToTimeout: false) + } + } + +} + +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 + // by the `timeout` argument. Only run for a short time in case `cancelPrimitiveWait()` was called and + // issued `CFRunLoopStop` just before we reach this point. + let timeIntervalToRun = min(0.1, timeout) + + // RunLoop.run(mode:before:) should have @discardableResult + _ = runLoop.run(mode: .default, before: Date(timeIntervalSinceNow: timeIntervalToRun)) + } + + func cancelPrimitiveWait() { + guard let runLoop = runLoop else { return } +#if os(Windows) + runLoop._stop() +#else + CFRunLoopStop(runLoop.getCFRunLoop()) +#endif + } +} + +extension XCTWaiter: Equatable { + public static func == (lhs: XCTWaiter, rhs: XCTWaiter) -> Bool { + return lhs === rhs + } +} + +extension XCTWaiter: CustomStringConvertible { + public var description: String { + return XCTWaiter.subsystemQueue.sync { + let expectationsString = state.allExpectations.map { "'\($0.queue_expectationDescription)'" }.joined(separator: ", ") + + return "" + } + } +} + +extension XCTWaiter: ManageableWaiter { + var isFinished: Bool { + return XCTWaiter.subsystemQueue.sync { + switch state { + case .ready, .waiting: return false + case .finished: return true + } + } + } + + func queue_handleWatchdogTimeout() { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + + queue_validateExpectationFulfillment(dueToTimeout: true) + manager!.queue_handleWatchdogTimeout(of: self) + cancelPrimitiveWait() + } + + func queue_interrupt(for interruptingWaiter: XCTWaiter) { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + + queue_finish(result: .interrupted, cancelPrimitiveWait: true) { delegate in + delegate.nestedWaiter(self, wasInterruptedByTimedOutWaiter: interruptingWaiter) + } + } +} diff --git a/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift b/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift new file mode 100644 index 000000000..f1b032b34 --- /dev/null +++ b/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift @@ -0,0 +1,240 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2018 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 +// +// +// XCTestCase+Asynchronous.swift +// Methods on XCTestCase for testing asynchronous operations +// + +public extension XCTestCase { + + /// Creates a point of synchronization in the flow of a test. Only one + /// "wait" can be active at any given time, but multiple discrete sequences + /// of { expectations -> wait } can be chained together. The related + /// XCTWaiter API allows multiple "nested" waits if that is required. + /// + /// - Parameter timeout: The amount of time within which all expectation + /// must be fulfilled. + /// - Parameter file: The file name to use in the error message if + /// expectations are not met before the given timeout. Default is the file + /// containing the call to this method. It is rare to provide this + /// parameter when calling this method. + /// - Parameter line: The line number to use in the error message if the + /// expectations are not met 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. + /// - Parameter handler: If provided, the handler will be invoked both on + /// timeout or fulfillment of all expectations. Timeout is always treated + /// as a test failure. + /// + /// - SeeAlso: XCTWaiter + /// + /// - Note: Whereas Objective-C XCTest determines the file and line + /// number of the "wait" call using symbolication, this implementation + /// opts to take `file` and `line` as parameters instead. As a result, + /// the interface to these methods are not exactly identical between + /// 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`. + func waitForExpectations(timeout: TimeInterval, file: StaticString = #file, line: Int = #line, handler: XCWaitCompletionHandler? = nil) { + precondition(Thread.isMainThread, "\(#function) must be called on the main thread") + if currentWaiter != nil { + return recordFailure(description: "API violation - calling wait on test case while already waiting.", at: SourceLocation(file: file, line: line), expected: false) + } + let expectations = self.expectations + if expectations.isEmpty { + return recordFailure(description: "API violation - call made to wait without any expectations having been set.", at: SourceLocation(file: file, line: line), expected: false) + } + + let waiter = XCTWaiter(delegate: self) + currentWaiter = waiter + + let waiterResult = waiter.wait(for: expectations, timeout: timeout, file: file, line: line) + + currentWaiter = nil + + cleanUpExpectations() + + // The handler is invoked regardless of whether the test passed. + if let handler = handler { + let error = (waiterResult == .completed) ? nil : XCTestError(.timeoutWhileWaiting) + handler(error) + } + } + + /// Wait on an array of expectations for up to the specified timeout, and optionally specify whether they + /// must be fulfilled in the given order. May return early based on fulfillment of the waited on expectations. + /// + /// - Parameter expectations: The expectations to wait on. + /// - Parameter timeout: The maximum total time duration to wait on all expectations. + /// - Parameter enforceOrder: Specifies whether the expectations must be fulfilled in the order + /// they are specified in the `expectations` Array. Default is false. + /// - Parameter file: The file name to use in the error message if + /// expectations are not fulfilled before the given timeout. Default is the file + /// containing the call to this method. It is rare to provide this + /// parameter when calling this method. + /// - Parameter line: The line number to use in the error message if the + /// 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. + /// + /// - SeeAlso: XCTWaiter + func wait(for expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) { + let waiter = XCTWaiter(delegate: self) + waiter.wait(for: expectations, timeout: timeout, enforceOrder: enforceOrder, file: file, line: line) + + cleanUpExpectations(expectations) + } + + /// Creates and returns an expectation associated with the test case. + /// + /// - Parameter description: This string will be displayed in the test log + /// to help diagnose failures. + /// - Parameter file: The file name to use in the error message if + /// this expectation is not waited for. Default is the file + /// containing the call to this method. It is rare to provide this + /// parameter when calling this method. + /// - Parameter line: The line number to use in the error message if the + /// this expectation is not waited for. 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. + /// + /// - Note: Whereas Objective-C XCTest determines the file and line + /// number of expectations that are created by using symbolication, this + /// implementation opts to take `file` and `line` as parameters instead. + /// As a result, the interface to these methods are not exactly identical + /// between 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`. + @discardableResult func expectation(description: String, file: StaticString = #file, line: Int = #line) -> XCTestExpectation { + let expectation = XCTestExpectation(description: description, file: file, line: line) + addExpectation(expectation) + return expectation + } + + /// Creates and returns an expectation for a notification. + /// + /// - Parameter notificationName: The name of the notification the + /// expectation observes. + /// - Parameter object: The object whose notifications the expectation will + /// receive; that is, only notifications with this object are observed by + /// the test case. If you pass nil, the expectation doesn't use + /// a notification's object to decide whether it is fulfilled. + /// - Parameter notificationCenter: The specific notification center that + /// the notification will be posted to. + /// - Parameter handler: If provided, the handler will be invoked when the + /// notification is observed. It will not be invoked on timeout. Use the + /// handler to further investigate if the notification fulfills the + /// expectation. + @discardableResult func expectation(forNotification notificationName: Notification.Name, object: Any? = nil, notificationCenter: NotificationCenter = .default, file: StaticString = #file, line: Int = #line, handler: XCTNSNotificationExpectation.Handler? = nil) -> XCTestExpectation { + let expectation = XCTNSNotificationExpectation(name: notificationName, object: object, notificationCenter: notificationCenter, file: file, line: line) + expectation.handler = handler + addExpectation(expectation) + return expectation + } + + /// Creates and returns an expectation for a notification. + /// + /// - Parameter notificationName: The name of the notification the + /// expectation observes. + /// - Parameter object: The object whose notifications the expectation will + /// receive; that is, only notifications with this object are observed by + /// the test case. If you pass nil, the expectation doesn't use + /// a notification's object to decide whether it is fulfilled. + /// - Parameter notificationCenter: The specific notification center that + /// the notification will be posted to. + /// - Parameter handler: If provided, the handler will be invoked when the + /// notification is observed. It will not be invoked on timeout. Use the + /// handler to further investigate if the notification fulfills the + /// expectation. + @discardableResult func expectation(forNotification notificationName: String, object: Any? = nil, notificationCenter: NotificationCenter = .default, file: StaticString = #file, line: Int = #line, handler: XCTNSNotificationExpectation.Handler? = nil) -> XCTestExpectation { + return expectation(forNotification: Notification.Name(rawValue: notificationName), object: object, notificationCenter: notificationCenter, file: file, line: line, handler: handler) + } + + /// Creates and returns an expectation that is fulfilled if the predicate + /// returns true when evaluated with the given object. The expectation + /// periodically evaluates the predicate and also may use notifications or + /// other events to optimistically re-evaluate. + /// + /// - Parameter predicate: The predicate that will be used to evaluate the + /// object. + /// - Parameter object: The object that is evaluated against the conditions + /// specified by the predicate, if any. Default is nil. + /// - Parameter file: The file name to use in the error message if + /// this expectation is not waited for. Default is the file + /// containing the call to this method. It is rare to provide this + /// parameter when calling this method. + /// - Parameter line: The line number to use in the error message if the + /// this expectation is not waited for. 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. + /// - Parameter handler: A block to be invoked when evaluating the predicate + /// against the object returns true. If the block is not provided the + /// first successful evaluation will fulfill the expectation. If provided, + /// the handler can override that behavior which leaves the caller + /// responsible for fulfilling the expectation. + @discardableResult func expectation(for predicate: NSPredicate, evaluatedWith object: Any? = nil, file: StaticString = #file, line: Int = #line, handler: XCTNSPredicateExpectation.Handler? = nil) -> XCTestExpectation { + let expectation = XCTNSPredicateExpectation(predicate: predicate, object: object, file: file, line: line) + expectation.handler = handler + addExpectation(expectation) + return expectation + } + +} + +/// A block to be invoked when a call to wait times out or has had all +/// associated expectations fulfilled. +/// +/// - Parameter error: If the wait timed out or a failure was raised while +/// waiting, the error's code will specify the type of failure. Otherwise +/// error will be nil. +public typealias XCWaitCompletionHandler = (Error?) -> () + +extension XCTestCase: XCTWaiterDelegate { + + public func waiter(_ waiter: XCTWaiter, didTimeoutWithUnfulfilledExpectations unfulfilledExpectations: [XCTestExpectation]) { + let expectationDescription = unfulfilledExpectations.map { $0.expectationDescription }.joined(separator: ", ") + let failureDescription = "Asynchronous wait failed - Exceeded timeout of \(waiter.timeout) seconds, with unfulfilled expectations: \(expectationDescription)" + recordFailure(description: failureDescription, at: waiter.waitSourceLocation ?? .unknown, expected: true) + } + + public func waiter(_ waiter: XCTWaiter, fulfillmentDidViolateOrderingConstraintsFor expectation: XCTestExpectation, requiredExpectation: XCTestExpectation) { + let failureDescription = "Failed due to expectation fulfilled in incorrect order: requires '\(requiredExpectation.expectationDescription)', actually fulfilled '\(expectation.expectationDescription)'" + recordFailure(description: failureDescription, at: expectation.fulfillmentSourceLocation ?? .unknown, expected: true) + } + + public func waiter(_ waiter: XCTWaiter, didFulfillInvertedExpectation expectation: XCTestExpectation) { + let failureDescription = "Asynchronous wait failed - Fulfilled inverted expectation '\(expectation.expectationDescription)'" + recordFailure(description: failureDescription, at: expectation.fulfillmentSourceLocation ?? .unknown, expected: true) + } + + public func nestedWaiter(_ waiter: XCTWaiter, wasInterruptedByTimedOutWaiter outerWaiter: XCTWaiter) { + let failureDescription = "Asynchronous waiter \(waiter) failed - Interrupted by timeout of containing waiter \(outerWaiter)" + recordFailure(description: failureDescription, at: waiter.waitSourceLocation ?? .unknown, expected: true) + } + +} + +internal extension XCTestCase { + // It is an API violation to create expectations but not wait for them to + // be completed. Notify the user of a mistake via a test failure. + func failIfExpectationsNotWaitedFor(_ expectations: [XCTestExpectation]) { + let orderedUnwaitedExpectations = expectations.filter { !$0.hasBeenWaitedOn }.sorted { $0.creationToken < $1.creationToken } + guard let expectationForFileLineReporting = orderedUnwaitedExpectations.first else { + return + } + + let expectationDescriptions = orderedUnwaitedExpectations.map { "'\($0.expectationDescription)'" }.joined(separator: ", ") + let failureDescription = "Failed due to unwaited expectation\(orderedUnwaitedExpectations.count > 1 ? "s" : "") \(expectationDescriptions)" + + recordFailure( + description: failureDescription, + at: expectationForFileLineReporting.creationSourceLocation, + expected: false) + } +} diff --git a/Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift b/Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift new file mode 100644 index 000000000..afe435617 --- /dev/null +++ b/Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift @@ -0,0 +1,329 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2018 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 +// +// +// XCTestExpectation.swift +// + +/// Expectations represent specific conditions in asynchronous testing. +open class XCTestExpectation { + + private static var currentMonotonicallyIncreasingToken: UInt64 = 0 + private static func queue_nextMonotonicallyIncreasingToken() -> UInt64 { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + currentMonotonicallyIncreasingToken += 1 + return currentMonotonicallyIncreasingToken + } + private static func nextMonotonicallyIncreasingToken() -> UInt64 { + return XCTWaiter.subsystemQueue.sync { queue_nextMonotonicallyIncreasingToken() } + } + + /* + Rules for properties + ==================== + + XCTestExpectation has many properties, many of which require synchronization on `XCTWaiter.subsystemQueue`. + When adding properties, use the following rules for consistency. The naming guidelines aim to allow + property names to be as short & simple as possible, while maintaining the necessary synchronization. + + - If property is constant (`let`), it is immutable so there is no synchronization concern. + - No underscore prefix on name + - No matching `queue_` property + - If it is only used within this file: + - `private` access + - If is is used outside this file but not outside the module: + - `internal` access + - If it is used outside the module: + - `public` or `open` access, depending on desired overridability + + - If property is variable (`var`), it is mutable so access to it must be synchronized. + - `private` access + - If it is only used within this file: + - No underscore prefix on name + - No matching `queue_` property + - If is is used outside this file: + - If access outside this file is always on-queue: + - No underscore prefix on name + - Matching internal `queue_` property with `.onQueue` dispatchPreconditions + - If access outside this file is sometimes off-queue + - Underscore prefix on name + - Matching `internal` property with `queue_` prefix and `XCTWaiter.subsystemQueue` dispatchPreconditions + - Matching `internal` or `public` property without underscore prefix but with `XCTWaiter.subsystemQueue` synchronization + */ + + private var _expectationDescription: String + + internal let creationToken: UInt64 + internal let creationSourceLocation: SourceLocation + + private var isFulfilled = false + private var fulfillmentToken: UInt64 = 0 + private var _fulfillmentSourceLocation: SourceLocation? + + private var _expectedFulfillmentCount = 1 + private var numberOfFulfillments = 0 + + private var _isInverted = false + + private var _assertForOverFulfill = false + + private var _hasBeenWaitedOn = false + + private var _didFulfillHandler: (() -> Void)? + + /// A human-readable string used to describe the expectation in log output and test reports. + open var expectationDescription: String { + get { + return XCTWaiter.subsystemQueue.sync { queue_expectationDescription } + } + set { + XCTWaiter.subsystemQueue.sync { queue_expectationDescription = newValue } + } + } + + /// The number of times `fulfill()` must be called on the expectation in order for it + /// to report complete fulfillment to its waiter. Default is 1. + /// This value must be greater than 0 and is not meaningful if combined with `isInverted`. + open var expectedFulfillmentCount: Int { + get { + return XCTWaiter.subsystemQueue.sync { queue_expectedFulfillmentCount } + } + set { + precondition(newValue > 0, "API violation - fulfillment count must be greater than 0.") + + XCTWaiter.subsystemQueue.sync { + precondition(!queue_hasBeenWaitedOn, "API violation - cannot set expectedFulfillmentCount on '\(queue_expectationDescription)' after already waiting on it.") + queue_expectedFulfillmentCount = newValue + } + } + } + + /// If an expectation is set to be inverted, then fulfilling it will have a similar effect as + /// failing to fulfill a conventional expectation has, as handled by the waiter and its delegate. + /// Furthermore, waiters that wait on an inverted expectation will allow the full timeout to elapse + /// and not report timeout to the delegate if it is not fulfilled. + open var isInverted: Bool { + get { + return XCTWaiter.subsystemQueue.sync { queue_isInverted } + } + set { + XCTWaiter.subsystemQueue.sync { + precondition(!queue_hasBeenWaitedOn, "API violation - cannot set isInverted on '\(queue_expectationDescription)' after already waiting on it.") + queue_isInverted = newValue + } + } + } + + /// If set, calls to fulfill() after the expectation has already been fulfilled - exceeding the fulfillment + /// count - will cause a fatal error and halt process execution. Default is false (disabled). + /// + /// - Note: This is the legacy behavior of expectations created through APIs on the ObjC version of XCTestCase + /// because that version raises ObjC exceptions (which may be caught) instead of causing a fatal error. + /// In this version of XCTest, no expectation ever has this property set to true (enabled) by default, it + /// must be opted-in to explicitly. + open var assertForOverFulfill: Bool { + get { + return XCTWaiter.subsystemQueue.sync { _assertForOverFulfill } + } + set { + XCTWaiter.subsystemQueue.sync { + precondition(!queue_hasBeenWaitedOn, "API violation - cannot set assertForOverFulfill on '\(queue_expectationDescription)' after already waiting on it.") + _assertForOverFulfill = newValue + } + } + } + + internal var fulfillmentSourceLocation: SourceLocation? { + return XCTWaiter.subsystemQueue.sync { _fulfillmentSourceLocation } + } + + internal var hasBeenWaitedOn: Bool { + return XCTWaiter.subsystemQueue.sync { queue_hasBeenWaitedOn } + } + + internal var queue_expectationDescription: String { + get { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + return _expectationDescription + } + set { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + _expectationDescription = newValue + } + } + internal var queue_isFulfilled: Bool { + get { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + return isFulfilled + } + set { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + isFulfilled = newValue + } + } + internal var queue_fulfillmentToken: UInt64 { + get { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + return fulfillmentToken + } + set { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + fulfillmentToken = newValue + } + } + internal var queue_expectedFulfillmentCount: Int { + get { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + return _expectedFulfillmentCount + } + set { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + _expectedFulfillmentCount = newValue + } + } + internal var queue_isInverted: Bool { + get { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + return _isInverted + } + set { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + _isInverted = newValue + } + } + internal var queue_hasBeenWaitedOn: Bool { + get { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + return _hasBeenWaitedOn + } + set { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + _hasBeenWaitedOn = newValue + + if _hasBeenWaitedOn { + didBeginWaiting() + } + } + } + internal var queue_didFulfillHandler: (() -> Void)? { + get { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + return _didFulfillHandler + } + set { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + _didFulfillHandler = newValue + } + } + + /// Initializes a new expectation with a description of the condition it is checking. + /// + /// - Parameter description: A human-readable string used to describe the condition the expectation is checking. + public init(description: String = "no description provided", file: StaticString = #file, line: Int = #line) { + _expectationDescription = description + creationToken = XCTestExpectation.nextMonotonicallyIncreasingToken() + creationSourceLocation = SourceLocation(file: file, line: line) + } + + /// Marks an expectation as having been met. It's an error to call this + /// method on an expectation that has already been fulfilled, or when the + /// test case that vended the expectation has already completed. + /// + /// - Parameter file: The file name to use in the error message if + /// expectations are not met before the given timeout. Default is the file + /// containing the call to this method. It is rare to provide this + /// parameter when calling this method. + /// - Parameter line: The line number to use in the error message if the + /// expectations are not met 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. + /// + /// - Note: Whereas Objective-C XCTest determines the file and line + /// number the expectation was fulfilled using symbolication, this + /// implementation opts to take `file` and `line` as parameters instead. + /// As a result, the interface to these methods are not exactly identical + /// between 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`. + open func fulfill(_ file: StaticString = #file, line: Int = #line) { + let sourceLocation = SourceLocation(file: file, line: line) + + let didFulfillHandler: (() -> Void)? = XCTWaiter.subsystemQueue.sync { + // FIXME: Objective-C XCTest emits failures when expectations are + // fulfilled after the test cases that generated those + // expectations have completed. Similarly, this should cause an + // error as well. + + if queue_isFulfilled { + // FIXME: No regression tests exist for this feature. We may break it + // without ever realizing (similar to `continueAfterFailure`). + if _assertForOverFulfill { + fatalError("API violation - multiple calls made to fulfill() for \(queue_expectationDescription)") + } + + if let testCase = XCTCurrentTestCase { + testCase.recordFailure( + description: "API violation - multiple calls made to XCTestExpectation.fulfill() for \(queue_expectationDescription).", + at: sourceLocation, + expected: false) + } + return nil + } + + if queue_fulfill(sourceLocation: sourceLocation) { + return queue_didFulfillHandler + } else { + return nil + } + } + + didFulfillHandler?() + } + + private func queue_fulfill(sourceLocation: SourceLocation) -> Bool { + dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) + + numberOfFulfillments += 1 + + if numberOfFulfillments == queue_expectedFulfillmentCount { + queue_isFulfilled = true + _fulfillmentSourceLocation = sourceLocation + queue_fulfillmentToken = XCTestExpectation.queue_nextMonotonicallyIncreasingToken() + return true + } else { + return false + } + } + + internal func didBeginWaiting() { + // Override point for subclasses + } + + internal func cleanUp() { + // Override point for subclasses + } + +} + +extension XCTestExpectation: Equatable { + public static func == (lhs: XCTestExpectation, rhs: XCTestExpectation) -> Bool { + return lhs === rhs + } +} + +extension XCTestExpectation: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } +} + +extension XCTestExpectation: CustomStringConvertible { + public var description: String { + return expectationDescription + } +} diff --git a/Sources/XCTest/Public/XCTestCase+Performance.swift b/Sources/XCTest/Public/XCTestCase+Performance.swift new file mode 100644 index 000000000..401fb5e9c --- /dev/null +++ b/Sources/XCTest/Public/XCTestCase+Performance.swift @@ -0,0 +1,176 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2016 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 +// +// +// XCTestCase+Performance.swift +// Methods on XCTestCase for testing the performance of code blocks. +// + +public struct XCTPerformanceMetric : RawRepresentable, Equatable, Hashable { + public let rawValue: String + + public init(_ rawValue: String) { + self.rawValue = rawValue + } + + public init(rawValue: String) { + self.rawValue = rawValue + } +} + +public extension XCTPerformanceMetric { + /// Records wall clock time in seconds between `startMeasuring`/`stopMeasuring`. + static let wallClockTime = XCTPerformanceMetric(rawValue: WallClockTimeMetric.name) +} + +/// The following methods are called from within a test method to carry out +/// performance testing on blocks of code. +public extension XCTestCase { + + /// The names of the performance metrics to measure when invoking `measure(block:)`. + /// Returns `XCTPerformanceMetric_WallClockTime` by default. Subclasses can + /// override this to change the behavior of `measure(block:)` + class var defaultPerformanceMetrics: [XCTPerformanceMetric] { + return [.wallClockTime] + } + + /// Call from a test method to measure resources (`defaultPerformanceMetrics`) + /// used by the block in the current process. + /// + /// func testPerformanceOfMyFunction() { + /// measure { + /// // Do that thing you want to measure. + /// MyFunction(); + /// } + /// } + /// + /// - Parameter block: A block whose performance to measure. + /// - Bug: The `block` param should have no external label, but there seems + /// to be a swiftc bug that causes issues when such a parameter comes + /// after a defaulted arg. See https://bugs.swift.org/browse/SR-1483 This + /// API incompatibility with Apple XCTest can be worked around in practice + /// by using trailing closure syntax when calling this method. + /// - Note: Whereas Apple XCTest determines the file and line number of + /// measurements by using symbolication, this implementation opts to take + /// `file` and `line` as parameters instead. As a result, the interface to + /// these methods are not exactly identical between 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`. + func measure(file: StaticString = #file, line: Int = #line, block: () -> Void) { + measureMetrics(type(of: self).defaultPerformanceMetrics, + automaticallyStartMeasuring: true, + file: file, + line: line, + for: block) + } + + /// Call from a test method to measure resources (XCTPerformanceMetrics) used + /// by the block in the current process. Each metric will be measured across + /// calls to the block. The number of times the block will be called is undefined + /// and may change in the future. For one example of why, as long as the requested + /// performance metrics do not interfere with each other the API will measure + /// all metrics across the same calls to the block. If the performance metrics + /// may interfere the API will measure them separately. + /// + /// func testMyFunction2_WallClockTime() { + /// measureMetrics(type(of: self).defaultPerformanceMetrics, automaticallyStartMeasuring: false) { + /// + /// // Do setup work that needs to be done for every iteration but + /// // you don't want to measure before the call to `startMeasuring()` + /// SetupSomething(); + /// self.startMeasuring() + /// + /// // Do that thing you want to measure. + /// MyFunction() + /// self.stopMeasuring() + /// + /// // Do teardown work that needs to be done for every iteration + /// // but you don't want to measure after the call to `stopMeasuring()` + /// TeardownSomething() + /// } + /// } + /// + /// Caveats: + /// * If `true` was passed for `automaticallyStartMeasuring` and `startMeasuring()` + /// is called anyway, the test will fail. + /// * If `false` was passed for `automaticallyStartMeasuring` then `startMeasuring()` + /// must be called once and only once before the end of the block or the test will fail. + /// * If `stopMeasuring()` is called multiple times during the block the test will fail. + /// + /// - Parameter metrics: An array of Strings (XCTPerformanceMetrics) to measure. + /// Providing an unrecognized string is a test failure. + /// - Parameter automaticallyStartMeasuring: If `false`, `XCTestCase` will + /// not take any measurements until -startMeasuring is called. + /// - Parameter block: A block whose performance to measure. + /// - Note: Whereas Apple XCTest determines the file and line number of + /// measurements by using symbolication, this implementation opts to take + /// `file` and `line` as parameters instead. As a result, the interface to + /// these methods are not exactly identical between 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`. + func measureMetrics(_ metrics: [XCTPerformanceMetric], automaticallyStartMeasuring: Bool, file: StaticString = #file, line: Int = #line, for block: () -> Void) { + guard _performanceMeter == nil else { + return recordAPIViolation(description: "Can only record one set of metrics per test method.", file: file, line: line) + } + + PerformanceMeter.measureMetrics(metrics.map({ $0.rawValue }), delegate: self, file: file, line: line) { meter in + self._performanceMeter = meter + if automaticallyStartMeasuring { + meter.startMeasuring(file: file, line: line) + } + block() + } + } + + /// Call this from within a measure block to set the beginning of the critical + /// section. Measurement of metrics will start at this point. + /// - Note: Whereas Apple XCTest determines the file and line number of + /// measurements by using symbolication, this implementation opts to take + /// `file` and `line` as parameters instead. As a result, the interface to + /// these methods are not exactly identical between 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`. + func startMeasuring(file: StaticString = #file, line: Int = #line) { + guard let performanceMeter = _performanceMeter, !performanceMeter.didFinishMeasuring else { + return recordAPIViolation(description: "Cannot start measuring. startMeasuring() is only supported from a block passed to measureMetrics(...).", file: file, line: line) + } + performanceMeter.startMeasuring(file: file, line: line) + } + + /// Call this from within a measure block to set the ending of the critical + /// section. Measurement of metrics will stop at this point. + /// - Note: Whereas Apple XCTest determines the file and line number of + /// measurements by using symbolication, this implementation opts to take + /// `file` and `line` as parameters instead. As a result, the interface to + /// these methods are not exactly identical between 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`. + func stopMeasuring(file: StaticString = #file, line: Int = #line) { + guard let performanceMeter = _performanceMeter, !performanceMeter.didFinishMeasuring else { + return recordAPIViolation(description: "Cannot stop measuring. stopMeasuring() is only supported from a block passed to measureMetrics(...).", file: file, line: line) + } + performanceMeter.stopMeasuring(file: file, line: line) + } +} + +extension XCTestCase: PerformanceMeterDelegate { + internal func recordAPIViolation(description: String, file: StaticString, line: Int) { + recordFailure(withDescription: "API violation - \(description)", + inFile: String(describing: file), + atLine: line, + expected: false) + } + + internal func recordMeasurements(results: String, file: StaticString, line: Int) { + XCTestObservationCenter.shared.testCase(self, didMeasurePerformanceResults: results, file: file, line: line) + } + + internal func recordFailure(description: String, file: StaticString, line: Int) { + recordFailure(withDescription: "failed: " + description, inFile: String(describing: file), atLine: line, expected: true) + } +} From 6763c7294dadac04991310e3c8cbbb48e0bd167e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 4 Jun 2020 18:27:45 +0900 Subject: [PATCH 06/26] Good bye WASIFoundation (#1) --- Sources/XCTest/Private/PrintObserver.swift | 3 --- Sources/XCTest/Public/XCTestRun.swift | 4 ---- Sources/XCTest/Public/XCTestSuiteRun.swift | 4 ---- 3 files changed, 11 deletions(-) diff --git a/Sources/XCTest/Private/PrintObserver.swift b/Sources/XCTest/Private/PrintObserver.swift index bed4700e0..e8fa0e4f4 100644 --- a/Sources/XCTest/Private/PrintObserver.swift +++ b/Sources/XCTest/Private/PrintObserver.swift @@ -14,9 +14,6 @@ #if canImport(Glibc) import Glibc #endif -#if canImport(WASIFoundation) -import WASIFoundation -#endif /// Prints textual representations of each XCTestObservation event to stdout. /// Mirrors the Apple XCTest output exactly. diff --git a/Sources/XCTest/Public/XCTestRun.swift b/Sources/XCTest/Public/XCTestRun.swift index f3a7f2fb1..4ca345268 100644 --- a/Sources/XCTest/Public/XCTestRun.swift +++ b/Sources/XCTest/Public/XCTestRun.swift @@ -11,10 +11,6 @@ // A test run collects information about the execution of a test. // -#if canImport(WASIFoundation) -import WASIFoundation -#endif - /// A test run collects information about the execution of a test. Failures in /// explicit test assertions are classified as "expected", while failures from /// unrelated or uncaught exceptions are classified as "unexpected". diff --git a/Sources/XCTest/Public/XCTestSuiteRun.swift b/Sources/XCTest/Public/XCTestSuiteRun.swift index f2295ab29..666a3b069 100644 --- a/Sources/XCTest/Public/XCTestSuiteRun.swift +++ b/Sources/XCTest/Public/XCTestSuiteRun.swift @@ -11,10 +11,6 @@ // A test run for an `XCTestSuite`. // -#if canImport(WASIFoundation) -import WASIFoundation -#endif - /// A test run for an `XCTestSuite`. open class XCTestSuiteRun: XCTestRun { /// The combined `testDuration` of each test case run in the suite. From 0bc8dee07d4bf1606db339de17db88d4c559f4ae Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sat, 6 Jun 2020 22:15:46 +0100 Subject: [PATCH 07/26] Fix .swiftmodule and .swiftdoc install directory --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a54770e63..53431fec0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -127,11 +127,11 @@ set_property(GLOBAL APPEND PROPERTY XCTest_EXPORTS XCTest) get_swift_host_arch(swift_arch) install(TARGETS XCTest ARCHIVE DESTINATION lib/swift$<$>:_static>/$ - LIBRARY DESTINATION lib/swift$<$>:_static>/$ + LIBRARY DESTINATION lib/swift/$ RUNTIME DESTINATION bin) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/swift/XCTest.swiftdoc ${CMAKE_CURRENT_BINARY_DIR}/swift/XCTest.swiftmodule - DESTINATION lib/swift$<$>:_static>/$/${swift_arch}) + DESTINATION lib/swift/$/${swift_arch}) add_subdirectory(cmake/modules) From 83d27fc3acf9f34ac4b2c1bd243e3b62a0143573 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 14 Jun 2020 20:34:33 +0100 Subject: [PATCH 08/26] Add new observer, make XCTMain more configurable --- Sources/XCTest/Public/CodableObserver.swift | 191 ++++++++++++++++++++ Sources/XCTest/Public/XCTestMain.swift | 11 +- 2 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 Sources/XCTest/Public/CodableObserver.swift diff --git a/Sources/XCTest/Public/CodableObserver.swift b/Sources/XCTest/Public/CodableObserver.swift new file mode 100644 index 000000000..31498acaa --- /dev/null +++ b/Sources/XCTest/Public/CodableObserver.swift @@ -0,0 +1,191 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2020 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 +// +// +// CodableObserver.swift +// Provides test progress as a certain model type to a handler closure +// + +public struct TimedEvent: Codable { + public let name: String + public let date: Date +} + +public struct FailedTestCase: Codable { + public let filePath: String? + public let lineNumber: Int + public let name: String + public let description: String +} + +public struct FinishedTestCase: Codable { + public enum State: String, Codable, CaseIterable { + case skipped + case passed + case failed + } + + public let state: State + public let durationInSeconds: TimeInterval +} + +public struct FinishedTestSuite: Codable { + public let executionCount: Int + public let totalFailureCount: Int + public let unexpectedExceptionCount: Int + public let testDuration: TimeInterval + public let totalDuration: TimeInterval +} + +public enum Event: Codable { + public enum CodingError: Error { + case invalidVersion + } + + public enum Kind: String, Codable, CaseIterable { + case testSuiteStarted + case testCaseStarted + case testCaseFailed + case testCaseFinished + case testSuiteFinished + } + + private enum CodingKeys: CodingKey { + case version + case kind + case value + } + + case testSuiteStarted(TimedEvent) + case testCaseStarted(TimedEvent) + case testCaseFailed(FailedTestCase) + case testCaseFinished(FinishedTestCase) + case testSuiteFinished(FinishedTestSuite) + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let version = try container.decode(Int.self, forKey: .version) + guard version == 0 else { throw CodingError.invalidVersion } + + let kind = try container.decode(Kind.self, forKey: .kind) + + switch kind { + case .testSuiteStarted: + self = .testSuiteStarted(try container.decode(TimedEvent.self, forKey: .value)) + case .testCaseStarted: + self = .testCaseStarted(try container.decode(TimedEvent.self, forKey: .value)) + case .testCaseFailed: + self = .testCaseFailed(try container.decode(FailedTestCase.self, forKey: .value)) + case .testCaseFinished: + self = .testCaseFinished(try container.decode(FinishedTestCase.self, forKey: .value)) + case .testSuiteFinished: + self = .testSuiteFinished(try container.decode(FinishedTestSuite.self, forKey: .value)) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(0, forKey: CodingKeys.version) + + switch self { + case let .testSuiteStarted(value): + try container.encode(Kind.testSuiteStarted, forKey: CodingKeys.kind) + try container.encode(value, forKey: CodingKeys.value) + case let .testCaseStarted(value): + try container.encode(Kind.testCaseStarted, forKey: CodingKeys.kind) + try container.encode(value, forKey: CodingKeys.value) + case let .testCaseFailed(value): + try container.encode(Kind.testCaseFailed, forKey: CodingKeys.kind) + try container.encode(value, forKey: CodingKeys.value) + case let .testCaseFinished(value): + try container.encode(Kind.testCaseFinished, forKey: CodingKeys.kind) + try container.encode(value, forKey: CodingKeys.value) + case let .testSuiteFinished(value): + try container.encode(Kind.testSuiteFinished, forKey: CodingKeys.kind) + try container.encode(value, forKey: CodingKeys.value) + } + } +} + +/// Provides representations of each XCTestObservation event to a given handler closure. +public class CodableObserver: NSObject, XCTestObservation { + let handler: (Event) -> () + + public init(handler: @escaping (Event) -> ()) { + self.handler = handler + } + + #if !os(WASI) + public func testBundleWillStart(_ testBundle: Bundle) {} + #endif + + public func testSuiteWillStart(_ testSuite: XCTestSuite) { + handler(Event.testSuiteStarted(.init( + name: testSuite.name, + date: testSuite.testRun!.startDate! + ))) + } + + public func testCaseWillStart(_ testCase: XCTestCase) { + handler(Event.testSuiteStarted(.init( + name: testCase.name, + date: testCase.testRun!.startDate! + ))) + } + + public func testCase( + _ testCase: XCTestCase, + didFailWithDescription description: String, + inFile filePath: String?, + atLine lineNumber: Int + ) { + handler(Event.testCaseFailed(.init( + filePath: filePath, + lineNumber: lineNumber, + name: testCase.name, + description: description + ))) + } + + public func testCaseDidFinish(_ testCase: XCTestCase) { + let testRun = testCase.testRun! + + let state: FinishedTestCase.State + if testRun.hasSucceeded { + if testRun.hasBeenSkipped { + state = .skipped + } else { + state = .passed + } + } else { + state = .failed + } + + handler(Event.testCaseFinished(.init( + state: state, + durationInSeconds: testRun.totalDuration + ))) + } + + public func testSuiteDidFinish(_ testSuite: XCTestSuite) { + let testRun = testSuite.testRun! + + handler(Event.testSuiteFinished(.init( + executionCount: testRun.executionCount, + totalFailureCount: testRun.totalFailureCount, + unexpectedExceptionCount: testRun.unexpectedExceptionCount, + testDuration: testRun.testDuration, + totalDuration: testRun.totalDuration + ))) + } + + #if !os(WASI) + public func testBundleDidFinish(_ testBundle: Bundle) {} + #endif +} diff --git a/Sources/XCTest/Public/XCTestMain.swift b/Sources/XCTest/Public/XCTestMain.swift index 909a8a8f9..fbc56e539 100644 --- a/Sources/XCTest/Public/XCTestMain.swift +++ b/Sources/XCTest/Public/XCTestMain.swift @@ -62,7 +62,16 @@ public func XCTMain(_ testCases: [XCTestCaseEntry]) -> Never { XCTMain(testCases, arguments: CommandLine.arguments) } + public func XCTMain(_ testCases: [XCTestCaseEntry], arguments: [String]) -> Never { + XCTMain(testCases, arguments: arguments, observation: PrintObserver()) +} + +public func XCTMain( + _ testCases: [XCTestCaseEntry], + arguments: [String], + observation: XCTestObservation +) -> Never { #if !os(WASI) let testBundle = Bundle.main #endif @@ -134,7 +143,7 @@ public func XCTMain(_ testCases: [XCTestCaseEntry], arguments: [String]) -> Neve case .run(selectedTestNames: _): // Add a test observer that prints test progress to stdout. let observationCenter = XCTestObservationCenter.shared - observationCenter.addTestObserver(PrintObserver()) + observationCenter.addTestObserver(observation) #if !os(WASI) observationCenter.testBundleWillStart(testBundle) From b85ea3b1aeab35c8e53ff4626496393318eadd1c Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 6 Jul 2020 09:58:54 +0100 Subject: [PATCH 09/26] Add .github/pull.yml to track upstream changes --- .github/pull.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/pull.yml diff --git a/.github/pull.yml b/.github/pull.yml new file mode 100644 index 000000000..ffcbf74b8 --- /dev/null +++ b/.github/pull.yml @@ -0,0 +1,15 @@ +version: "1" +rules: + - base: swiftwasm + upstream: master + mergeMethod: merge + - base: master + upstream: apple:master + mergeMethod: hardreset + - base: release/5.3 + upstream: apple:release/5.3 + mergeMethod: hardreset + - base: swiftwasm-release/5.3 + upstream: release/5.3 + mergeMethod: merge +label: ":arrow_heading_down: Upstream Tracking" From 1c32474e3ac3acc226406959111b524bbbd13813 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 6 Jul 2020 15:18:08 +0100 Subject: [PATCH 10/26] Add missing keyword argument to XCTestCase.swift --- Sources/XCTest/Public/XCTestCase.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/XCTest/Public/XCTestCase.swift b/Sources/XCTest/Public/XCTestCase.swift index 0819fbf3a..7fc6c6581 100644 --- a/Sources/XCTest/Public/XCTestCase.swift +++ b/Sources/XCTest/Public/XCTestCase.swift @@ -264,7 +264,7 @@ open class XCTestCase: XCTest { #if os(WASI) let blocks = closure() #else - let blocks = teardownBlocksQueue.sync(closure) + let blocks = teardownBlocksQueue.sync(execute: closure) #endif for block in blocks.reversed() { From b0504aefb0028c82871eb4bea93bbb48137088c5 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 23 Sep 2020 18:01:38 +0100 Subject: [PATCH 11/26] Pull from the `main` upstream branch Updated for consistency with swiftwasm/swift#1812. --- .github/pull.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/pull.yml b/.github/pull.yml index ffcbf74b8..6443cd0c6 100644 --- a/.github/pull.yml +++ b/.github/pull.yml @@ -1,10 +1,10 @@ version: "1" rules: - base: swiftwasm - upstream: master + upstream: main mergeMethod: merge - - base: master - upstream: apple:master + - base: main + upstream: apple:main mergeMethod: hardreset - base: release/5.3 upstream: apple:release/5.3 From a56484c7d3cbff11547e31b6139749b75bec8087 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 14 Oct 2020 12:34:31 +0100 Subject: [PATCH 12/26] Add `CodableObserver.swift` to `CMakeLists.txt` --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index eb257f8cf..98b81afb0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ add_library(XCTest Sources/XCTest/Private/ArgumentParser.swift Sources/XCTest/Private/SourceLocation.swift Sources/XCTest/Private/IgnoredErrors.swift + Sources/XCTest/Public/CodableObserver.swift Sources/XCTest/Public/XCTestRun.swift Sources/XCTest/Public/XCTestMain.swift Sources/XCTest/Public/XCTestCase.swift From 8e4098a63544794ee144398c55110eec0cc29804 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 24 Oct 2020 08:03:20 +0900 Subject: [PATCH 13/26] [WASM] Install swiftmodule in swift_static when building static library --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 98b81afb0..5b59cba13 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -133,6 +133,6 @@ install(TARGETS XCTest install(FILES ${CMAKE_CURRENT_BINARY_DIR}/swift/XCTest.swiftdoc ${CMAKE_CURRENT_BINARY_DIR}/swift/XCTest.swiftmodule - DESTINATION lib/swift/$/${swift_arch}) + DESTINATION lib/swift$<$>:_static>/$/${swift_arch}) add_subdirectory(cmake/modules) From bc355969f7fa87a61f1e62a7a367dfb53eecbf5d Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 24 Oct 2020 08:13:12 +0900 Subject: [PATCH 14/26] [WASM] Install libs in swift_static when building static library --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b59cba13..2942f85a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -128,7 +128,7 @@ set_property(GLOBAL APPEND PROPERTY XCTest_EXPORTS XCTest) get_swift_host_arch(swift_arch) install(TARGETS XCTest ARCHIVE DESTINATION lib/swift$<$>:_static>/$ - LIBRARY DESTINATION lib/swift/$ + LIBRARY DESTINATION lib/swift$<$>:_static>/$ RUNTIME DESTINATION bin) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/swift/XCTest.swiftdoc From f08163c39614e1de842c357e79ff0a1d37db177b Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sat, 19 Dec 2020 20:24:33 +0000 Subject: [PATCH 15/26] Add `swiftwasm-release/5.4` to `pull.yml` --- .github/pull.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/pull.yml b/.github/pull.yml index 6443cd0c6..1e8bdcd35 100644 --- a/.github/pull.yml +++ b/.github/pull.yml @@ -6,10 +6,18 @@ rules: - base: main upstream: apple:main mergeMethod: hardreset + - base: release/5.3 upstream: apple:release/5.3 mergeMethod: hardreset - base: swiftwasm-release/5.3 upstream: release/5.3 mergeMethod: merge + + - base: release/5.4 + upstream: apple:release/5.4 + mergeMethod: hardreset + - base: swiftwasm-release/5.4 + upstream: release/5.4 + mergeMethod: merge label: ":arrow_heading_down: Upstream Tracking" From 76b479ba1f76cfb1331eba16fba3deee5106de3a Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 11 Mar 2022 03:51:15 +0000 Subject: [PATCH 16/26] [Wasm] Enable performance APIs for WASI --- CMakeLists.txt | 6 +++--- Sources/XCTest/Public/XCTestCase.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e8e0e06c6..c70b2abd2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,10 +22,7 @@ endif() set(XCTEST_WASI_UNAVAILABLE_SOURCES) if(NOT CMAKE_SYSTEM_NAME STREQUAL WASI) list(APPEND XCTEST_WASI_UNAVAILABLE_SOURCES - Sources/XCTest/Private/WallClockTimeMetric.swift - Sources/XCTest/Private/PerformanceMeter.swift Sources/XCTest/Private/WaiterManager.swift - Sources/XCTest/Public/XCTestCase+Performance.swift Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift @@ -44,6 +41,9 @@ add_library(XCTest Sources/XCTest/Private/ArgumentParser.swift Sources/XCTest/Private/SourceLocation.swift Sources/XCTest/Private/IgnoredErrors.swift + Sources/XCTest/Private/PerformanceMeter.swift + Sources/XCTest/Private/WallClockTimeMetric.swift + Sources/XCTest/Public/XCTestCase+Performance.swift Sources/XCTest/Public/CodableObserver.swift Sources/XCTest/Public/XCTestRun.swift Sources/XCTest/Public/XCTestMain.swift diff --git a/Sources/XCTest/Public/XCTestCase.swift b/Sources/XCTest/Public/XCTestCase.swift index 7fc6c6581..ceb2ee3a1 100644 --- a/Sources/XCTest/Public/XCTestCase.swift +++ b/Sources/XCTest/Public/XCTestCase.swift @@ -84,10 +84,10 @@ open class XCTestCase: XCTest { } } } +#endif /// An internal object implementing performance measurements. internal var _performanceMeter: PerformanceMeter? -#endif open override var testRunClass: AnyClass? { return XCTestCaseRun.self From 062e8fd0d56f304b79dbbf4f278117dd34aa328a Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 15 Mar 2022 09:30:42 +0000 Subject: [PATCH 17/26] Add 5.5 and 5.6 release branches to `pull.yml` --- .github/pull.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/pull.yml b/.github/pull.yml index 1e8bdcd35..f656ab331 100644 --- a/.github/pull.yml +++ b/.github/pull.yml @@ -20,4 +20,19 @@ rules: - base: swiftwasm-release/5.4 upstream: release/5.4 mergeMethod: merge + + - base: release/5.5 + upstream: apple:release/5.5 + mergeMethod: hardreset + - base: swiftwasm-release/5.5 + upstream: release/5.5 + mergeMethod: merge + + - base: release/5.6 + upstream: apple:release/5.6 + mergeMethod: hardreset + - base: swiftwasm-release/5.6 + upstream: release/5.6 + mergeMethod: merge + label: ":arrow_heading_down: Upstream Tracking" From 678221d3125063e909076c28718f3f0ce86a5ead Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 15 Mar 2022 10:42:45 +0000 Subject: [PATCH 18/26] Add `#if os(WASI)` checks in `TeardownBlocksState` --- .../Private/XCTestCase.TearDownBlocksState.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift b/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift index 83f43fe47..8bbe476b8 100644 --- a/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift +++ b/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift @@ -27,18 +27,29 @@ extension XCTestCase { } func append(_ block: @escaping () throws -> Void) { + #if os(WASI) + precondition(wasFinalized == false, "API violation -- attempting to add a teardown block after teardown blocks have been dequeued") + blocks.append(block) + #else XCTWaiter.subsystemQueue.sync { precondition(wasFinalized == false, "API violation -- attempting to add a teardown block after teardown blocks have been dequeued") blocks.append(block) } + #endif } func finalize() -> [() throws -> Void] { + #if os(WASI) + precondition(wasFinalized == false, "API violation -- attempting to run teardown blocks after they've already run") + wasFinalized = true + return blocks + #else XCTWaiter.subsystemQueue.sync { precondition(wasFinalized == false, "API violation -- attempting to run teardown blocks after they've already run") wasFinalized = true return blocks } + #endif } } } From 0c4776f8ae4f3854c1eddebb7a5e0fa6991efd3b Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 15 Mar 2022 11:37:59 +0000 Subject: [PATCH 19/26] Clean up declarations in `XCTestCase.swift` --- Sources/XCTest/Public/XCTestCase.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Sources/XCTest/Public/XCTestCase.swift b/Sources/XCTest/Public/XCTestCase.swift index 5d06f49c2..cdb3bed29 100644 --- a/Sources/XCTest/Public/XCTestCase.swift +++ b/Sources/XCTest/Public/XCTestCase.swift @@ -233,11 +233,13 @@ open class XCTestCase: XCTest { } do { + #if !os(WASI) if #available(macOS 12.0, *) { try awaitUsingExpectation { try await self.setUp() } } + #endif } catch { handleErrorDuringSetUp(error) } @@ -277,18 +279,15 @@ open class XCTestCase: XCTest { } catch { handleErrorDuringTearDown(error) } - #if os(WASI) - let blocks = closure() - #else - let blocks = teardownBlocksQueue.sync(execute: closure) - #endif do { + #if !os(WASI) if #available(macOS 12.0, *) { try awaitUsingExpectation { try await self.tearDown() } } + #endif } catch { handleErrorDuringTearDown(error) } @@ -332,6 +331,7 @@ private func test(_ testFunc: @escaping (T) -> () throws -> Void) } } +#if !os(WASI) @available(macOS 12.0, *) public func asyncTest( _ testClosureGenerator: @escaping (T) -> () async throws -> Void @@ -367,6 +367,7 @@ func awaitUsingExpectation( throw error } } +#endif private final class ThrownErrorWrapper: @unchecked Sendable { From 2e9984f1370bd77e7734911e6d4e01e9bfaee8c9 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 15 Mar 2022 12:43:25 +0000 Subject: [PATCH 20/26] Stop using `awaitUsingExpectation` on WASI --- Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift b/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift index 8bbe476b8..b4c199ad0 100644 --- a/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift +++ b/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift @@ -22,7 +22,13 @@ extension XCTestCase { @available(macOS 12.0, *) func appendAsync(_ block: @Sendable @escaping () async throws -> Void) { self.append { + #if os(WASI) try awaitUsingExpectation { try await block() } + #else + Task { + try await block() + } + #endif } } From cea8a590f057fb8388c9c2bfec391106cd21b2e1 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 15 Mar 2022 13:12:04 +0000 Subject: [PATCH 21/26] Fix `#if !os(WASI)` condition --- Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift b/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift index b4c199ad0..3384c1f5f 100644 --- a/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift +++ b/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift @@ -22,7 +22,7 @@ extension XCTestCase { @available(macOS 12.0, *) func appendAsync(_ block: @Sendable @escaping () async throws -> Void) { self.append { - #if os(WASI) + #if !os(WASI) try awaitUsingExpectation { try await block() } #else Task { From f048af00e36328dc3cbe477cb2b362b1b9fb9f49 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 15 Mar 2022 13:32:51 +0000 Subject: [PATCH 22/26] Exclude `ThrownErrorWrapper` on WASI --- Sources/XCTest/Public/XCTestCase.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/XCTest/Public/XCTestCase.swift b/Sources/XCTest/Public/XCTestCase.swift index cdb3bed29..0a3ebb021 100644 --- a/Sources/XCTest/Public/XCTestCase.swift +++ b/Sources/XCTest/Public/XCTestCase.swift @@ -367,7 +367,6 @@ func awaitUsingExpectation( throw error } } -#endif private final class ThrownErrorWrapper: @unchecked Sendable { @@ -382,6 +381,7 @@ private final class ThrownErrorWrapper: @unchecked Sendable { } } } +#endif // This time interval is set to a very large value due to their being no real native timeout functionality within corelibs-xctest. From beb77f6a7ced225ddcb73f00ad8f3576c7225721 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 25 Apr 2022 16:04:14 +0100 Subject: [PATCH 23/26] Update `pull.yml` for 5.7 release branch --- .github/pull.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/pull.yml b/.github/pull.yml index f656ab331..833c12e2e 100644 --- a/.github/pull.yml +++ b/.github/pull.yml @@ -35,4 +35,11 @@ rules: upstream: release/5.6 mergeMethod: merge + - base: release/5.7 + upstream: apple:release/5.7 + mergeMethod: hardreset + - base: swiftwasm-release/5.7 + upstream: release/5.7 + mergeMethod: merge + label: ":arrow_heading_down: Upstream Tracking" From 9f39bf6e0178b9df7eb3e5a22b7fea2c962671c3 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 30 Jun 2022 23:02:26 +0900 Subject: [PATCH 24/26] [Wasm] Unlock async test and expectation APIs (#40) * [Wasm] Unlock async test and expectation APIs * [Wasm] Remove awaitUsingExpectation guards * [Wasm] Don't donate current thread to yield to JS loop This patch enables async test methods with JS event loop based executor. To avoid breaking public API interfaces, this patch adds manually Note that expectation API doesn't work on the executor because it has blocking interface and we don't want to break interface. * [Wasm] Compile with fPIC and donate thread by ABI runtime function * [Wasm] import swift_task_asyncMainDrainQueue as C symbol --- CMakeLists.txt | 36 ++++++++-- .../ConcurrencySupport/ConcurrencySupport.cpp | 22 ++++++ Sources/XCTest/Private/Shims.swift | 49 +++++++++++++ Sources/XCTest/Private/WaiterManager.swift | 15 ++++ .../XCTestCase.TearDownBlocksState.swift | 15 ++-- .../Public/Asynchronous/XCTWaiter.swift | 44 ++++++++++++ Sources/XCTest/Public/XCAbstractTest.swift | 2 + Sources/XCTest/Public/XCTestCase.swift | 68 ++++++++++++------- Sources/XCTest/Public/XCTestMain.swift | 21 +++++- Sources/XCTest/Public/XCTestSuite.swift | 8 ++- 10 files changed, 238 insertions(+), 42 deletions(-) create mode 100644 Sources/XCTest/Private/ConcurrencySupport/ConcurrencySupport.cpp create mode 100644 Sources/XCTest/Private/Shims.swift diff --git a/CMakeLists.txt b/CMakeLists.txt index 534dcfcc3..27c6f261f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,11 +3,19 @@ cmake_minimum_required(VERSION 3.15.1) list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) -project(XCTest LANGUAGES Swift) +project(XCTest LANGUAGES Swift CXX) option(BUILD_SHARED_LIBS "Build shared libraries" ON) option(USE_FOUNDATION_FRAMEWORK "Use Foundation.framework on Darwin" NO) +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}") + if(NOT CMAKE_SYSTEM_NAME STREQUAL Darwin AND NOT CMAKE_SYSTEM_PROCESSOR STREQUAL wasm32) find_package(dispatch CONFIG REQUIRED) find_package(Foundation CONFIG REQUIRED) @@ -22,13 +30,9 @@ endif() set(XCTEST_WASI_UNAVAILABLE_SOURCES) if(NOT CMAKE_SYSTEM_NAME STREQUAL WASI) list(APPEND XCTEST_WASI_UNAVAILABLE_SOURCES - Sources/XCTest/Private/WaiterManager.swift - Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift - Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift - Sources/XCTest/Public/Asynchronous/XCTWaiter.swift - Sources/XCTest/Public/Asynchronous/XCTWaiter+Validation.swift) + Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift) endif() add_library(XCTest @@ -40,10 +44,12 @@ add_library(XCTest Sources/XCTest/Private/PrintObserver.swift Sources/XCTest/Private/ArgumentParser.swift Sources/XCTest/Private/SourceLocation.swift + Sources/XCTest/Private/WaiterManager.swift Sources/XCTest/Private/IgnoredErrors.swift Sources/XCTest/Private/PerformanceMeter.swift Sources/XCTest/Private/WallClockTimeMetric.swift Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift + Sources/XCTest/Private/Shims.swift Sources/XCTest/Public/XCTestCase+Performance.swift Sources/XCTest/Public/CodableObserver.swift Sources/XCTest/Public/XCTestRun.swift @@ -58,8 +64,26 @@ add_library(XCTest Sources/XCTest/Public/XCTestObservationCenter.swift Sources/XCTest/Public/XCTAssert.swift Sources/XCTest/Public/XCTSkip.swift + Sources/XCTest/Public/Asynchronous/XCTWaiter+Validation.swift + Sources/XCTest/Public/Asynchronous/XCTWaiter.swift + Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift ${XCTEST_WASI_UNAVAILABLE_SOURCES}) + +add_library(XCTestConcurrencySupport OBJECT + Sources/XCTest/Private/ConcurrencySupport/ConcurrencySupport.cpp) +set_property(TARGET XCTestConcurrencySupport PROPERTY POSITION_INDEPENDENT_CODE ON) +add_dependencies(XCTest XCTestConcurrencySupport) + +set_property(TARGET XCTest PROPERTY STATIC_LIBRARY_OPTIONS + $) +target_link_options(XCTest PRIVATE $) + +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) diff --git a/Sources/XCTest/Private/ConcurrencySupport/ConcurrencySupport.cpp b/Sources/XCTest/Private/ConcurrencySupport/ConcurrencySupport.cpp new file mode 100644 index 000000000..0dbd7851d --- /dev/null +++ b/Sources/XCTest/Private/ConcurrencySupport/ConcurrencySupport.cpp @@ -0,0 +1,22 @@ +#define SWIFT_CC_swift __attribute__((swiftcall)) +#define SWIFT_CC(CC) SWIFT_CC_##CC + +#define SWIFT_ATTRIBUTE_FOR_IMPORTS __attribute__((__visibility__("default"))) +#define SWIFT_EXPORT_FROM_ATTRIBUTE(LIBRARY) SWIFT_ATTRIBUTE_FOR_IMPORTS +#define SWIFT_EXPORT_FROM(LIBRARY) SWIFT_EXPORT_FROM_ATTRIBUTE(LIBRARY) + + +SWIFT_EXPORT_FROM(swift_Concurrency) +extern void *_Nullable swift_task_enqueueGlobal_hook; + +extern "C" SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift) +void swift_task_asyncMainDrainQueue [[noreturn]](); + +SWIFT_CC(swift) +extern "C" void XCTMainRunLoopMain(void) { + // If the global executor is handled by outside environment (e.g. JavaScript), + // we can't donate thread because it will stop the outside event loop. + if (swift_task_enqueueGlobal_hook == nullptr) { + swift_task_asyncMainDrainQueue(); + } +} diff --git a/Sources/XCTest/Private/Shims.swift b/Sources/XCTest/Private/Shims.swift new file mode 100644 index 000000000..b42405cf0 --- /dev/null +++ b/Sources/XCTest/Private/Shims.swift @@ -0,0 +1,49 @@ +#if USE_SWIFT_CONCURRENCY_WAITER + +struct DispatchPredicate { + static func onQueue(_: X) -> Self { + return DispatchPredicate() + } + + static func notOnQueue(_: X) -> Self { + return DispatchPredicate() + } +} + +func dispatchPrecondition(condition: DispatchPredicate) {} + +extension XCTWaiter { + struct BlockingQueue { + init(label: String) {} + + func sync(_ body: () -> T) -> T { + body() + } + func async(_ body: @escaping () -> Void) { + body() + } + } + + typealias DispatchQueue = BlockingQueue + + struct RunLoop { + static let current = RunLoop() + } + + class Thread: Equatable { + var threadDictionary: [String: Any] = [:] + + static let current: Thread = Thread() + + static func == (lhs: Thread, rhs: Thread) -> Bool { + return true + } + } +} + +extension WaiterManager { + typealias DispatchQueue = XCTWaiter.DispatchQueue + typealias Thread = XCTWaiter.Thread +} + +#endif diff --git a/Sources/XCTest/Private/WaiterManager.swift b/Sources/XCTest/Private/WaiterManager.swift index f705165fe..9d99d117d 100644 --- a/Sources/XCTest/Private/WaiterManager.swift +++ b/Sources/XCTest/Private/WaiterManager.swift @@ -21,7 +21,11 @@ internal protocol ManageableWaiter: AnyObject, Equatable { private protocol ManageableWaiterWatchdog { func cancel() } +#if USE_SWIFT_CONCURRENCY_WAITER +extension Task: ManageableWaiterWatchdog {} +#else extension DispatchWorkItem: ManageableWaiterWatchdog {} +#endif /// This class manages the XCTWaiter instances which are currently waiting on a particular thread. /// It facilitates "nested" waiters, allowing an outer waiter to interrupt inner waiters if it times @@ -111,6 +115,16 @@ internal final class WaiterManager : NSObject { } } +#if USE_SWIFT_CONCURRENCY_WAITER + private static func installWatchdog(for waiter: WaiterType, timeout: TimeInterval) -> ManageableWaiterWatchdog { + let outerTimeoutSlop = TimeInterval(0.25) + let task = Task { [weak waiter] in + try await Task.sleep(nanoseconds: UInt64((timeout + outerTimeoutSlop) * 1_000_000_000)) + waiter?.queue_handleWatchdogTimeout() + } + return task + } +#else private static func installWatchdog(for waiter: WaiterType, timeout: TimeInterval) -> ManageableWaiterWatchdog { // Use DispatchWorkItem instead of a basic closure since it can be canceled. let watchdog = DispatchWorkItem { [weak waiter] in @@ -123,6 +137,7 @@ internal final class WaiterManager : NSObject { return watchdog } +#endif func queue_handleWatchdogTimeout(of waiter: WaiterType) { dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) diff --git a/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift b/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift index 3384c1f5f..bdde83c8b 100644 --- a/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift +++ b/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift @@ -13,7 +13,7 @@ extension XCTestCase { final class TeardownBlocksState { private var wasFinalized = false - private var blocks: [() throws -> Void] = [] + private var blocks: [() async throws -> Void] = [] // We don't want to overload append(_:) below because of how Swift will implicitly promote sync closures to async closures, // which can unexpectedly change their semantics in difficult to track down ways. @@ -21,14 +21,11 @@ extension XCTestCase { // Because of this, we chose the unusual decision to forgo overloading (which is a super sweet language feature <3) to prevent this issue from surprising any contributors to corelibs-xctest @available(macOS 12.0, *) func appendAsync(_ block: @Sendable @escaping () async throws -> Void) { - self.append { - #if !os(WASI) - try awaitUsingExpectation { try await block() } - #else - Task { - try await block() + XCTWaiter.subsystemQueue.sync { + precondition(wasFinalized == false, "API violation -- attempting to add a teardown block after teardown blocks have been dequeued") + blocks.append { + try await awaitUsingExpectation { try await block() } } - #endif } } @@ -44,7 +41,7 @@ extension XCTestCase { #endif } - func finalize() -> [() throws -> Void] { + func finalize() -> [() async throws -> Void] { #if os(WASI) precondition(wasFinalized == false, "API violation -- attempting to run teardown blocks after they've already run") wasFinalized = true diff --git a/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift b/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift index c9ed6d381..9aac94fa2 100644 --- a/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift +++ b/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift @@ -119,6 +119,7 @@ open class XCTWaiter { internal var waitSourceLocation: SourceLocation? private weak var manager: WaiterManager? private var runLoop: RunLoop? + private var primitiveWaitState = PrimitiveWaitState() private weak var _delegate: XCTWaiterDelegate? private let delegateQueue = DispatchQueue(label: "org.swift.XCTest.XCTWaiter.delegate") @@ -356,7 +357,49 @@ open class XCTWaiter { } +#if USE_SWIFT_CONCURRENCY_WAITER +@_silgen_name("swift_task_donateThreadToGlobalExecutorUntil") +func _task_donateThreadToGlobalExecutorUntil(_ condition: @convention(c) (UnsafeMutableRawPointer) -> Bool, _ context: UnsafeMutableRawPointer) + private extension XCTWaiter { + struct PrimitiveWaitState { + struct Context { + var runOnce: Bool = false + var isCancelled: Bool = false + } + var context: UnsafeMutablePointer? + } + + func primitiveWait(using runLoop: RunLoop, duration timeout: TimeInterval) { + // If the waiter is already waiting, do nothing + guard primitiveWaitState.context == nil else { return } + + // reset the waiting state + let context = UnsafeMutablePointer.allocate(capacity: 1) + context.initialize(to: PrimitiveWaitState.Context()) + primitiveWaitState.context = context + // `swift_task_donateThreadToGlobalExecutorUntil` can return when the condition met + // or when no runnable job is available. In the latter case, we need to wait again. + _task_donateThreadToGlobalExecutorUntil({ contextRawPtr in + let context = contextRawPtr.assumingMemoryBound(to: PrimitiveWaitState.Context.self) + defer { context.pointee.runOnce = true } + return context.pointee.runOnce || context.pointee.isCancelled + }, context) + context.deinitialize(count: 1) + context.deallocate() + primitiveWaitState.context = nil + } + + func cancelPrimitiveWait() { + if let context = primitiveWaitState.context { + context.pointee.isCancelled = true + } + } +} +#else +private extension XCTWaiter { + struct PrimitiveWaitState {} + func primitiveWait(using runLoop: RunLoop, duration timeout: TimeInterval) { // The contract for `primitiveWait(for:)` explicitly allows waiting for a shorter period than requested // by the `timeout` argument. Only run for a short time in case `cancelPrimitiveWait()` was called and @@ -376,6 +419,7 @@ private extension XCTWaiter { #endif } } +#endif extension XCTWaiter: Equatable { public static func == (lhs: XCTWaiter, rhs: XCTWaiter) -> Bool { diff --git a/Sources/XCTest/Public/XCAbstractTest.swift b/Sources/XCTest/Public/XCAbstractTest.swift index cf37cba0d..4992b8130 100644 --- a/Sources/XCTest/Public/XCAbstractTest.swift +++ b/Sources/XCTest/Public/XCAbstractTest.swift @@ -36,6 +36,8 @@ 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? + /// The method through which tests are executed. Must be overridden by /// subclasses. open func perform(_ run: XCTestRun) { diff --git a/Sources/XCTest/Public/XCTestCase.swift b/Sources/XCTest/Public/XCTestCase.swift index 0a3ebb021..5d081d44e 100644 --- a/Sources/XCTest/Public/XCTestCase.swift +++ b/Sources/XCTest/Public/XCTestCase.swift @@ -36,6 +36,9 @@ open class XCTestCase: XCTest { private var skip: XCTSkip? + private var invokeTestTask: Task? + fileprivate var testClosureTask: Task? + /// The name of the test case, consisting of its class name and the method /// name it will run. open override var name: String { @@ -92,6 +95,7 @@ open class XCTestCase: XCTest { } open override func perform(_ run: XCTestRun) { + self.performTask = Task { guard let testRun = run as? XCTestCaseRun else { fatalError("Wrong XCTestRun class.") } @@ -100,6 +104,10 @@ open class XCTestCase: XCTest { testRun.start() invokeTest() + if let invokeTestTask = invokeTestTask { + _ = await invokeTestTask.value + } + #if !os(WASI) let allExpectations = XCTWaiter.subsystemQueue.sync { _allExpectations } failIfExpectationsNotWaitedFor(allExpectations) @@ -107,6 +115,7 @@ open class XCTestCase: XCTest { testRun.stop() XCTCurrentTestCase = nil + } } /// The designated initializer for SwiftXCTest's XCTestCase. @@ -121,12 +130,16 @@ open class XCTestCase: XCTest { /// Invoking a test performs its setUp, invocation, and tearDown. In /// general this should not be called directly. open func invokeTest() { - performSetUpSequence() + self.invokeTestTask = Task { + await performSetUpSequence() do { if skip == nil { try testClosure(self) } + if let task = testClosureTask { + _ = try await task.value + } } catch { if error.xct_shouldRecordAsTestFailure { recordFailure(for: error) @@ -145,7 +158,8 @@ open class XCTestCase: XCTest { testRun?.recordSkip(description: skip.summary, sourceLocation: skip.sourceLocation) } - performTearDownSequence() + await performTearDownSequence() + } } /// Records a failure in the execution of the test and is used by all test @@ -217,7 +231,9 @@ open class XCTestCase: XCTest { teardownBlocksState.appendAsync(block) } - private func performSetUpSequence() { + // FIXME(katei): `private` is removed from the following function because + // Swift 5.7 incorrectly requires the hidden symbol from subclass definitions. + func performSetUpSequence() async { func handleErrorDuringSetUp(_ error: Error) { if error.xct_shouldRecordAsTestFailure { recordFailure(for: error) @@ -233,13 +249,11 @@ open class XCTestCase: XCTest { } do { - #if !os(WASI) if #available(macOS 12.0, *) { - try awaitUsingExpectation { + try await awaitUsingExpectation { try await self.setUp() } } - #endif } catch { handleErrorDuringSetUp(error) } @@ -250,29 +264,37 @@ open class XCTestCase: XCTest { handleErrorDuringSetUp(error) } - setUp() + // Workaround: Compiler incorrectly infers the sync 'tearDown' as async one in async context. + func doSetUp() { + setUp() + } + doSetUp() } - private func performTearDownSequence() { + private func performTearDownSequence() async { func handleErrorDuringTearDown(_ error: Error) { if error.xct_shouldRecordAsTestFailure { recordFailure(for: error) } } - func runTeardownBlocks() { + func runTeardownBlocks() async { for block in self.teardownBlocksState.finalize().reversed() { do { - try block() + try await block() } catch { handleErrorDuringTearDown(error) } } } - runTeardownBlocks() + await runTeardownBlocks() - tearDown() + // Workaround: Compiler incorrectly infers the sync 'tearDown' as async one in async context. + func doTearDown() { + tearDown() + } + doTearDown() do { try tearDownWithError() @@ -281,13 +303,7 @@ open class XCTestCase: XCTest { } do { - #if !os(WASI) - if #available(macOS 12.0, *) { - try awaitUsingExpectation { - try await self.tearDown() - } - } - #endif + try await self.tearDown() } catch { handleErrorDuringTearDown(error) } @@ -331,7 +347,6 @@ private func test(_ testFunc: @escaping (T) -> () throws -> Void) } } -#if !os(WASI) @available(macOS 12.0, *) public func asyncTest( _ testClosureGenerator: @escaping (T) -> () async throws -> Void @@ -339,7 +354,10 @@ public func asyncTest( return { (testType: T) in let testClosure = testClosureGenerator(testType) return { - try awaitUsingExpectation(testClosure) + assert(testType.testClosureTask == nil, "Async test case \(testType) cannot be run more than once") + testType.testClosureTask = Task { + try await awaitUsingExpectation(testClosure) + } } } } @@ -347,7 +365,11 @@ public func asyncTest( @available(macOS 12.0, *) func awaitUsingExpectation( _ closure: @escaping () async throws -> Void -) throws -> Void { +) async throws -> Void { +#if os(WASI) + try await closure() + return +#else let expectation = XCTestExpectation(description: "async test completion") let thrownErrorWrapper = ThrownErrorWrapper() @@ -366,6 +388,7 @@ func awaitUsingExpectation( if let error = thrownErrorWrapper.error { throw error } +#endif } private final class ThrownErrorWrapper: @unchecked Sendable { @@ -381,7 +404,6 @@ private final class ThrownErrorWrapper: @unchecked Sendable { } } } -#endif // This time interval is set to a very large value due to their being no real native timeout functionality within corelibs-xctest. diff --git a/Sources/XCTest/Public/XCTestMain.swift b/Sources/XCTest/Public/XCTestMain.swift index 945753320..06d46d177 100644 --- a/Sources/XCTest/Public/XCTestMain.swift +++ b/Sources/XCTest/Public/XCTestMain.swift @@ -59,19 +59,22 @@ /// /// - Parameter testCases: An array of test cases run, each produced by a call to the `testCase` function /// - seealso: `testCase` -public func XCTMain(_ testCases: [XCTestCaseEntry]) -> Never { +public func XCTMain(_ testCases: [XCTestCaseEntry]) { XCTMain(testCases, arguments: CommandLine.arguments) } -public func XCTMain(_ testCases: [XCTestCaseEntry], arguments: [String]) -> Never { +public func XCTMain(_ testCases: [XCTestCaseEntry], arguments: [String]) { XCTMain(testCases, arguments: arguments, observers: [PrintObserver()]) } +@_silgen_name("XCTMainRunLoopMain") +func XCTMainRunLoopMain() + public func XCTMain( _ testCases: [XCTestCaseEntry], arguments: [String], observers: [XCTestObservation] -) -> Never { +) { #if !os(WASI) let testBundle = Bundle.main #endif @@ -151,10 +154,22 @@ public func XCTMain( observationCenter.testBundleWillStart(testBundle) #endif rootTestSuite.run() + @Sendable func afterRun() -> Never { #if !os(WASI) observationCenter.testBundleDidFinish(testBundle) #endif exit(rootTestSuite.testRun!.totalFailureCount == 0 ? EXIT_SUCCESS : EXIT_FAILURE) + } + + if let performTask = rootTestSuite.performTask { + Task { + _ = await performTask.value + afterRun() + } + XCTMainRunLoopMain() + } else { + afterRun() + } } } diff --git a/Sources/XCTest/Public/XCTestSuite.swift b/Sources/XCTest/Public/XCTestSuite.swift index 177dd1cb7..870b87823 100644 --- a/Sources/XCTest/Public/XCTestSuite.swift +++ b/Sources/XCTest/Public/XCTestSuite.swift @@ -45,12 +45,18 @@ open class XCTestSuite: XCTest { run.start() setUp() + self.performTask = Task { for test in tests { test.run() + if let childPerformTask = test.performTask { + _ = await childPerformTask.value + } testRun.addTestRun(test.testRun!) } - tearDown() + func doTearDown() { tearDown() } + doTearDown() run.stop() + } } public init(name: String) { From 77f7ff4b616579b0e25a84502cf4ad30d10a8963 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 20 Aug 2022 07:39:22 +0000 Subject: [PATCH 25/26] [Wasm] LLVM 14 requires C as a project lang https://github.com/llvm/llvm-project/issues/53950 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 27c6f261f..fe66d5a8e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.15.1) list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) -project(XCTest LANGUAGES Swift CXX) +project(XCTest LANGUAGES Swift C CXX) option(BUILD_SHARED_LIBS "Build shared libraries" ON) option(USE_FOUNDATION_FRAMEWORK "Use Foundation.framework on Darwin" NO) From 2924b4b761cf749865243d4b4ca3321f0c93e8c4 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 2 Jan 2023 04:37:54 +0000 Subject: [PATCH 26/26] setup pull for 5.8 releae branch --- .github/pull.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/pull.yml b/.github/pull.yml index 833c12e2e..0f8021460 100644 --- a/.github/pull.yml +++ b/.github/pull.yml @@ -42,4 +42,11 @@ rules: upstream: release/5.7 mergeMethod: merge + - base: release/5.8 + upstream: apple:release/5.8 + mergeMethod: hardreset + - base: swiftwasm-release/5.8 + upstream: release/5.8 + mergeMethod: merge + label: ":arrow_heading_down: Upstream Tracking"