Files
swift-mirror/test/stdlib/Observation/Observable.swift
Philippe Hausler 9d6a4f17bc [Observation] Partial revert of the dirty bit tracking (#88167)
This is a partial revert to the dirty bit tracking since that has caused
runtime failures for existing applications. It may be resurrected later
once we can isolate the actual impact to behavior.
2026-03-30 16:33:17 -07:00

612 lines
14 KiB
Swift

// REQUIRES: swift_swift_parser, executable_test
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library -enable-experimental-feature Macros -Xfrontend -plugin-path -Xfrontend %swift-plugin-dir)
// Run this test via the swift-plugin-server
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library -enable-experimental-feature Macros -Xfrontend -external-plugin-path -Xfrontend %swift-plugin-dir#%swift-plugin-server)
// REQUIRES: observation
// REQUIRES: concurrency
// REQUIRES: objc_interop
// REQUIRES: swift_feature_Macros
// UNSUPPORTED: use_os_stdlib
// UNSUPPORTED: back_deployment_runtime
import StdlibUnittest
import Observation
@usableFromInline
@inline(never)
func _blackHole<T>(_ value: T) { }
@Observable
class ContainsNothing { }
@Observable
class ContainsWeak {
weak var obj: AnyObject? = nil
}
@Observable
public class PublicContainsWeak {
public weak var obj: AnyObject? = nil
}
@Observable
class ContainsUnowned {
unowned var obj: AnyObject? = nil
}
@Observable
class ContainsIUO {
var obj: Int! = nil
}
class NonObservable {
}
@Observable
class InheritsFromNonObservable: NonObservable {
}
protocol NonObservableProtocol {
}
@Observable
class ConformsToNonObservableProtocol: NonObservableProtocol {
}
struct NonObservableContainer {
@Observable
class ObservableContents {
var field: Int = 3
}
}
@Observable
final class SendableClass: Sendable {
var field: Int = 3
}
@Observable
class CodableClass: Codable {
var field: Int = 3
}
@Observable
final class HashableClass {
var field: Int = 3
}
extension HashableClass: Hashable {
static func == (lhs: HashableClass, rhs: HashableClass) -> Bool {
lhs.field == rhs.field
}
func hash(into hasher: inout Hasher) {
hasher.combine(field)
}
}
@Observable
class ImplementsAccessAndMutation {
var field = 3
let accessCalled: (PartialKeyPath<ImplementsAccessAndMutation>) -> Void
let withMutationCalled: (PartialKeyPath<ImplementsAccessAndMutation>) -> Void
init(accessCalled: @escaping (PartialKeyPath<ImplementsAccessAndMutation>) -> Void, withMutationCalled: @escaping (PartialKeyPath<ImplementsAccessAndMutation>) -> Void) {
self.accessCalled = accessCalled
self.withMutationCalled = withMutationCalled
}
internal func access<Member>(
keyPath: KeyPath<ImplementsAccessAndMutation , Member>
) {
accessCalled(keyPath)
_$observationRegistrar.access(self, keyPath: keyPath)
}
internal func withMutation<Member, T>(
keyPath: KeyPath<ImplementsAccessAndMutation , Member>,
_ mutation: () throws -> T
) rethrows -> T {
withMutationCalled(keyPath)
return try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
}
}
@Observable
class HasIgnoredProperty {
var field = 3
@ObservationIgnored var ignored = 4
}
@Observable
class Entity {
var age: Int = 0
}
@Observable
class Person : Entity {
var firstName = ""
var lastName = ""
var friends = [Person]()
var fullName: String { firstName + " " + lastName }
}
@Observable
class MiddleNamePerson: Person {
var middleName = ""
override var fullName: String { firstName + " " + middleName + " " + lastName }
}
@Observable
class IsolatedClass {
@MainActor var test = "hello"
}
@MainActor
@Observable
class IsolatedInstance {
var test = "hello"
}
@Observable
class IgnoredComputed {
@ObservationIgnored
var message: String { "hello" }
}
@Observable
class ClassHasExistingConformance: Observable { }
protocol Intermediary: Observable { }
@Observable
class HasIntermediaryConformance: Intermediary { }
class CapturedState<State>: @unchecked Sendable {
var state: State
init(state: State) {
self.state = state
}
}
@Observable
class RecursiveInner {
var value = "prefix"
}
@Observable
class GenericClass<T> {
var value = 3
}
struct StructParent {
@Observable
class NestedGenericClass<T> {
var value = 3
}
}
struct GenericStructParent<T> {
@Observable
class NestedClass {
var value = 3
}
}
class ClassParent {
@Observable
class NestedGenericClass<T> {
var value = 3
}
}
class GenericClassParent<T> {
@Observable
class NestedClass {
var value = 3
}
}
@Observable
class RecursiveOuter {
var inner = RecursiveInner()
var value = "prefix"
@ObservationIgnored var innerEventCount = 0
@ObservationIgnored var outerEventCount = 0
func recursiveTrackingCalls() {
withObservationTracking({
let _ = value
_ = withObservationTracking({
inner.value
}, onChange: {
self.innerEventCount += 1
})
}, onChange: {
self.outerEventCount += 1
})
}
}
@Observable
#if FOO
@available(SwiftStdlib 5.9, *)
#elseif BAR
@available(SwiftStdlib 5.9, *)
#else
#endif
class GuardedAvailability {
}
@Observable class TestASTScopeLCA {
// Make sure ASTScope unqualified lookup can find local variables
// inside initial values with closures when accessor macros are
// involved.
var state : Bool = {
let value = true
return value
}()
}
@Observable class Parent {
class Nested {}
}
extension Parent.Nested {}
struct CowContainer {
final class Contents { }
var contents = Contents()
mutating func mutate() {
if !isKnownUniquelyReferenced(&contents) {
contents = Contents()
}
}
var id: ObjectIdentifier {
ObjectIdentifier(contents)
}
}
@Observable
final class CowTest {
var container = CowContainer()
}
@main
struct Validator {
@MainActor
static func main() async {
let suite = TestSuite("Observable")
suite.test("only instantiate") {
let test = MiddleNamePerson()
}
suite.test("unobserved value changes") {
let test = MiddleNamePerson()
for i in 0..<100 {
test.firstName = "\(i)"
}
}
suite.test("tracking changes") {
let changed = CapturedState(state: false)
let test = MiddleNamePerson()
withObservationTracking {
_blackHole(test.firstName)
} onChange: {
changed.state = true
}
test.firstName = "c"
expectEqual(changed.state, true)
changed.state = false
test.firstName = "c"
expectEqual(changed.state, false)
}
suite.test("conformance") {
func testConformance<O: Observable>(_ o: O) -> Bool {
return true
}
func testConformance<O>(_ o: O) -> Bool {
return false
}
let test = Person()
expectEqual(testConformance(test), true)
}
suite.test("tracking nonchanged") {
let changed = CapturedState(state: false)
let test = MiddleNamePerson()
withObservationTracking {
_blackHole(test.lastName)
} onChange: {
changed.state = true
}
test.firstName = "c"
expectEqual(changed.state, false)
}
suite.test("tracking computed") {
let changed = CapturedState(state: false)
let test = MiddleNamePerson()
withObservationTracking {
_blackHole(test.fullName)
} onChange: {
changed.state = true
}
test.middleName = "c"
expectEqual(changed.state, true)
changed.state = false
test.middleName = "c"
expectEqual(changed.state, false)
}
suite.test("graph changes") {
let changed = CapturedState(state: false)
let test = MiddleNamePerson()
let friend = MiddleNamePerson()
test.friends.append(friend)
withObservationTracking {
_blackHole(test.friends.first?.fullName)
} onChange: {
changed.state = true
}
test.middleName = "c"
expectEqual(changed.state, false)
friend.middleName = "c"
expectEqual(changed.state, true)
}
suite.test("nesting") {
let changedOuter = CapturedState(state: false)
let changedInner = CapturedState(state: false)
let test = MiddleNamePerson()
withObservationTracking {
withObservationTracking {
_blackHole(test.firstName)
} onChange: {
changedInner.state = true
}
} onChange: {
changedOuter.state = true
}
test.firstName = "c"
expectEqual(changedInner.state, true)
expectEqual(changedOuter.state, true)
changedOuter.state = false
test.firstName = "c"
expectEqual(changedOuter.state, false)
}
suite.test("access and mutation") {
let accessKeyPath = CapturedState<PartialKeyPath<ImplementsAccessAndMutation>?>(state: nil)
let mutationKeyPath = CapturedState<PartialKeyPath<ImplementsAccessAndMutation>?>(state: nil)
let test = ImplementsAccessAndMutation { keyPath in
accessKeyPath.state = keyPath
} withMutationCalled: { keyPath in
mutationKeyPath.state = keyPath
}
expectEqual(accessKeyPath.state, nil)
_blackHole(test.field)
expectEqual(accessKeyPath.state, \.field)
expectEqual(mutationKeyPath.state, nil)
accessKeyPath.state = nil
test.field = 123
expectEqual(accessKeyPath.state, nil)
expectEqual(mutationKeyPath.state, \.field)
}
suite.test("ignores no change") {
let changed = CapturedState(state: false)
let test = HasIgnoredProperty()
withObservationTracking {
_blackHole(test.ignored)
} onChange: {
changed.state = true
}
test.ignored = 122112
expectEqual(changed.state, false)
changed.state = false
test.field = 3429
expectEqual(changed.state, false)
}
suite.test("ignores change") {
let changed = CapturedState(state: false)
let test = HasIgnoredProperty()
withObservationTracking {
_blackHole(test.ignored)
_blackHole(test.field)
} onChange: {
changed.state = true
}
test.ignored = 122112
expectEqual(changed.state, false)
changed.state = false
test.field = 3429
expectEqual(changed.state, true)
}
suite.test("isolated class") { @MainActor in
let changed = CapturedState(state: false)
let test = IsolatedClass()
withObservationTracking {
_blackHole(test.test)
} onChange: {
changed.state = true
}
test.test = "c"
expectEqual(changed.state, true)
changed.state = false
test.test = "c"
expectEqual(changed.state, false)
}
suite.test("recursive tracking inner then outer") {
let obj = RecursiveOuter()
obj.recursiveTrackingCalls()
obj.inner.value = "test"
expectEqual(obj.innerEventCount, 1)
expectEqual(obj.outerEventCount, 1)
obj.recursiveTrackingCalls()
obj.value = "test"
expectEqual(obj.innerEventCount, 1)
expectEqual(obj.outerEventCount, 2)
}
suite.test("recursive tracking outer then inner") {
let obj = RecursiveOuter()
obj.recursiveTrackingCalls()
obj.value = "test"
expectEqual(obj.innerEventCount, 0)
expectEqual(obj.outerEventCount, 1)
obj.recursiveTrackingCalls()
obj.inner.value = "test"
expectEqual(obj.innerEventCount, 2)
expectEqual(obj.outerEventCount, 2)
}
suite.test("validate copy on write semantics") {
let subject = CowTest()
let startId = subject.container.id
expectEqual(subject.container.id, startId)
subject.container.mutate()
expectEqual(subject.container.id, startId)
}
suite.test("tracking changes willSet option") {
let changed = CapturedState(state: false)
let test = MiddleNamePerson()
withObservationTracking(options: .willSet) {
_blackHole(test.firstName)
} onChange: { _ in
changed.state = true
}
test.firstName = "c"
expectEqual(changed.state, true)
changed.state = false
test.firstName = "c"
expectEqual(changed.state, false)
}
suite.test("tracking changes didSet option") {
let changed = CapturedState(state: false)
let test = MiddleNamePerson()
withObservationTracking(options: .didSet) {
_blackHole(test.firstName)
} onChange: { _ in
changed.state = true
}
test.firstName = "c"
expectEqual(changed.state, true)
changed.state = false
test.firstName = "c"
expectEqual(changed.state, false)
}
suite.test("tracking changes willSet & didSet option") {
let changed = CapturedState(state: 0)
let test = MiddleNamePerson()
withObservationTracking(options: [.willSet, .didSet]) {
_blackHole(test.firstName)
} onChange: { _ in
changed.state += 1
}
test.firstName = "c"
expectEqual(changed.state, 2) // an event is triggered for both the willSet and the didSet
changed.state = 0
test.firstName = "c"
expectEqual(changed.state, 0)
}
suite.test("tracking changes deinit option") {
let changed = CapturedState(state: false)
var test: MiddleNamePerson? = MiddleNamePerson()
withObservationTracking(options: .deinit) {
_blackHole(test?.firstName)
} onChange: { _ in
changed.state = true
}
test?.firstName = "c"
expectEqual(changed.state, false)
changed.state = false
test?.firstName = "c"
expectEqual(changed.state, false)
test = nil
expectEqual(changed.state, true)
}
suite.test("continuous tracking") {
let changed = CapturedState(state: 0)
let test: MiddleNamePerson = MiddleNamePerson()
let token = withContinuousObservation(options: .didSet) { _ in
_blackHole(test.firstName)
changed.state += 1
}
// the sleeps here are intended to be a simulation of returning to the main run loop
try! await Task.sleep(for: .seconds(0.1))
expectEqual(changed.state, 1)
test.firstName = "c"
try! await Task.sleep(for: .seconds(0.1))
expectEqual(changed.state, 2)
test.firstName = "d"
try! await Task.sleep(for: .seconds(0.1))
expectEqual(changed.state, 3)
token.cancel()
try! await Task.sleep(for: .seconds(0.1)) // ensure a small grace w.r.t. cancellation
test.firstName = "e"
try! await Task.sleep(for: .seconds(0.1))
expectEqual(changed.state, 3)
}
await runAllTestsAsync()
}
}