// 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(_ 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) -> Void let withMutationCalled: (PartialKeyPath) -> Void init(accessCalled: @escaping (PartialKeyPath) -> Void, withMutationCalled: @escaping (PartialKeyPath) -> Void) { self.accessCalled = accessCalled self.withMutationCalled = withMutationCalled } internal func access( keyPath: KeyPath ) { accessCalled(keyPath) _$observationRegistrar.access(self, keyPath: keyPath) } internal func withMutation( keyPath: KeyPath, _ 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: @unchecked Sendable { var state: State init(state: State) { self.state = state } } @Observable class RecursiveInner { var value = "prefix" } @Observable class GenericClass { var value = 3 } struct StructParent { @Observable class NestedGenericClass { var value = 3 } } struct GenericStructParent { @Observable class NestedClass { var value = 3 } } class ClassParent { @Observable class NestedGenericClass { var value = 3 } } class GenericClassParent { @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() } @Observable final class DeinitTriggeredObserver { var property: Int = 3 let deinitTrigger: () -> Void init(_ deinitTrigger: @escaping () -> Void) { self.deinitTrigger = deinitTrigger } deinit { deinitTrigger() } } @main struct Validator { @MainActor static func main() { 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: O) -> Bool { return true } func testConformance(_ 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?>(state: nil) let mutationKeyPath = CapturedState?>(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("weak container observation") { let changed = CapturedState(state: false) let deinitialized = CapturedState(state: false) var test = DeinitTriggeredObserver { deinitialized.state = true } withObservationTracking { [weak test] in _blackHole(test?.property) } onChange: { changed.state = true } test = DeinitTriggeredObserver { } expectEqual(deinitialized.state, true) expectEqual(changed.state, true) } runAllTests() } }