//////////////////////////////////////////////////////////////////////////// // // Copyright 2015 Realm Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // //////////////////////////////////////////////////////////////////////////// import XCTest import RealmSwift var pkCounter = 0 func nextPrimaryKey() -> Int { pkCounter += 1 return pkCounter } class SwiftKVOObject: Object { @objc dynamic var pk = nextPrimaryKey() // primary key for equality @objc dynamic var ignored: Int = 0 @objc dynamic var boolCol: Bool = false @objc dynamic var int8Col: Int8 = 1 @objc dynamic var int16Col: Int16 = 2 @objc dynamic var int32Col: Int32 = 3 @objc dynamic var int64Col: Int64 = 4 @objc dynamic var floatCol: Float = 5 @objc dynamic var doubleCol: Double = 6 @objc dynamic var stringCol: String = "" @objc dynamic var binaryCol: Data = Data() @objc dynamic var dateCol: Date = Date(timeIntervalSince1970: 0) @objc dynamic var objectCol: SwiftKVOObject? let arrayCol = List<SwiftKVOObject>() let optIntCol = RealmOptional<Int>() let optFloatCol = RealmOptional<Float>() let optDoubleCol = RealmOptional<Double>() let optBoolCol = RealmOptional<Bool>() @objc dynamic var optStringCol: String? @objc dynamic var optBinaryCol: Data? @objc dynamic var optDateCol: Date? let arrayBool = List<Bool>() let arrayInt8 = List<Int8>() let arrayInt16 = List<Int16>() let arrayInt32 = List<Int32>() let arrayInt64 = List<Int64>() let arrayFloat = List<Float>() let arrayDouble = List<Double>() let arrayString = List<String>() let arrayBinary = List<Data>() let arrayDate = List<Date>() let arrayOptBool = List<Bool?>() let arrayOptInt8 = List<Int8?>() let arrayOptInt16 = List<Int16?>() let arrayOptInt32 = List<Int32?>() let arrayOptInt64 = List<Int64?>() let arrayOptFloat = List<Float?>() let arrayOptDouble = List<Double?>() let arrayOptString = List<String?>() let arrayOptBinary = List<Data?>() let arrayOptDate = List<Date?>() override class func primaryKey() -> String { return "pk" } override class func ignoredProperties() -> [String] { return ["ignored"] } } // Most of the testing of KVO functionality is done in the obj-c tests // These tests just verify that it also works on Swift types class KVOTests: TestCase { var realm: Realm! = nil override func setUp() { super.setUp() realm = try! Realm() realm.beginWrite() } override func tearDown() { realm.cancelWrite() realm = nil super.tearDown() } var changeDictionary: [NSKeyValueChangeKey: Any]? override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { changeDictionary = change } // swiftlint:disable:next cyclomatic_complexity func observeChange<T: Equatable>(_ obj: SwiftKVOObject, _ key: String, _ old: T?, _ new: T?, fileName: StaticString = #file, lineNumber: UInt = #line, _ block: () -> Void) { let kvoOptions: NSKeyValueObservingOptions = [.old, .new] obj.addObserver(self, forKeyPath: key, options: kvoOptions, context: nil) block() obj.removeObserver(self, forKeyPath: key) XCTAssert(changeDictionary != nil, "Did not get a notification", file: fileName, line: lineNumber) guard changeDictionary != nil else { return } let actualOld = changeDictionary![.oldKey]! as? T let actualNew = changeDictionary![.newKey]! as? T XCTAssert(old == actualOld, "Old value: expected \(String(describing: old)), got \(String(describing: actualOld))", file: fileName, line: lineNumber) XCTAssert(new == actualNew, "New value: expected \(String(describing: new)), got \(String(describing: actualNew))", file: fileName, line: lineNumber) changeDictionary = nil } func observeChange<T: Equatable>(_ obj: SwiftKVOObject, _ keyPath: KeyPath<SwiftKVOObject, T>, _ old: Any?, _ new: Any?, fileName: StaticString = #file, lineNumber: UInt = #line, _ block: () -> Void) { let kvoOptions: NSKeyValueObservingOptions = [.old, .new] var gotNotification = false let observation = obj.observe(keyPath, options: kvoOptions) { _, change in if let old = old { XCTAssertEqual(change.oldValue, (old as! T), file: fileName, line: lineNumber) } else { XCTAssertNil(change.oldValue, file: fileName, line: lineNumber) } if let new = new { XCTAssertEqual(change.newValue, (new as! T), file: fileName, line: lineNumber) } else { XCTAssertNil(change.newValue, file: fileName, line: lineNumber) } gotNotification = true } block() observation.invalidate() XCTAssertTrue(gotNotification, file: fileName, line: lineNumber) } func observeListChange(_ obj: NSObject, _ key: String, _ kind: NSKeyValueChange, _ indexes: NSIndexSet = NSIndexSet(index: 0), fileName: StaticString = #file, lineNumber: UInt = #line, _ block: () -> Void) { obj.addObserver(self, forKeyPath: key, options: [.old, .new], context: nil) block() obj.removeObserver(self, forKeyPath: key) XCTAssert(changeDictionary != nil, "Did not get a notification", file: fileName, line: lineNumber) guard changeDictionary != nil else { return } let actualKind = NSKeyValueChange(rawValue: (changeDictionary![NSKeyValueChangeKey.kindKey] as! NSNumber).uintValue)! let actualIndexes = changeDictionary![NSKeyValueChangeKey.indexesKey]! as! NSIndexSet XCTAssert(actualKind == kind, "Change kind: expected \(kind), got \(actualKind)", file: fileName, line: lineNumber) XCTAssert(actualIndexes.isEqual(indexes), "Changed indexes: expected \(indexes), got \(actualIndexes)", file: fileName, line: lineNumber) changeDictionary = nil } func getObject(_ obj: SwiftKVOObject) -> (SwiftKVOObject, SwiftKVOObject) { return (obj, obj) } // Actual tests follow func testAllPropertyTypes() { let (obj, obs) = getObject(SwiftKVOObject()) observeChange(obs, "boolCol", false, true) { obj.boolCol = true } observeChange(obs, "int8Col", 1 as Int8, 10) { obj.int8Col = 10 } observeChange(obs, "int16Col", 2 as Int16, 10) { obj.int16Col = 10 } observeChange(obs, "int32Col", 3 as Int32, 10) { obj.int32Col = 10 } observeChange(obs, "int64Col", 4 as Int64, 10) { obj.int64Col = 10 } observeChange(obs, "floatCol", 5 as Float, 10) { obj.floatCol = 10 } observeChange(obs, "doubleCol", 6 as Double, 10) { obj.doubleCol = 10 } observeChange(obs, "stringCol", "", "abc") { obj.stringCol = "abc" } observeChange(obs, "objectCol", nil, obj) { obj.objectCol = obj } let data = "abc".data(using: String.Encoding.utf8, allowLossyConversion: false)! observeChange(obs, "binaryCol", Data(), data) { obj.binaryCol = data } let date = Date(timeIntervalSince1970: 1) observeChange(obs, "dateCol", Date(timeIntervalSince1970: 0), date) { obj.dateCol = date } observeListChange(obs, "arrayCol", .insertion) { obj.arrayCol.append(obj) } observeListChange(obs, "arrayCol", .removal) { obj.arrayCol.removeAll() } observeChange(obs, "optIntCol", nil, 10) { obj.optIntCol.value = 10 } observeChange(obs, "optFloatCol", nil, 10.0) { obj.optFloatCol.value = 10 } observeChange(obs, "optDoubleCol", nil, 10.0) { obj.optDoubleCol.value = 10 } observeChange(obs, "optBoolCol", nil, true) { obj.optBoolCol.value = true } observeChange(obs, "optStringCol", nil, "abc") { obj.optStringCol = "abc" } observeChange(obs, "optBinaryCol", nil, data) { obj.optBinaryCol = data } observeChange(obs, "optDateCol", nil, date) { obj.optDateCol = date } observeChange(obs, "optIntCol", 10, nil) { obj.optIntCol.value = nil } observeChange(obs, "optFloatCol", 10.0, nil) { obj.optFloatCol.value = nil } observeChange(obs, "optDoubleCol", 10.0, nil) { obj.optDoubleCol.value = nil } observeChange(obs, "optBoolCol", true, nil) { obj.optBoolCol.value = nil } observeChange(obs, "optStringCol", "abc", nil) { obj.optStringCol = nil } observeChange(obs, "optBinaryCol", data, nil) { obj.optBinaryCol = nil } observeChange(obs, "optDateCol", date, nil) { obj.optDateCol = nil } observeListChange(obs, "arrayBool", .insertion) { obj.arrayBool.append(true); } observeListChange(obs, "arrayInt8", .insertion) { obj.arrayInt8.append(10); } observeListChange(obs, "arrayInt16", .insertion) { obj.arrayInt16.append(10); } observeListChange(obs, "arrayInt32", .insertion) { obj.arrayInt32.append(10); } observeListChange(obs, "arrayInt64", .insertion) { obj.arrayInt64.append(10); } observeListChange(obs, "arrayFloat", .insertion) { obj.arrayFloat.append(10); } observeListChange(obs, "arrayDouble", .insertion) { obj.arrayDouble.append(10); } observeListChange(obs, "arrayString", .insertion) { obj.arrayString.append("abc"); } observeListChange(obs, "arrayOptBool", .insertion) { obj.arrayOptBool.append(true); } observeListChange(obs, "arrayOptInt8", .insertion) { obj.arrayOptInt8.append(10); } observeListChange(obs, "arrayOptInt16", .insertion) { obj.arrayOptInt16.append(10); } observeListChange(obs, "arrayOptInt32", .insertion) { obj.arrayOptInt32.append(10); } observeListChange(obs, "arrayOptInt64", .insertion) { obj.arrayOptInt64.append(10); } observeListChange(obs, "arrayOptFloat", .insertion) { obj.arrayOptFloat.append(10); } observeListChange(obs, "arrayOptDouble", .insertion) { obj.arrayOptDouble.append(10); } observeListChange(obs, "arrayOptString", .insertion) { obj.arrayOptString.append("abc"); } observeListChange(obs, "arrayOptBinary", .insertion) { obj.arrayOptBinary.append(data); } observeListChange(obs, "arrayOptDate", .insertion) { obj.arrayOptDate.append(date); } observeListChange(obs, "arrayOptBool", .insertion) { obj.arrayOptBool.insert(nil, at: 0); } observeListChange(obs, "arrayOptInt8", .insertion) { obj.arrayOptInt8.insert(nil, at: 0); } observeListChange(obs, "arrayOptInt16", .insertion) { obj.arrayOptInt16.insert(nil, at: 0); } observeListChange(obs, "arrayOptInt32", .insertion) { obj.arrayOptInt32.insert(nil, at: 0); } observeListChange(obs, "arrayOptInt64", .insertion) { obj.arrayOptInt64.insert(nil, at: 0); } observeListChange(obs, "arrayOptFloat", .insertion) { obj.arrayOptFloat.insert(nil, at: 0); } observeListChange(obs, "arrayOptDouble", .insertion) { obj.arrayOptDouble.insert(nil, at: 0); } observeListChange(obs, "arrayOptString", .insertion) { obj.arrayOptString.insert(nil, at: 0); } observeListChange(obs, "arrayOptDate", .insertion) { obj.arrayOptDate.insert(nil, at: 0); } observeListChange(obs, "arrayOptBinary", .insertion) { obj.arrayOptBinary.insert(nil, at: 0); } if obs.realm == nil { return } observeChange(obs, "invalidated", false, true) { self.realm.delete(obj) } let (obj2, obs2) = getObject(SwiftKVOObject()) observeChange(obs2, "arrayCol.invalidated", false, true) { self.realm.delete(obj2) } } func testTypedObservation() { let (obj, obs) = getObject(SwiftKVOObject()) observeChange(obs, \.boolCol, false, true) { obj.boolCol = true } observeChange(obs, \.int8Col, 1 as Int8, 10 as Int8) { obj.int8Col = 10 } observeChange(obs, \.int16Col, 2 as Int16, 10 as Int16) { obj.int16Col = 10 } observeChange(obs, \.int32Col, 3 as Int32, 10 as Int32) { obj.int32Col = 10 } observeChange(obs, \.int64Col, 4 as Int64, 10 as Int64) { obj.int64Col = 10 } observeChange(obs, \.floatCol, 5 as Float, 10 as Float) { obj.floatCol = 10 } observeChange(obs, \.doubleCol, 6 as Double, 10 as Double) { obj.doubleCol = 10 } observeChange(obs, \.stringCol, "", "abc") { obj.stringCol = "abc" } let data = "abc".data(using: String.Encoding.utf8, allowLossyConversion: false)! observeChange(obs, \.binaryCol, Data(), data) { obj.binaryCol = data } let date = Date(timeIntervalSince1970: 1) observeChange(obs, \.dateCol, Date(timeIntervalSince1970: 0), date) { obj.dateCol = date } observeChange(obs, \.objectCol, nil, obj) { obj.objectCol = obj } observeChange(obs, \.optStringCol, nil, "abc") { obj.optStringCol = "abc" } observeChange(obs, \.optBinaryCol, nil, data) { obj.optBinaryCol = data } observeChange(obs, \.optDateCol, nil, date) { obj.optDateCol = date } observeChange(obs, \.optStringCol, "abc", nil) { obj.optStringCol = nil } observeChange(obs, \.optBinaryCol, data, nil) { obj.optBinaryCol = nil } observeChange(obs, \.optDateCol, date, nil) { obj.optDateCol = nil } if obs.realm == nil { return } observeChange(obs, \.isInvalidated, false, true) { self.realm.delete(obj) } } func testReadSharedSchemaFromObservedObject() { let obj = SwiftKVOObject() obj.addObserver(self, forKeyPath: "boolCol", options: [.old, .new], context: nil) XCTAssertEqual(type(of: obj).sharedSchema(), SwiftKVOObject.sharedSchema()) obj.removeObserver(self, forKeyPath: "boolCol") } } class KVOPersistedTests: KVOTests { override func getObject(_ obj: SwiftKVOObject) -> (SwiftKVOObject, SwiftKVOObject) { realm.add(obj) return (obj, obj) } } class KVOMultipleAccessorsTests: KVOTests { override func getObject(_ obj: SwiftKVOObject) -> (SwiftKVOObject, SwiftKVOObject) { realm.add(obj) return (obj, realm.object(ofType: SwiftKVOObject.self, forPrimaryKey: obj.pk)!) } }