diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 936096b32..80c39967f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,32 @@ jobs: - name: Run tests run: make test-${{ matrix.variation }} + wasm: + name: SwiftWasm + runs-on: ubuntu-latest + strategy: + matrix: + include: + - toolchain: swift-DEVELOPMENT-SNAPSHOT-2024-07-08-a + swift-sdk: swift-wasm-DEVELOPMENT-SNAPSHOT-2024-07-09-a + steps: + - uses: actions/checkout@v4 + - uses: bytecodealliance/actions/wasmtime/setup@v1 + - name: Install Swift and Swift SDK for WebAssembly + run: | + PREFIX=/opt/swift + SWIFT_TOOLCHAIN_TAG="${{ matrix.toolchain }}" + SWIFT_SDK_TAG="${{ matrix.swift-sdk }}" + set -ex + curl -f -o /tmp/swift.tar.gz "https://download.swift.org/development/ubuntu2204/$SWIFT_TOOLCHAIN_TAG/$SWIFT_TOOLCHAIN_TAG-ubuntu22.04.tar.gz" + sudo mkdir -p $PREFIX; sudo tar -xzf /tmp/swift.tar.gz -C $PREFIX --strip-component 1 + $PREFIX/usr/bin/swift experimental-sdk install "https://github.com/swiftwasm/swift/releases/download/$SWIFT_SDK_TAG/$SWIFT_SDK_TAG-wasm32-unknown-wasi.artifactbundle.zip" + echo "$PREFIX/usr/bin" >> $GITHUB_PATH + - name: Build tests + run: swift build --swift-sdk wasm32-unknown-wasi --build-tests -Xlinker -z -Xlinker stack-size=$((1024 * 1024)) + - name: Run tests + run: wasmtime --dir . .build/debug/swift-navigationPackageTests.wasm + windows: name: Windows strategy: diff --git a/Package.resolved b/Package.resolved index 6113fe9e7..3d8bceeeb 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,12 +1,13 @@ { + "originHash" : "f10d2f6e9f8c467a6984b87f3aba67d2119452577271115a38ed7d5e7ada2444", "pins" : [ { "identity" : "swift-case-paths", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "71344dd930fde41e8f3adafe260adcbb2fc2a3dc", - "version" : "1.5.4" + "revision" : "642e6aab8e03e5f992d9c83e38c5be98cfad5078", + "version" : "1.5.5" } }, { @@ -14,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "3d2dc41a01f9e49d84f0a3925fb858bed64f702d", - "version" : "1.1.2" + "revision" : "9bf03ff58ce34478e66aaee630e491823326fd06", + "version" : "1.1.3" } }, { @@ -41,14 +42,14 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-docc-plugin", "state" : { - "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", - "version" : "1.3.0" + "revision" : "2eb22993b3dfd0c0d32729b357c8dabb6cd44680", + "version" : "1.4.2" } }, { "identity" : "swift-docc-symbolkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-docc-symbolkit", + "location" : "https://github.com/swiftlang/swift-docc-symbolkit", "state" : { "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", "version" : "1.0.0" @@ -59,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-perception", "state" : { - "revision" : "1552c8f722ac256cc0b8daaf1a7073217d4fcdfb", - "version" : "1.3.4" + "revision" : "bc67aa8e461351c97282c2419153757a446ae1c9", + "version" : "1.3.5" } }, { @@ -68,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "06b5cdc432e93b60e3bdf53aff2857c6b312991a", - "version" : "600.0.0-prerelease-2024-07-30" + "revision" : "515f79b522918f83483068d99c68daeb5116342d", + "version" : "600.0.0-prerelease-2024-08-20" } }, { @@ -77,10 +78,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "357ca1e5dd31f613a1d43320870ebc219386a495", - "version" : "1.2.2" + "revision" : "96beb108a57f24c8476ae1f309239270772b2940", + "version" : "1.2.5" } } ], - "version" : 2 + "version" : 3 } diff --git a/Tests/SwiftUINavigationTests/ButtonStateTests.swift b/Tests/SwiftNavigationTests/ButtonStateTests.swift similarity index 87% rename from Tests/SwiftUINavigationTests/ButtonStateTests.swift rename to Tests/SwiftNavigationTests/ButtonStateTests.swift index d6ecc2bf1..88b7d252d 100644 --- a/Tests/SwiftUINavigationTests/ButtonStateTests.swift +++ b/Tests/SwiftNavigationTests/ButtonStateTests.swift @@ -1,7 +1,6 @@ -#if canImport(SwiftUI) && canImport(Testing) +#if canImport(Testing) import CustomDump - import SwiftUI - import SwiftUINavigation + import SwiftNavigation import Testing struct ButtonStateTests { @@ -31,4 +30,4 @@ } } } -#endif // canImport(SwiftUI) +#endif diff --git a/Tests/SwiftNavigationTests/IsolationTests.swift b/Tests/SwiftNavigationTests/IsolationTests.swift index fa5fa62f5..a045481aa 100644 --- a/Tests/SwiftNavigationTests/IsolationTests.swift +++ b/Tests/SwiftNavigationTests/IsolationTests.swift @@ -3,34 +3,38 @@ import XCTest class IsolationTests: XCTestCase { - @MainActor - func testIsolationOnMinActor() async { - let model = MainActorModel() - let expectation = expectation(description: "observation") - expectation.expectedFulfillmentCount = 2 - let token = SwiftNavigation.observe { - _ = model.count - MainActor.assertIsolated() - expectation.fulfill() + func testIsolationOnMainActor() async throws { + try await Task { @MainActor in + let model = MainActorModel() + var didObserve = false + let token = SwiftNavigation.observe { + _ = model.count + MainActor.assertIsolated() + didObserve = true + } + model.count += 1 + try await Task.sleep(nanoseconds: 300_000_000) + XCTAssertEqual(didObserve, true) + _ = token } - model.count += 1 - await fulfillment(of: [expectation], timeout: 1) - _ = token + .value } - @GlobalActorIsolated - func testIsolationOnGlobalActor() async { - let model = GlobalActorModel() - let expectation = expectation(description: "observation") - expectation.expectedFulfillmentCount = 2 - let token = SwiftNavigation.observe { - _ = model.count - GlobalActorIsolated.assertIsolated() - expectation.fulfill() + func testIsolationOnGlobalActor() async throws { + try await Task { @GlobalActorIsolated in + let model = GlobalActorModel() + var didObserve = false + let token = SwiftNavigation.observe { + _ = model.count + GlobalActorIsolated.assertIsolated() + didObserve = true + } + model.count += 1 + try await Task.sleep(nanoseconds: 300_000_000) + XCTAssertEqual(didObserve, true) + _ = token } - model.count += 1 - await fulfillment(of: [expectation], timeout: 1) - _ = token + .value } } diff --git a/Tests/SwiftNavigationTests/LifetimeTests.swift b/Tests/SwiftNavigationTests/LifetimeTests.swift index 6c68de463..20240dd32 100644 --- a/Tests/SwiftNavigationTests/LifetimeTests.swift +++ b/Tests/SwiftNavigationTests/LifetimeTests.swift @@ -3,27 +3,29 @@ import XCTest final class LifetimeTests: XCTestCase { - @MainActor func testObserveToken() async { - let model = Model() - var counts = [Int]() - var token: ObserveToken? - do { - token = SwiftNavigation.observe { - counts.append(model.count) + await Task { @MainActor in + let model = Model() + var counts = [Int]() + var token: ObserveToken? + do { + token = SwiftNavigation.observe { + counts.append(model.count) + } } - } - XCTAssertEqual(counts, [0]) - model.count += 1 - await Task.yield() - XCTAssertEqual(counts, [0, 1]) + XCTAssertEqual(counts, [0]) + model.count += 1 + await Task.yield() + XCTAssertEqual(counts, [0, 1]) - _ = token - token = nil + _ = token + token = nil - model.count += 1 - await Task.yield() - XCTAssertEqual(counts, [0, 1]) + model.count += 1 + await Task.yield() + XCTAssertEqual(counts, [0, 1]) + } + .value } } diff --git a/Tests/SwiftNavigationTests/ObserveTests.swift b/Tests/SwiftNavigationTests/ObserveTests.swift index d85f7bf24..afafe1731 100644 --- a/Tests/SwiftNavigationTests/ObserveTests.swift +++ b/Tests/SwiftNavigationTests/ObserveTests.swift @@ -3,26 +3,32 @@ import XCTest class ObserveTests: XCTestCase { #if swift(>=6) - @MainActor - func testIsolation() { - var count = 0 - let token = SwiftNavigation.observe { - count = 1 + func testIsolation() async { + await MainActor.run { + var count = 0 + let token = SwiftNavigation.observe { + count = 1 + } + XCTAssertEqual(count, 1) + _ = token } - XCTAssertEqual(count, 1) - _ = token } #endif - @MainActor - func testTokenStorage() { - var count = 0 - observe { - count += 1 - } - observe { - count += 1 + #if !os(WASI) + @MainActor + func testTokenStorage() async { + var count = 0 + var tokens: Set = [] + observe { + count += 1 + } + .store(in: &tokens) + observe { + count += 1 + } + .store(in: &tokens) + XCTAssertEqual(count, 2) } - XCTAssertEqual(count, 2) - } + #endif } diff --git a/Tests/SwiftUINavigationTests/TextStateTests.swift b/Tests/SwiftNavigationTests/TextStateTests.swift similarity index 93% rename from Tests/SwiftUINavigationTests/TextStateTests.swift rename to Tests/SwiftNavigationTests/TextStateTests.swift index 0f50477b8..3331f66ad 100644 --- a/Tests/SwiftUINavigationTests/TextStateTests.swift +++ b/Tests/SwiftNavigationTests/TextStateTests.swift @@ -1,9 +1,9 @@ -#if canImport(SwiftUI) - import CustomDump - import SwiftUINavigation - import XCTest +import CustomDump +import SwiftNavigation +import XCTest - final class TextStateTests: XCTestCase { +final class TextStateTests: XCTestCase { + #if !os(WASI) func testTextState() { var dump = "" customDump(TextState("Hello, world!"), to: &dump) @@ -72,5 +72,5 @@ """# ) } - } -#endif // canImport(SwiftUI) + #endif +} diff --git a/Tests/UIKitNavigationTests/UIBindableTests.swift b/Tests/SwiftNavigationTests/UIBindableTests.swift similarity index 92% rename from Tests/UIKitNavigationTests/UIBindableTests.swift rename to Tests/SwiftNavigationTests/UIBindableTests.swift index bf2c1f40d..f6f7a2f7d 100644 --- a/Tests/UIKitNavigationTests/UIBindableTests.swift +++ b/Tests/SwiftNavigationTests/UIBindableTests.swift @@ -9,8 +9,7 @@ private final class Model { } final class UIBindableTests: XCTestCase { - @MainActor - func testDynamicMemberLookupBindable() throws { + func testDynamicMemberLookupBindable() { @UIBindable var model = Model() let textBinding = $model.text XCTAssert(type(of: textBinding) == UIBinding.self) @@ -24,8 +23,7 @@ final class UIBindableTests: XCTestCase { XCTAssertEqual(textBinding.wrappedValue, "Blob, Jr.") } - @MainActor - func testEquatableHashable() throws { + func testEquatableHashable() { let model = Model() @UIBindable var model1 = model @UIBindable var model2 = model diff --git a/Tests/UIKitNavigationTests/UIBindingTests.swift b/Tests/SwiftNavigationTests/UIBindingTests.swift similarity index 97% rename from Tests/UIKitNavigationTests/UIBindingTests.swift rename to Tests/SwiftNavigationTests/UIBindingTests.swift index cffce2894..d2942345e 100644 --- a/Tests/UIKitNavigationTests/UIBindingTests.swift +++ b/Tests/SwiftNavigationTests/UIBindingTests.swift @@ -2,8 +2,7 @@ import SwiftNavigation import XCTest final class UIBindingTests: XCTestCase { - @MainActor - func testInitProjectedValue() throws { + func testInitProjectedValue() { @UIBinding var text = "" let textBinding = UIBinding(projectedValue: $text) @@ -16,7 +15,6 @@ final class UIBindingTests: XCTestCase { XCTAssertEqual(textBinding.wrappedValue, "Blob, Jr.") } - @MainActor func testOperationFromOptional() throws { @UIBinding var count: Int? = nil @@ -48,7 +46,6 @@ final class UIBindingTests: XCTestCase { XCTAssertEqual(unwrappedCountBinding.wrappedValue, 1729) } - @MainActor func testOperationToOptional() { @UIBinding var count = 0 @@ -67,7 +64,6 @@ final class UIBindingTests: XCTestCase { XCTAssertEqual(optionalCountBinding.wrappedValue, 2) } - // @MainActor // func testOperationToAnyHashable() { // @UIBinding var count = 0 // @@ -84,7 +80,6 @@ final class UIBindingTests: XCTestCase { // XCTAssertEqual(optionalCountBinding.wrappedValue, 2) // } - @MainActor func testOperationConstant() { @UIBinding var count: Int _count = .constant(0) @@ -93,7 +88,6 @@ final class UIBindingTests: XCTestCase { XCTAssertEqual(count, 0) } - @MainActor func testDynamicMemberLookupProperty() { struct User { var name = "" @@ -111,7 +105,6 @@ final class UIBindingTests: XCTestCase { XCTAssertEqual(nameBinding.wrappedValue, "Blob, Jr.") } - @MainActor func testDynamicMemberLookupCase() throws { struct Failure: Error, Equatable {} @@ -144,7 +137,6 @@ final class UIBindingTests: XCTestCase { XCTAssertEqual(countBinding.wrappedValue, 1729) } - @MainActor func testDynamicMemberLookupOptionalEnumCase() throws { struct Failure: Error, Equatable {} diff --git a/Tests/UIKitNavigationTests/UINavigationPathTests.swift b/Tests/SwiftNavigationTests/UINavigationPathTests.swift similarity index 100% rename from Tests/UIKitNavigationTests/UINavigationPathTests.swift rename to Tests/SwiftNavigationTests/UINavigationPathTests.swift diff --git a/Tests/SwiftNavigationTests/UITransactionTests.swift b/Tests/SwiftNavigationTests/UITransactionTests.swift index 3c5ceb5d5..8fe94c61b 100644 --- a/Tests/SwiftNavigationTests/UITransactionTests.swift +++ b/Tests/SwiftNavigationTests/UITransactionTests.swift @@ -2,81 +2,93 @@ import SwiftNavigation import XCTest class UITransactionTests: XCTestCase { - @MainActor - func testTransactionKeyPropagates() async { - let expectation = expectation(description: "onChange") - expectation.expectedFulfillmentCount = 2 - - let model = Model() - XCTAssertEqual(UITransaction.current.isSet, false) - - observe { - if model.count == 0 { - XCTAssertEqual(UITransaction.current.isSet, false) - } else if model.count == 1 { - XCTAssertEqual(UITransaction.current.isSet, true) - } else { - XCTFail() + #if compiler(>=6) + func testTransactionKeyPropagates() async throws { + try await Task { @MainActor in + var tokens: Set = [] + let model = Model() + XCTAssertEqual(UITransaction.current.isSet, false) + + var didObserve = false + SwiftNavigation.observe { + if model.count == 0 { + XCTAssertEqual(UITransaction.current.isSet, false) + } else if model.count == 1 { + XCTAssertEqual(UITransaction.current.isSet, true) + } else { + XCTFail() + } + didObserve = true } - expectation.fulfill() - } + .store(in: &tokens) - withUITransaction(\.isSet, true) { - model.count += 1 + withUITransaction(\.isSet, true) { + model.count += 1 + } + try await Task.sleep(nanoseconds: 300_000_000) + XCTAssertEqual(didObserve, true) + XCTAssertEqual(model.count, 1) + XCTAssertEqual(UITransaction.current.isSet, false) } - await fulfillment(of: [expectation], timeout: 1) - XCTAssertEqual(model.count, 1) - XCTAssertEqual(UITransaction.current.isSet, false) + .value } - @MainActor - func testTransactionMerging() async { - observe { transaction in + func testTransactionMerging() { + var tokens: Set = [] + SwiftNavigation.observe { transaction in XCTAssertFalse(transaction.isSet) XCTAssertFalse(transaction.isAlsoSet) } + .store(in: &tokens) withUITransaction(\.isSet, true) { - observe { transaction in + SwiftNavigation.observe { transaction in XCTAssertTrue(transaction.isSet) XCTAssertFalse(transaction.isAlsoSet) } - _ = withUITransaction(\.isAlsoSet, true) { - observe { transaction in + .store(in: &tokens) + withUITransaction(\.isAlsoSet, true) { + SwiftNavigation.observe { transaction in XCTAssertTrue(transaction.isSet) XCTAssertTrue(transaction.isAlsoSet) } + .store(in: &tokens) } - observe { transaction in + SwiftNavigation.observe { transaction in XCTAssertTrue(transaction.isSet) XCTAssertFalse(transaction.isAlsoSet) } + .store(in: &tokens) } - observe { transaction in + SwiftNavigation.observe { transaction in XCTAssertFalse(transaction.isSet) XCTAssertFalse(transaction.isAlsoSet) } + .store(in: &tokens) } - @MainActor - func testSynchronousTransactionKey() async { - let expectation = expectation(description: "onChange") - - let model = Model() - XCTAssertEqual(UITransaction.current.isSet, false) - - _ = withUITransaction(\.isSet, true) { - observe { - XCTAssertEqual(model.count, 0) - XCTAssertEqual(UITransaction.current.isSet, true) - expectation.fulfill() + func testSynchronousTransactionKey() async throws { + try await Task { @MainActor in + var tokens: Set = [] + let model = Model() + XCTAssertEqual(UITransaction.current.isSet, false) + + var didObserve = false + withUITransaction(\.isSet, true) { + SwiftNavigation.observe { + XCTAssertEqual(model.count, 0) + XCTAssertEqual(UITransaction.current.isSet, true) + didObserve = true + } + .store(in: &tokens) } - } - await fulfillment(of: [expectation], timeout: 1) - XCTAssertEqual(UITransaction.current.isSet, false) + try await Task.sleep(nanoseconds: 300_000_000) + XCTAssertEqual(didObserve, true) + XCTAssertEqual(UITransaction.current.isSet, false) + } + .value } - @MainActor func testOverrideTransactionKey() async { XCTAssertEqual(UITransaction.current.isSet, false) withUITransaction(\.isSet, true) { @@ -87,31 +99,35 @@ class UITransactionTests: XCTestCase { } } - @MainActor - func testBindingTransactionKey() async { - let expectation = expectation(description: "onChange") - expectation.expectedFulfillmentCount = 2 - - @UIBinding var count = 0 - var transaction = UITransaction() - transaction.isSet = true - - observe { - if count == 0 { - XCTAssertEqual(UITransaction.current.isSet, false) - } else if count == 1 { - XCTAssertEqual(UITransaction.current.isSet, true) - } else { - XCTFail() + func testBindingTransactionKey() async throws { + try await Task { @MainActor in + var tokens: Set = [] + @UIBinding var count = 0 + var transaction = UITransaction() + transaction.isSet = true + + var didObserve = false + SwiftNavigation.observe { + if count == 0 { + XCTAssertEqual(UITransaction.current.isSet, false) + } else if count == 1 { + XCTAssertEqual(UITransaction.current.isSet, true) + } else { + XCTFail() + } + didObserve = true } - expectation.fulfill() - } + .store(in: &tokens) - let bindingWithTransaction = $count.transaction(transaction) - bindingWithTransaction.wrappedValue = 1 + let bindingWithTransaction = $count.transaction(transaction) + bindingWithTransaction.wrappedValue = 1 - await fulfillment(of: [expectation], timeout: 1) + try await Task.sleep(nanoseconds: 300_000_000) + XCTAssertEqual(didObserve, true) + } + .value } + #endif } @Perceptible diff --git a/Tests/UIKitNavigationTests/Internal/XCTTODO.swift b/Tests/UIKitNavigationTests/Internal/XCTTODO.swift deleted file mode 100644 index 5f1bbb453..000000000 --- a/Tests/UIKitNavigationTests/Internal/XCTTODO.swift +++ /dev/null @@ -1,11 +0,0 @@ -import XCTest - -@_transparent -@available( - *, - deprecated, - message: "This is a test that currently fails but should not in the future." -) -func XCTTODO(_ message: String) { - XCTExpectFailure(message) -}