diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 377aaa22f..278596b16 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -28,17 +28,21 @@ on: jobs: ubuntu_test: - name: Execute tests on Ubuntu + name: Execute tests on Ubuntu (${{ matrix.backend }}, Swift ${{ matrix.swift_version }}) outputs: sha: ${{ steps.checkout.outputs.commit }} strategy: fail-fast: false matrix: swift_version: ["6.3.3"] + backend: ["OAG", "IAG"] runs-on: ubuntu-22.04 env: OPENSWIFTUI_WERROR: 0 # Disable it to avoid enable OAG's werror and hit conflicts OPENSWIFTUI_OPENATTRIBUTESHIMS_ATTRIBUTEGRAPH: 0 + OPENSWIFTUI_OPENATTRIBUTESHIMS_COMPUTE: ${{ matrix.backend == 'IAG' && '1' || '0' }} + OPENSWIFTUI_OPENATTRIBUTESHIMS_COMPUTE_BINARY: 0 + OPENSWIFTUI_OPENATTRIBUTESHIMS_COMPUTE_SOURCE_VERSION: "0.5.2-bugfix.1" OPENSWIFTUI_COMPATIBILITY_TEST: 0 OPENSWIFTUI_SWIFT_LOG: 1 OPENSWIFTUI_SWIFT_CRYPTO: 1 @@ -53,6 +57,9 @@ jobs: run: | apt-get update apt-get install -y --no-install-recommends curl + - name: Install Compute dependencies + if: matrix.backend == 'IAG' + run: apt-get install -y --no-install-recommends libssl-dev uuid-dev - name: Building and running tests in debug mode with coverage run: | swift test \ diff --git a/Docs/Toolchain/CHANGELOG.md b/Docs/Toolchain/CHANGELOG.md index d6c06c223..d68837a48 100644 --- a/Docs/Toolchain/CHANGELOG.md +++ b/Docs/Toolchain/CHANGELOG.md @@ -307,19 +307,20 @@ record a failure if the expected runtime issue never appears. ### MainActor and Main Thread Checks `@MainActor` isolation is not the same thing as `Thread.isMainThread` on every -platform. On Linux, swift-testing can execute an `@MainActor` test body on a -Swift executor that is not Foundation's main thread. Code that checks -`Thread.isMainThread` can therefore record a runtime issue even inside an -`@MainActor` test. +platform. On Linux, the main dispatch queue can execute on a thread that is not +Foundation's main thread. `isMainThreadOrMainQueue()` accepts both contexts on +Linux. The legacy `MainActor.assumeIsolatedIfLinkedOnOrAfter` fallback uses this +helper to avoid false warnings. The modern path uses `MainActor.assumeIsolated` +to check actor isolation. For `MainActor.assumeIsolatedIfLinkedOnOrAfter` tests: - Use a linked-on-or-after semantic such as `.firstRelease` to test the `MainActor.assumeIsolated` path. -- Use `.maximal` to test the fallback path, but wrap expected warnings with the - runtime issue handler. -- Keep Darwin-only assertions guarded with `#if canImport(Darwin)` when they - rely on `@MainActor` being backed by the OS main thread. +- Use `.maximal` to test the fallback path. Calls on the main actor must pass + without a warning on both Darwin and Linux. +- Use the runtime issue handler for expected warnings from background calls + to the fallback path. ### Exit Tests diff --git a/Package.resolved b/Package.resolved index aaac3e90b..63fd98294 100644 --- a/Package.resolved +++ b/Package.resolved @@ -16,7 +16,7 @@ "location" : "https://github.com/OpenSwiftUIProject/OpenAttributeGraph", "state" : { "branch" : "main", - "revision" : "6a260ef4aa2fd926b0cf34d21d5290921e6e7323" + "revision" : "7b30d536202535c8e2e235de48764c9d5e6e6f00" } }, { diff --git a/Sources/OpenSwiftUI/View/Combine/SubscriptionView.swift b/Sources/OpenSwiftUI/View/Combine/SubscriptionView.swift index 279e540cd..9b538b8ed 100644 --- a/Sources/OpenSwiftUI/View/Combine/SubscriptionView.swift +++ b/Sources/OpenSwiftUI/View/Combine/SubscriptionView.swift @@ -115,7 +115,7 @@ private struct ActionDispatcherSubscriber: Subscriber, Cancellable { typealias Failure = Never func respond(to input: V) { - if !Thread.isMainThread { + if !isMainThreadOrMainQueue() { Log.runtimeIssues("Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.") } onMainThread { diff --git a/Sources/OpenSwiftUICore/Data/Binding/Binding+ObjectLocation.swift b/Sources/OpenSwiftUICore/Data/Binding/Binding+ObjectLocation.swift index 376e53785..ed24b6335 100644 --- a/Sources/OpenSwiftUICore/Data/Binding/Binding+ObjectLocation.swift +++ b/Sources/OpenSwiftUICore/Data/Binding/Binding+ObjectLocation.swift @@ -49,7 +49,7 @@ private struct ObjectLocation: Location where Root: AnyObject { } func checkIsolation() { - guard let isolation, isolation === MainActor.shared, !Thread.isMainThread else { + guard let isolation, isolation === MainActor.shared, !isMainThreadOrMainQueue() else { return } let description = String(describing: keyPath) diff --git a/Sources/OpenSwiftUICore/Data/Combine/AttributeInvalidatingSubscriber.swift b/Sources/OpenSwiftUICore/Data/Combine/AttributeInvalidatingSubscriber.swift index bfcbd642c..067729d4b 100644 --- a/Sources/OpenSwiftUICore/Data/Combine/AttributeInvalidatingSubscriber.swift +++ b/Sources/OpenSwiftUICore/Data/Combine/AttributeInvalidatingSubscriber.swift @@ -5,7 +5,6 @@ // Audited for 6.5.4 // Status: Complete -import class Foundation.Thread import OpenAttributeGraphShims #if OPENSWIFTUI_OPENCOMBINE import OpenCombine @@ -42,7 +41,7 @@ class AttributeInvalidatingSubscriber where Upstream: Publisher { private func invalidateAttribute() { let style: GraphMutation.Style - if !Thread.isMainThread { + if !isMainThreadOrMainQueue() { Log.runtimeIssues("Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.") style = .immediate } else if Update.threadIsUpdating, isLinkedOnOrAfter(.v4) { diff --git a/Sources/OpenSwiftUICore/Data/Observation/ObservationUtils.swift b/Sources/OpenSwiftUICore/Data/Observation/ObservationUtils.swift index bc99f5da2..fa794e561 100644 --- a/Sources/OpenSwiftUICore/Data/Observation/ObservationUtils.swift +++ b/Sources/OpenSwiftUICore/Data/Observation/ObservationUtils.swift @@ -180,7 +180,7 @@ private func installObservationSlow( let _ = viewGraph.asyncTransaction( Transaction.current, mutation: mutation, - style: Thread.isMainThread ? .immediate : .deferred, + style: isMainThreadOrMainQueue() ? .immediate : .deferred, ) // TODO: AGGraphAddTraceEvent } diff --git a/Sources/OpenSwiftUICore/Data/Update.swift b/Sources/OpenSwiftUICore/Data/Update.swift index 1bbe23baf..e562471c8 100644 --- a/Sources/OpenSwiftUICore/Data/Update.swift +++ b/Sources/OpenSwiftUICore/Data/Update.swift @@ -141,7 +141,7 @@ package enum Update { // FIXME: See #76 body() #else - if Thread.isMainThread { + if isMainThreadOrMainQueue() { body() } else { withoutActuallyEscaping(body) { escapableBody in diff --git a/Sources/OpenSwiftUICore/Util/MainActorUtils.swift b/Sources/OpenSwiftUICore/Util/MainActorUtils.swift index 984c16d61..5e015c70e 100644 --- a/Sources/OpenSwiftUICore/Util/MainActorUtils.swift +++ b/Sources/OpenSwiftUICore/Util/MainActorUtils.swift @@ -20,7 +20,7 @@ extension MainActor { return try assumeIsolated(operation, file: file, line: line) } else { let context = context.map { "\($0) " } ?? "" - if !Thread.isMainThread { + if !isMainThreadOrMainQueue() { Log.runtimeIssues( "%s This warning will become a runtime crash in a future version of OpenSwiftUI.", [context] diff --git a/Sources/OpenSwiftUICore/Util/ThreadUtils.swift b/Sources/OpenSwiftUICore/Util/ThreadUtils.swift index 346926d31..39a4f3aaf 100644 --- a/Sources/OpenSwiftUICore/Util/ThreadUtils.swift +++ b/Sources/OpenSwiftUICore/Util/ThreadUtils.swift @@ -6,6 +6,9 @@ // Status: Complete // ID: 82B2D47816BC992595021D60C278AFF0 (SwiftUICore) +#if os(Linux) +import Dispatch +#endif import Foundation // MARK: - ThreadSpecific @@ -56,25 +59,54 @@ final package class ThreadSpecific { // MARK: - Thread + Global helper function +// OpenSwiftUI Addition begin + +#if os(Linux) +// Identify the main queue even when it runs on a different thread. +// https://github.com/swiftlang/swift-corelibs-libdispatch/issues/846 +private let mainQueueKey: DispatchSpecificKey = { + let key = DispatchSpecificKey() + DispatchQueue.main.setSpecific(key: key, value: ()) + return key +}() +#endif + +package func isMainThreadOrMainQueue() -> Bool { + #if os(WASI) + return true + #elseif os(Linux) + return Thread.isMainThread || DispatchQueue.getSpecific(key: mainQueueKey) != nil + #else + return Thread.isMainThread + #endif +} + +// OpenSwiftUI Addition end + package func onMainThread(do body: @escaping () -> Void) { #if os(WASI) // See #76: Thread and RunLoopMode.common is not available on WASI currently body() #else - if Thread.isMainThread { + if isMainThreadOrMainQueue() { body() } else { + #if os(Linux) + // The default Linux main executor does not run a CFRunLoop. + DispatchQueue.main.async(execute: body) + #else RunLoop.main.perform(inModes: [.common]) { // Workaround the @Senable warning body() } + #endif } #endif } package func mainThreadPrecondition() { #if !os(WASI) - precondition(Thread.isMainThread, "calling into OpenSwiftUI on a non-main thread is not supported") + precondition(isMainThreadOrMainQueue(), "calling into OpenSwiftUI on a non-main thread is not supported") #endif } diff --git a/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyCacheTests.swift b/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyCacheTests.swift index 46515e63e..ef0a8696e 100644 --- a/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyCacheTests.swift +++ b/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyCacheTests.swift @@ -11,9 +11,15 @@ import Testing struct DynamicPropertyCacheTests { @Test func size() { + #if os(Linux) + #expect(MemoryLayout.size == 20) + #expect(MemoryLayout.size == 16) + #expect(MemoryLayout.size == 20) + #else #expect(MemoryLayout.size == 24) #expect(MemoryLayout.size == 17) #expect(MemoryLayout.size == 24) + #endif #expect(MemoryLayout.size == 4) } diff --git a/Tests/OpenSwiftUICoreTests/Util/MainActorUtilsTests.swift b/Tests/OpenSwiftUICoreTests/Util/MainActorUtilsTests.swift index 88c96057a..ce1d9840a 100644 --- a/Tests/OpenSwiftUICoreTests/Util/MainActorUtilsTests.swift +++ b/Tests/OpenSwiftUICoreTests/Util/MainActorUtilsTests.swift @@ -31,16 +31,11 @@ struct MainActorUtilsTests { Self.assumeWithFirstRelease() } - // On non-Darwin platforms, Swift Testing may run @MainActor tests on a - // Swift executor that is not Thread.isMainThread, which intentionally - // records a runtime issue in the fallback path. - #if canImport(Darwin) @Test @MainActor func mainActorAssumeFail() { Self.assumeWithMaximal() } - #endif #if !os(iOS) && !os(visionOS) @Test diff --git a/Tests/OpenSwiftUICoreTests/Util/ThreadUtilsTests.swift b/Tests/OpenSwiftUICoreTests/Util/ThreadUtilsTests.swift index addd782ac..0c254eb5f 100644 --- a/Tests/OpenSwiftUICoreTests/Util/ThreadUtilsTests.swift +++ b/Tests/OpenSwiftUICoreTests/Util/ThreadUtilsTests.swift @@ -5,6 +5,54 @@ import OpenSwiftUICore import Testing +// MARK: - ThreadUtilsTests + +#if !os(WASI) +struct ThreadUtilsTests { + @Test + @MainActor + func mainActor() { + mainThreadPrecondition() + } + + @Test + @MainActor + func onMainThreadRunsImmediately() { + var ran = false + onMainThread { + mainThreadPrecondition() + ran = true + } + #expect(ran) + } + + #if os(Linux) + @Test(.timeLimit(.minutes(1))) + func onMainThreadFromDetachedTask() async { + await Task.detached { + await withCheckedContinuation { continuation in + onMainThread { + mainThreadPrecondition() + continuation.resume() + } + } + }.value + } + #endif + + #if !os(iOS) && !os(visionOS) + @Test + func detachedTask() async { + await #expect(processExitsWith: .failure) { + await Task.detached { + mainThreadPrecondition() + }.value + } + } + #endif +} +#endif + // MARK: - ThreadSpecificTests struct ThreadSpecificTests { diff --git a/Tests/OpenSwiftUICoreTests/Util/TracingTests.swift b/Tests/OpenSwiftUICoreTests/Util/TracingTests.swift index 6fe57fec3..c11ba869c 100644 --- a/Tests/OpenSwiftUICoreTests/Util/TracingTests.swift +++ b/Tests/OpenSwiftUICoreTests/Util/TracingTests.swift @@ -2,8 +2,8 @@ // TracingTests.swift // OpenSwiftUICoreTests -@testable import OpenSwiftUICore import OpenAttributeGraphShims +@testable import OpenSwiftUICore import Testing @Suite(.disabled(if: attributeGraphVendor == .oag, "Not implemented in OAG yet")) @@ -21,6 +21,15 @@ struct TracingTests { #expect(Tracing.nominalTypeName(type) == nominalName) } + #if os(Linux) + @Test( + arguments: [ + (type: Int.self as Any.Type, libraryNames: ["libswiftCore.so"]), + (type: String.self as Any.Type, libraryNames: ["libswiftCore.so"]), + (type: Demo.self as Any.Type, libraryNames: ["OpenSwiftUIPackageTests.xctest"]), + ] + ) + #else @Test( arguments: [ (type: Int.self as Any.Type, libraryNames: ["libswiftCore.dylib"]), @@ -28,6 +37,7 @@ struct TracingTests { (type: Demo.self as Any.Type, libraryNames: ["OpenSwiftUICoreTests", "OpenSwiftUIPackageTests"]), ] ) + #endif func library(type: Any.Type, libraryNames: [String]) { #expect(libraryNames.contains(Tracing.libraryName(defining: type))) } diff --git a/mise.compute.toml b/mise.compute.toml index a5b3408a2..0fdd71d7b 100644 --- a/mise.compute.toml +++ b/mise.compute.toml @@ -8,5 +8,5 @@ OPENSWIFTUI_OPENATTRIBUTESHIMS_COMPUTE = "1" OPENSWIFTUI_OPENATTRIBUTESHIMS_COMPUTE_BINARY = { default = "1" } OPENSWIFTUI_OPENATTRIBUTESHIMS_COMPUTE_BINARY_VERSION = "0.5.2" OPENSWIFTUI_OPENATTRIBUTESHIMS_COMPUTE_BINARY_CHECKSUM = "0eca53a3620776cffcc3d2047d11efaf893b53078b95e59e793f86caa3f2c169" -OPENSWIFTUI_OPENATTRIBUTESHIMS_COMPUTE_SOURCE_VERSION = "0.5.2" +OPENSWIFTUI_OPENATTRIBUTESHIMS_COMPUTE_SOURCE_VERSION = "0.5.2-bugfix.1" # OPENSWIFTUI_USE_LOCAL_DEPS = "1"