Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 \
Expand Down
17 changes: 9 additions & 8 deletions Docs/Toolchain/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Sources/OpenSwiftUI/View/Combine/SubscriptionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ private struct ActionDispatcherSubscriber<V>: 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ private struct ObjectLocation<Root, Value>: 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
// Audited for 6.5.4
// Status: Complete

import class Foundation.Thread
import OpenAttributeGraphShims
#if OPENSWIFTUI_OPENCOMBINE
import OpenCombine
Expand Down Expand Up @@ -42,7 +41,7 @@ class AttributeInvalidatingSubscriber<Upstream> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ private func installObservationSlow<T>(
let _ = viewGraph.asyncTransaction(
Transaction.current,
mutation: mutation,
style: Thread.isMainThread ? .immediate : .deferred,
style: isMainThreadOrMainQueue() ? .immediate : .deferred,
)
// TODO: AGGraphAddTraceEvent
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/OpenSwiftUICore/Data/Update.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ package enum Update {
// FIXME: See #76
body()
#else
if Thread.isMainThread {
if isMainThreadOrMainQueue() {
body()
} else {
withoutActuallyEscaping(body) { escapableBody in
Expand Down
2 changes: 1 addition & 1 deletion Sources/OpenSwiftUICore/Util/MainActorUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
36 changes: 34 additions & 2 deletions Sources/OpenSwiftUICore/Util/ThreadUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
// Status: Complete
// ID: 82B2D47816BC992595021D60C278AFF0 (SwiftUICore)

#if os(Linux)
import Dispatch
#endif
import Foundation

// MARK: - ThreadSpecific
Expand Down Expand Up @@ -56,25 +59,54 @@ final package class ThreadSpecific<T> {

// 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<Void> = {
let key = DispatchSpecificKey<Void>()
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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ import Testing
struct DynamicPropertyCacheTests {
@Test
func size() {
#if os(Linux)
#expect(MemoryLayout<DynamicPropertyCache.Fields>.size == 20)
#expect(MemoryLayout<DynamicPropertyCache.Fields.Layout>.size == 16)
#expect(MemoryLayout<DynamicPropertyCache.Fields?>.size == 20)
#else
#expect(MemoryLayout<DynamicPropertyCache.Fields>.size == 24)
#expect(MemoryLayout<DynamicPropertyCache.Fields.Layout>.size == 17)
#expect(MemoryLayout<DynamicPropertyCache.Fields?>.size == 24)
#endif
#expect(MemoryLayout<DynamicPropertyBehaviors>.size == 4)
}

Expand Down
5 changes: 0 additions & 5 deletions Tests/OpenSwiftUICoreTests/Util/MainActorUtilsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 48 additions & 0 deletions Tests/OpenSwiftUICoreTests/Util/ThreadUtilsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 11 additions & 1 deletion Tests/OpenSwiftUICoreTests/Util/TracingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand All @@ -21,13 +21,23 @@ 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"]),
(type: String.self as Any.Type, libraryNames: ["libswiftCore.dylib"]),
(type: Demo.self as Any.Type, libraryNames: ["OpenSwiftUICoreTests", "OpenSwiftUIPackageTests"]),
]
)
#endif
func library(type: Any.Type, libraryNames: [String]) {
#expect(libraryNames.contains(Tracing.libraryName(defining: type)))
}
Expand Down
2 changes: 1 addition & 1 deletion mise.compute.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading