diff --git a/Package.resolved b/Package.resolved index c13f1c30..ffd7382e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "65c4762fc267e15331f3aed90105265c610242906b39cfe4fb1bd366ae5ddcca", + "originHash" : "7ad3e8511a63915009dd025ffe4d701d034989c2468ac56c7d063a0c962dca4b", "pins" : [ { "identity" : "darwinprivateframeworks", diff --git a/Package.swift b/Package.swift index 09d48338..285d7fe4 100644 --- a/Package.swift +++ b/Package.swift @@ -161,7 +161,7 @@ let openGraphShimsTarget = Target.target( // MARK: - Test Targets -let openGraphTestTarget = Target.testTarget( +let openGraphTestsTarget = Target.testTarget( name: "OpenGraphTests", dependencies: [ "OpenGraph", @@ -170,7 +170,7 @@ let openGraphTestTarget = Target.testTarget( cSettings: sharedCSettings, swiftSettings: sharedSwiftSettings ) -let openGraphSPITestTarget = Target.testTarget( +let openGraphCxxTestsTarget = Target.testTarget( name: "OpenGraphCxxTests", dependencies: [ "OpenGraphCxx", @@ -178,7 +178,7 @@ let openGraphSPITestTarget = Target.testTarget( exclude: ["README.md"], swiftSettings: sharedSwiftSettings + [.interoperabilityMode(.Cxx)] ) -let openGraphShimsTestTarget = Target.testTarget( +let openGraphShimsTestsTarget = Target.testTarget( name: "OpenGraphShimsTests", dependencies: [ "OpenGraphShims", @@ -187,7 +187,7 @@ let openGraphShimsTestTarget = Target.testTarget( cSettings: sharedCSettings, swiftSettings: sharedSwiftSettings ) -let openGraphCompatibilityTestTarget = Target.testTarget( +let openGraphCompatibilityTestsTarget = Target.testTarget( name: "OpenGraphCompatibilityTests", dependencies: [ .product(name: "Numerics", package: "swift-numerics"), @@ -212,11 +212,11 @@ let package = Package( openGraphTarget, openGraphSPITarget, openGraphShimsTarget, - - openGraphTestTarget, - openGraphSPITestTarget, - openGraphShimsTestTarget, - openGraphCompatibilityTestTarget, + + openGraphTestsTarget, + openGraphCxxTestsTarget, + openGraphShimsTestsTarget, + openGraphCompatibilityTestsTarget, ], cxxLanguageStandard: .cxx20 ) @@ -268,9 +268,9 @@ if attributeGraphCondition { let compatibilityTestCondition = envEnable("OPENGRAPH_COMPATIBILITY_TEST") if compatibilityTestCondition && attributeGraphCondition { - openGraphCompatibilityTestTarget.addCompatibilitySettings() + openGraphCompatibilityTestsTarget.addCompatibilitySettings() } else { - openGraphCompatibilityTestTarget.dependencies.append("OpenGraph") + openGraphCompatibilityTestsTarget.dependencies.append("OpenGraph") } extension [Platform] { diff --git a/Tests/OpenGraphCompatibilityTests/Attribute/Attribute/AnyAttributeCompatibilityTests.swift b/Tests/OpenGraphCompatibilityTests/Attribute/Attribute/AnyAttributeCompatibilityTests.swift index 757dcc7d..c11e80b4 100644 --- a/Tests/OpenGraphCompatibilityTests/Attribute/Attribute/AnyAttributeCompatibilityTests.swift +++ b/Tests/OpenGraphCompatibilityTests/Attribute/Attribute/AnyAttributeCompatibilityTests.swift @@ -8,9 +8,9 @@ import Testing // swift-testing framework will crash here on Linux // Report to upstream for investigation when we bump to 5.10 #if canImport(Darwin) -//@Suite(.disabled(if: !compatibilityTestEnabled, "Attribute is not implemented")) -@Suite(.disabled("Skip flaky CI tests after #154 temporary, See more info on #157")) -final class AnyAttributeCompatibilityTests: AttributeTestBase { +//@Suite(.disabled(if: !compatibilityTestEnabled, "Attribute is not implemented"), .graphScope) +@Suite(.disabled("Skip flaky CI tests after #154 temporary, See more info on #157"), .graphScope) +struct AnyAttributeCompatibilityTests { @Test func constantValue() throws { let attributeNil = AnyAttribute.nil diff --git a/Tests/OpenGraphCompatibilityTests/Attribute/Attribute/AttributeCompatibilityTests.swift b/Tests/OpenGraphCompatibilityTests/Attribute/Attribute/AttributeCompatibilityTests.swift index 86eed2f9..a1b8b486 100644 --- a/Tests/OpenGraphCompatibilityTests/Attribute/Attribute/AttributeCompatibilityTests.swift +++ b/Tests/OpenGraphCompatibilityTests/Attribute/Attribute/AttributeCompatibilityTests.swift @@ -5,8 +5,9 @@ import Testing #if canImport(Darwin) -@Suite(.disabled(if: !compatibilityTestEnabled, "Attribute is not implemented")) -final class AttributeCompatibilityTests: AttributeTestBase { +@MainActor +@Suite(.disabled(if: !compatibilityTestEnabled, "Attribute is not implemented"), .graphScope) +struct AttributeCompatibilityTests { @Test func initWithValue() { let intAttribute = Attribute(value: 0) diff --git a/Tests/OpenGraphCompatibilityTests/Attribute/Attribute/ExternalCompatibilityTests.swift b/Tests/OpenGraphCompatibilityTests/Attribute/Attribute/ExternalCompatibilityTests.swift index f7d28593..96425283 100644 --- a/Tests/OpenGraphCompatibilityTests/Attribute/Attribute/ExternalCompatibilityTests.swift +++ b/Tests/OpenGraphCompatibilityTests/Attribute/Attribute/ExternalCompatibilityTests.swift @@ -8,8 +8,9 @@ import Testing // swift-testing framework will crash here on Linux // Report to upstream for investigation when we bump to 5.10 #if canImport(Darwin) -@Suite(.disabled(if: !compatibilityTestEnabled, "Attribute is not implemented")) -final class ExternalCompatibilityTests: AttributeTestBase { +@MainActor +@Suite(.disabled(if: !compatibilityTestEnabled, "Attribute is not implemented"), .graphScope) +struct ExternalCompatibilityTests { @Test func example() throws { let type = External.self diff --git a/Tests/OpenGraphCompatibilityTests/Attribute/Attribute/FocusCompatibilityTests.swift b/Tests/OpenGraphCompatibilityTests/Attribute/Attribute/FocusCompatibilityTests.swift index 709f457c..c7d565a7 100644 --- a/Tests/OpenGraphCompatibilityTests/Attribute/Attribute/FocusCompatibilityTests.swift +++ b/Tests/OpenGraphCompatibilityTests/Attribute/Attribute/FocusCompatibilityTests.swift @@ -5,8 +5,9 @@ import Testing #if canImport(Darwin) -@Suite(.disabled(if: !compatibilityTestEnabled, "Attribute is not implemented")) -final class FocusCompatibilityTests: AttributeTestBase { +@MainActor +@Suite(.disabled(if: !compatibilityTestEnabled, "Attribute is not implemented"), .graphScope) +struct FocusCompatibilityTests { struct Demo { var a: Int var b: Double diff --git a/Tests/OpenGraphCompatibilityTests/Attribute/AttributeTestBase.swift b/Tests/OpenGraphCompatibilityTests/Attribute/AttributeTestBase.swift deleted file mode 100644 index 95294511..00000000 --- a/Tests/OpenGraphCompatibilityTests/Attribute/AttributeTestBase.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// AttributeTestBase.swift -// -// -// - -import Testing - -/// Base class for Attribute Related test case -class AttributeTestBase { - private static let sharedGraph = Graph() - private var graph: Graph - private var subgraph: Subgraph - - init() { - graph = Graph(shared: Self.sharedGraph) - subgraph = Subgraph(graph: graph) - Subgraph.current = subgraph - } - - deinit { - Subgraph.current = nil - } -} diff --git a/Tests/OpenGraphCompatibilityTests/Attribute/AttributeTestHelper.swift b/Tests/OpenGraphCompatibilityTests/Attribute/AttributeTestHelper.swift deleted file mode 100644 index 4b72d16d..00000000 --- a/Tests/OpenGraphCompatibilityTests/Attribute/AttributeTestHelper.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// AttributeTestHelper.swift -// -// -// - -struct Tuple { - var first: A - var second: B -} - -struct Triple { - var first: A - var second: B - var third: C -} diff --git a/Tests/OpenGraphCompatibilityTests/Attribute/Indirect/IndirectAttributeCompatibilityTests.swift b/Tests/OpenGraphCompatibilityTests/Attribute/Indirect/IndirectAttributeCompatibilityTests.swift index 6017ed34..0435cbee 100644 --- a/Tests/OpenGraphCompatibilityTests/Attribute/Indirect/IndirectAttributeCompatibilityTests.swift +++ b/Tests/OpenGraphCompatibilityTests/Attribute/Indirect/IndirectAttributeCompatibilityTests.swift @@ -5,8 +5,9 @@ import Testing #if canImport(Darwin) -@Suite(.disabled(if: !compatibilityTestEnabled, "IndirectAttribute is not implemented")) -final class IndirectAttributeCompatibilityTests: AttributeTestBase { +@MainActor +@Suite(.disabled(if: !compatibilityTestEnabled, "IndirectAttribute is not implemented"), .graphScope) +struct IndirectAttributeCompatibilityTests { @Test func basic() { let source = Attribute(value: 0) diff --git a/Tests/OpenGraphCompatibilityTests/Attribute/Optional/AnyOptionalAttributeCompatibilityTests.swift b/Tests/OpenGraphCompatibilityTests/Attribute/Optional/AnyOptionalAttributeCompatibilityTests.swift index 1ae19d91..1c7d6e00 100644 --- a/Tests/OpenGraphCompatibilityTests/Attribute/Optional/AnyOptionalAttributeCompatibilityTests.swift +++ b/Tests/OpenGraphCompatibilityTests/Attribute/Optional/AnyOptionalAttributeCompatibilityTests.swift @@ -5,8 +5,9 @@ import Testing #if canImport(Darwin) -@Suite(.disabled(if: !compatibilityTestEnabled, "AnyOptionalAttribute is not implemented")) -final class AnyOptionalAttributeCompatibilityTests: AttributeTestBase { +@MainActor +@Suite(.disabled(if: !compatibilityTestEnabled, "AnyOptionalAttribute is not implemented"), .graphScope) +struct AnyOptionalAttributeCompatibilityTests { @Test func basicInit() { let o1 = AnyOptionalAttribute() diff --git a/Tests/OpenGraphCompatibilityTests/Attribute/Optional/OptionalAttributeCompatibilityTests.swift b/Tests/OpenGraphCompatibilityTests/Attribute/Optional/OptionalAttributeCompatibilityTests.swift index 317ca5ef..236b596a 100644 --- a/Tests/OpenGraphCompatibilityTests/Attribute/Optional/OptionalAttributeCompatibilityTests.swift +++ b/Tests/OpenGraphCompatibilityTests/Attribute/Optional/OptionalAttributeCompatibilityTests.swift @@ -5,8 +5,9 @@ import Testing #if canImport(Darwin) +@MainActor @Suite(.disabled(if: !compatibilityTestEnabled, "OptionalAttribute is not implemented")) -final class OptionalAttributeCompatibilityTests: AttributeTestBase { +struct OptionalAttributeCompatibilityTests { @Test func basicInit() { let ao1 = AnyOptionalAttribute() diff --git a/Tests/OpenGraphCompatibilityTests/Attribute/Rule/MapCompatibilityTests.swift b/Tests/OpenGraphCompatibilityTests/Attribute/Rule/MapCompatibilityTests.swift index 4ee6a7d7..bfb74af4 100644 --- a/Tests/OpenGraphCompatibilityTests/Attribute/Rule/MapCompatibilityTests.swift +++ b/Tests/OpenGraphCompatibilityTests/Attribute/Rule/MapCompatibilityTests.swift @@ -4,8 +4,9 @@ import Testing -@Suite(.disabled(if: !compatibilityTestEnabled)) -final class MapCompatibilityTests: AttributeTestBase { +@MainActor +@Suite(.disabled(if: !compatibilityTestEnabled), .graphScope) +struct MapCompatibilityTests { @Test func description() throws { let map = Map(.init(value: 2)) { $0.description } diff --git a/Tests/OpenGraphCompatibilityTests/Attribute/Weak/AnyWeakAttributeCompatibilityTests.swift b/Tests/OpenGraphCompatibilityTests/Attribute/Weak/AnyWeakAttributeCompatibilityTests.swift index b36c5bbb..f9b08a94 100644 --- a/Tests/OpenGraphCompatibilityTests/Attribute/Weak/AnyWeakAttributeCompatibilityTests.swift +++ b/Tests/OpenGraphCompatibilityTests/Attribute/Weak/AnyWeakAttributeCompatibilityTests.swift @@ -5,9 +5,9 @@ import Testing #if canImport(Darwin) - -@Suite(.enabled(if: compatibilityTestEnabled)) -final class AnyWeakAttributeCompatibilityTests: AttributeTestBase { +@MainActor +@Suite(.enabled(if: compatibilityTestEnabled), .graphScope) +struct AnyWeakAttributeCompatibilityTests { @Test func basic() { let w1 = AnyWeakAttribute(nil) diff --git a/Tests/OpenGraphCompatibilityTests/Attribute/Weak/WeakAttributeCompatibilityTests.swift b/Tests/OpenGraphCompatibilityTests/Attribute/Weak/WeakAttributeCompatibilityTests.swift index 40aa33f2..4f944134 100644 --- a/Tests/OpenGraphCompatibilityTests/Attribute/Weak/WeakAttributeCompatibilityTests.swift +++ b/Tests/OpenGraphCompatibilityTests/Attribute/Weak/WeakAttributeCompatibilityTests.swift @@ -5,8 +5,9 @@ import Testing #if canImport(Darwin) -@Suite(.enabled(if: compatibilityTestEnabled)) -final class WeakAttributeCompatibilityTests: AttributeTestBase { +@MainActor +@Suite(.enabled(if: compatibilityTestEnabled), .graphScope) +struct WeakAttributeCompatibilityTests { @Test func initTest() { let _ = WeakAttribute() diff --git a/Tests/OpenGraphCompatibilityTests/Graph/SubgraphCompatibilityTests.swift b/Tests/OpenGraphCompatibilityTests/Graph/SubgraphCompatibilityTests.swift index 17c47fb9..35b1343d 100644 --- a/Tests/OpenGraphCompatibilityTests/Graph/SubgraphCompatibilityTests.swift +++ b/Tests/OpenGraphCompatibilityTests/Graph/SubgraphCompatibilityTests.swift @@ -4,7 +4,8 @@ import Testing -@Suite(.enabled(if: compatibilityTestEnabled)) +@MainActor +@Suite(.enabled(if: compatibilityTestEnabled), .graphScope) struct SubgraphCompatibilityTests { @Test func shouldRecordTree() { diff --git a/Tests/OpenGraphCompatibilityTests/OpenGraphTestsSupport b/Tests/OpenGraphCompatibilityTests/OpenGraphTestsSupport new file mode 120000 index 00000000..d79343a8 --- /dev/null +++ b/Tests/OpenGraphCompatibilityTests/OpenGraphTestsSupport @@ -0,0 +1 @@ +../OpenGraphTestsSupport/ \ No newline at end of file diff --git a/Tests/OpenGraphShimsTests/Export.swift b/Tests/OpenGraphShimsTests/Export.swift new file mode 100644 index 00000000..95a0d190 --- /dev/null +++ b/Tests/OpenGraphShimsTests/Export.swift @@ -0,0 +1,5 @@ +// +// Export.swift +// OpenGraphShimsTests + +@_exported import OpenGraphShims diff --git a/Tests/OpenGraphShimsTests/MetadataDebugTests.swift b/Tests/OpenGraphShimsTests/MetadataDebugTests.swift index 6d8b0678..ba0a6a43 100644 --- a/Tests/OpenGraphShimsTests/MetadataDebugTests.swift +++ b/Tests/OpenGraphShimsTests/MetadataDebugTests.swift @@ -1,6 +1,6 @@ // // MetadataDebugTests.swift -// OpenGraphTests +// OpenGraphShimsTests @_spi(Debug) import OpenGraphShims import Testing diff --git a/Tests/OpenGraphShimsTests/OpenGraphTestsSupport b/Tests/OpenGraphShimsTests/OpenGraphTestsSupport new file mode 120000 index 00000000..d79343a8 --- /dev/null +++ b/Tests/OpenGraphShimsTests/OpenGraphTestsSupport @@ -0,0 +1 @@ +../OpenGraphTestsSupport/ \ No newline at end of file diff --git a/Tests/OpenGraphTestsSupport/AsyncSemaphore.swift b/Tests/OpenGraphTestsSupport/AsyncSemaphore.swift new file mode 100644 index 00000000..8ab5bdc2 --- /dev/null +++ b/Tests/OpenGraphTestsSupport/AsyncSemaphore.swift @@ -0,0 +1,253 @@ +// Copyright (C) 2022 Gwendal Roué +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import Foundation + +/// An object that controls access to a resource across multiple execution +/// contexts through use of a traditional counting semaphore. +/// +/// You increment a semaphore count by calling the ``signal()`` method, and +/// decrement a semaphore count by calling ``wait()`` or one of its variants. +/// +/// ## Topics +/// +/// ### Creating a Semaphore +/// +/// - ``init(value:)`` +/// +/// ### Signaling the Semaphore +/// +/// - ``signal()`` +/// +/// ### Waiting for the Semaphore +/// +/// - ``wait()`` +/// - ``waitUnlessCancelled()`` +public final class AsyncSemaphore: @unchecked Sendable { + /// `Suspension` is the state of a task waiting for a signal. + /// + /// It is a class because instance identity helps `waitUnlessCancelled()` + /// deal with both early and late cancellation. + /// + /// We make it @unchecked Sendable in order to prevent compiler warnings: + /// instances are always protected by the semaphore's lock. + private class Suspension: @unchecked Sendable { + enum State { + /// Initial state. Next is suspendedUnlessCancelled, or cancelled. + case pending + + /// Waiting for a signal, with support for cancellation. + case suspendedUnlessCancelled(UnsafeContinuation) + + /// Waiting for a signal, with no support for cancellation. + case suspended(UnsafeContinuation) + + /// Cancelled before we have started waiting. + case cancelled + } + + var state: State + + init(state: State) { + self.state = state + } + } + + // MARK: - Internal State + + /// The semaphore value. + private var value: Int + + /// As many elements as there are suspended tasks waiting for a signal. + private var suspensions: [Suspension] = [] + + /// The lock that protects `value` and `suspensions`. + /// + /// It is recursive in order to handle cancellation (see the implementation + /// of ``waitUnlessCancelled()``). + private let _lock = NSRecursiveLock() + + // MARK: - Creating a Semaphore + + /// Creates a semaphore. + /// + /// - parameter value: The starting value for the semaphore. Do not pass a + /// value less than zero. + public init(value: Int) { + precondition(value >= 0, "AsyncSemaphore requires a value equal or greater than zero") + self.value = value + } + + deinit { + precondition(suspensions.isEmpty, "AsyncSemaphore is deallocated while some task(s) are suspended waiting for a signal.") + } + + // MARK: - Locking + + // Let's hide the locking primitive in order to avoid a compiler warning: + // + // > Instance method 'lock' is unavailable from asynchronous contexts; + // > Use async-safe scoped locking instead; this is an error in Swift 6. + // + // We're not sweeping bad stuff under the rug. We really need to protect + // our inner state (`value` and `suspension`) across the calls to + // `withUnsafeContinuation`. Unfortunately, this method introduces a + // suspension point. So we need a lock. + private func lock() { _lock.lock() } + private func unlock() { _lock.unlock() } + + // MARK: - Waiting for the Semaphore + + /// Waits for, or decrements, a semaphore. + /// + /// Decrement the counting semaphore. If the resulting value is less than + /// zero, this function suspends the current task until a signal occurs, + /// without blocking the underlying thread. Otherwise, no suspension happens. + public func wait() async { + lock() + + value -= 1 + if value >= 0 { + unlock() + return + } + + await withUnsafeContinuation { continuation in + // Register the continuation that `signal` will resume. + let suspension = Suspension(state: .suspended(continuation)) + suspensions.insert(suspension, at: 0) // FIFO + unlock() + } + } + + /// Waits for, or decrements, a semaphore, with support for cancellation. + /// + /// Decrement the counting semaphore. If the resulting value is less than + /// zero, this function suspends the current task until a signal occurs, + /// without blocking the underlying thread. Otherwise, no suspension happens. + /// + /// If the task is canceled before a signal occurs, this function + /// throws `CancellationError`. + public func waitUnlessCancelled() async throws { + lock() + + value -= 1 + if value >= 0 { + defer { unlock() } + + do { + // All code paths check for cancellation + try Task.checkCancellation() + } catch { + // Cancellation is like a signal: we don't really "consume" + // the semaphore, and restore the value. + value += 1 + throw error + } + + return + } + + // Get ready for being suspended waiting for a continuation, or for + // early cancellation. + let suspension = Suspension(state: .pending) + + try await withTaskCancellationHandler { + try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in + if case .cancelled = suspension.state { + // Early cancellation: waitUnlessCancelled() is called from + // a cancelled task, and the `onCancel` closure below + // has marked the suspension as cancelled. + // Resume with a CancellationError. + unlock() + continuation.resume(throwing: CancellationError()) + } else { + // Current task is not cancelled: register the continuation + // that `signal` will resume. + suspension.state = .suspendedUnlessCancelled(continuation) + suspensions.insert(suspension, at: 0) // FIFO + unlock() + } + } + } onCancel: { + // withTaskCancellationHandler may immediately call this block (if + // the current task is cancelled), or call it later (if the task is + // cancelled later). In the first case, we're still holding the lock, + // waiting for the continuation. In the second case, we do not hold + // the lock. Being able to handle both situations is the reason why + // we use a recursive lock. + lock() + + // We're no longer waiting for a signal + value += 1 + if let index = suspensions.firstIndex(where: { $0 === suspension }) { + suspensions.remove(at: index) + } + + if case let .suspendedUnlessCancelled(continuation) = suspension.state { + // Late cancellation: the task is cancelled while waiting + // from the semaphore. Resume with a CancellationError. + unlock() + continuation.resume(throwing: CancellationError()) + } else { + // Early cancellation: waitUnlessCancelled() is called from + // a cancelled task. + // + // The next step is the `withTaskCancellationHandler` + // operation closure right above. + suspension.state = .cancelled + unlock() + } + } + } + + // MARK: - Signaling the Semaphore + + /// Signals (increments) a semaphore. + /// + /// Increment the counting semaphore. If the previous value was less than + /// zero, this function resumes a task currently suspended in ``wait()`` + /// or ``waitUnlessCancelled()``. + /// + /// - returns: This function returns true if a suspended task is + /// resumed. Otherwise, the result is false, meaning that no task was + /// waiting for the semaphore. + @discardableResult + public func signal() -> Bool { + lock() + + value += 1 + + switch suspensions.popLast()?.state { // FIFO + case let .suspendedUnlessCancelled(continuation): + unlock() + continuation.resume() + return true + case let .suspended(continuation): + unlock() + continuation.resume() + return true + default: + unlock() + return false + } + } +} diff --git a/Tests/OpenGraphTestsSupport/DataHelper.swift b/Tests/OpenGraphTestsSupport/DataHelper.swift new file mode 100644 index 00000000..ce2bc940 --- /dev/null +++ b/Tests/OpenGraphTestsSupport/DataHelper.swift @@ -0,0 +1,25 @@ +// +// DataHelper.swift +// OpenGraphTestsSupport + +public struct Tuple { + public var first: A + public var second: B + + public init(first: A, second: B) { + self.first = first + self.second = second + } +} + +public struct Triple { + public var first: A + public var second: B + public var third: C + + public init(first: A, second: B, third: C) { + self.first = first + self.second = second + self.third = third + } +} diff --git a/Tests/OpenGraphTestsSupport/GraphEnvironmentTrait.swift b/Tests/OpenGraphTestsSupport/GraphEnvironmentTrait.swift new file mode 100644 index 00000000..2b61892d --- /dev/null +++ b/Tests/OpenGraphTestsSupport/GraphEnvironmentTrait.swift @@ -0,0 +1,33 @@ +// +// GraphEnvironmentTrait.swift +// OpenGraphTestsSupport + +public import Testing + +public struct GraphEnvironmentTrait: TestTrait, TestScoping, SuiteTrait { + private static let sharedGraph = Graph() + private static let semaphore = AsyncSemaphore(value: 1) + + @MainActor + public func provideScope(for test: Test, testCase: Test.Case?, performing function: @Sendable () async throws -> Void) async throws { + await Self.semaphore.wait() + defer { Self.semaphore.signal() } + let graph = Graph(shared: Self.sharedGraph) + let subgraph = Subgraph(graph: graph) + let oldSubgraph = Subgraph.current + + Subgraph.current = subgraph + try await function() + Subgraph.current = oldSubgraph + } + + public var isRecursive: Bool { + true + } +} + +extension Trait where Self == GraphEnvironmentTrait { + public static var graphScope: Self { + GraphEnvironmentTrait() + } +}