mirror of
https://github.com/home-assistant/iOS.git
synced 2026-02-13 02:53:00 -06:00
<!-- Thank you for submitting a Pull Request and helping to improve Home Assistant. Please complete the following sections to help the processing and review of your changes. Please do not delete anything from this template. --> ## Summary <!-- Provide a brief summary of the changes you have made and most importantly what they aim to achieve --> This PR will change the approach of how the app keeps it's data updated by not subscribing to all state changes and only periodic updating it's app entities instead. This will avoid several issues where users experience their app to freeze due to 9k entities updating every second and notifying the app. ## Screenshots <!-- If this is a user-facing change not in the frontend, please include screenshots in light and dark mode. --> ## Link to pull request in Documentation repository <!-- Pull requests that add, change or remove functionality must have a corresponding pull request in the Companion App Documentation repository (https://github.com/home-assistant/companion.home-assistant). Please add the number of this pull request after the "#" --> Documentation: home-assistant/companion.home-assistant# ## Any other notes <!-- If there is any other information of note, like if this Pull Request is part of a bigger change, please include it here. -->
901 lines
32 KiB
Swift
901 lines
32 KiB
Swift
import Foundation
|
|
import HAKit
|
|
import PromiseKit
|
|
import RealmSwift
|
|
@testable import Shared
|
|
import XCTest
|
|
|
|
class ModelManagerTests: XCTestCase {
|
|
private var realm: Realm!
|
|
private var testQueue: DispatchQueue!
|
|
private var manager: LegacyModelManager!
|
|
private var servers: FakeServerManager!
|
|
private var api1: FakeHomeAssistantAPI!
|
|
private var api2: FakeHomeAssistantAPI!
|
|
private var apiConnection1: HAMockConnection!
|
|
private var apiConnection2: HAMockConnection!
|
|
|
|
override func setUpWithError() throws {
|
|
try super.setUpWithError()
|
|
|
|
testQueue = DispatchQueue(label: #file)
|
|
manager = LegacyModelManager()
|
|
manager.workQueue = testQueue
|
|
|
|
servers = FakeServerManager(initial: 0)
|
|
let server1 = servers.add(identifier: "s1", serverInfo: .fake())
|
|
let server2 = servers.add(identifier: "s2", serverInfo: .fake())
|
|
api1 = FakeHomeAssistantAPI(server: server1)
|
|
api2 = FakeHomeAssistantAPI(server: server2)
|
|
apiConnection1 = HAMockConnection()
|
|
api1.connection = apiConnection1
|
|
apiConnection2 = HAMockConnection()
|
|
api2.connection = apiConnection2
|
|
Current.servers = servers
|
|
Current.cachedApis = [server1.identifier: api1, server2.identifier: api2]
|
|
|
|
let executionIdentifier = UUID().uuidString
|
|
try testQueue.sync {
|
|
realm = try Realm(configuration: .init(inMemoryIdentifier: executionIdentifier), queue: testQueue)
|
|
Current.realm = { self.realm }
|
|
}
|
|
}
|
|
|
|
override func tearDown() {
|
|
super.tearDown()
|
|
|
|
Current.realm = Realm.live
|
|
TestStoreModel1.lastDidUpdates = []
|
|
TestStoreModel1.lastWillDeleteIds = []
|
|
TestStoreModel1.updateFalseIds = []
|
|
|
|
TestStoreModel3.lastWillDeleteIds = []
|
|
}
|
|
|
|
func testObserve() throws {
|
|
try testQueue.sync {
|
|
let results = AnyRealmCollection(realm.objects(TestStoreModel1.self))
|
|
|
|
let executedExpectation = self.expectation(description: "observed")
|
|
executedExpectation.expectedFulfillmentCount = 2
|
|
|
|
var didObserveCount = 0
|
|
|
|
manager.observe(for: results) { collection -> Promise<Void> in
|
|
XCTAssertEqual(Array(collection), Array(results))
|
|
didObserveCount += 1
|
|
executedExpectation.fulfill()
|
|
return .value(())
|
|
}
|
|
|
|
realm.refresh()
|
|
|
|
XCTAssertEqual(didObserveCount, 0)
|
|
|
|
try realm.write {
|
|
realm.add(with(TestStoreModel1()) {
|
|
$0.identifier = "123"
|
|
$0.value = "456"
|
|
})
|
|
}
|
|
|
|
realm.refresh()
|
|
|
|
XCTAssertEqual(didObserveCount, 1)
|
|
|
|
try realm.write {
|
|
realm.add(with(TestStoreModel1()) {
|
|
$0.identifier = "qrs"
|
|
$0.value = "tuv"
|
|
})
|
|
}
|
|
|
|
realm.refresh()
|
|
|
|
XCTAssertEqual(didObserveCount, 2)
|
|
|
|
wait(for: [executedExpectation], timeout: 10.0)
|
|
}
|
|
}
|
|
|
|
func testCleanupWithoutItems() {
|
|
let promise = manager.cleanup(definitions: [])
|
|
XCTAssertNoThrow(try hang(promise))
|
|
}
|
|
|
|
func testNoneToCleanUp() throws {
|
|
let now = Date()
|
|
Current.date = { now }
|
|
|
|
let models = [
|
|
TestDeleteModel1(now.addingTimeInterval(-1)),
|
|
TestDeleteModel1(now.addingTimeInterval(-2)),
|
|
TestDeleteModel1(now.addingTimeInterval(-3)),
|
|
]
|
|
|
|
try testQueue.sync {
|
|
try realm.write {
|
|
realm.add(models)
|
|
}
|
|
}
|
|
|
|
XCTAssertTrue(models.allSatisfy { $0.realm != nil })
|
|
|
|
let promise = manager.cleanup(
|
|
definitions: [
|
|
.init(
|
|
model: TestDeleteModel1.self,
|
|
createdKey: #keyPath(TestDeleteModel1.createdAt),
|
|
duration: .init(value: 100, unit: .seconds)
|
|
),
|
|
]
|
|
)
|
|
|
|
XCTAssertNoThrow(try hang(promise))
|
|
XCTAssertTrue(models.allSatisfy { !$0.isInvalidated })
|
|
}
|
|
|
|
func testCleanupRemovesOnlyOlder() throws {
|
|
let now = Date()
|
|
|
|
let deletedTimeInterval1: TimeInterval = 100
|
|
let deletedTimeInterval2: TimeInterval = 1000
|
|
|
|
let deletedLimit1 = Date(timeIntervalSinceNow: -deletedTimeInterval1)
|
|
let deletedLimit2 = Date(timeIntervalSinceNow: -deletedTimeInterval2)
|
|
|
|
Current.date = { now }
|
|
|
|
let (expectedExpired, expectedAlive) = try testQueue.sync { () -> (expired: [Object], alive: [Object]) in
|
|
let expired = [
|
|
TestDeleteModel1(deletedLimit1.addingTimeInterval(-1)),
|
|
TestDeleteModel1(deletedLimit1.addingTimeInterval(-100)),
|
|
TestDeleteModel1(deletedLimit1.addingTimeInterval(-1000)),
|
|
TestDeleteModel2(deletedLimit2.addingTimeInterval(-1)),
|
|
TestDeleteModel2(deletedLimit2.addingTimeInterval(-100)),
|
|
TestDeleteModel2(deletedLimit2.addingTimeInterval(-1000)),
|
|
]
|
|
|
|
let alive = [
|
|
// shouldn't be deleted due to time
|
|
TestDeleteModel1(deletedLimit1),
|
|
TestDeleteModel1(deletedLimit1.addingTimeInterval(10)),
|
|
TestDeleteModel1(deletedLimit1.addingTimeInterval(100)),
|
|
TestDeleteModel2(deletedLimit2),
|
|
TestDeleteModel2(deletedLimit2.addingTimeInterval(10)),
|
|
TestDeleteModel2(deletedLimit2.addingTimeInterval(100)),
|
|
// shouldn't be deleted due to not being requested
|
|
TestDeleteModel3(now.addingTimeInterval(-10000)),
|
|
]
|
|
|
|
try realm.write {
|
|
realm.add(expired)
|
|
realm.add(alive)
|
|
}
|
|
|
|
return (expired, alive)
|
|
}
|
|
|
|
XCTAssertTrue(expectedExpired.allSatisfy { $0.realm != nil })
|
|
XCTAssertTrue(expectedAlive.allSatisfy { $0.realm != nil })
|
|
|
|
let promise = manager.cleanup(
|
|
definitions: [
|
|
.init(
|
|
model: TestDeleteModel1.self,
|
|
createdKey: #keyPath(TestDeleteModel1.createdAt),
|
|
duration: .init(value: deletedTimeInterval1, unit: .seconds)
|
|
),
|
|
.init(
|
|
model: TestDeleteModel2.self,
|
|
createdKey: #keyPath(TestDeleteModel2.createdAt),
|
|
duration: .init(value: deletedTimeInterval2, unit: .seconds)
|
|
),
|
|
]
|
|
)
|
|
|
|
XCTAssertNoThrow(try hang(promise))
|
|
XCTAssertTrue(expectedExpired.allSatisfy(\.isInvalidated))
|
|
XCTAssertTrue(expectedAlive.allSatisfy { !$0.isInvalidated })
|
|
}
|
|
|
|
func testCleanupMissingServers() throws {
|
|
let server3 = servers.addFake()
|
|
let api3 = FakeHomeAssistantAPI(server: server3)
|
|
Current.cachedApis[server3.identifier] = api3
|
|
|
|
let start1 = [
|
|
with(TestStoreModel1()) {
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.identifier = "s1m1"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.identifier = "s1m2"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.identifier = "s1m3"
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.identifier = "s1m4"
|
|
$0.value = 1 // not deleted
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.identifier = "s1m5"
|
|
$0.value = 6 // deleted
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.identifier = "s1m6"
|
|
$0.value = 8 // deleted
|
|
},
|
|
]
|
|
let start2 = [
|
|
with(TestStoreModel1()) {
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.identifier = "s2m1"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.identifier = "s2m2"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.identifier = "s2m3"
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.identifier = "s2m4"
|
|
$0.value = 1 // not deleted
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.identifier = "s2m5"
|
|
$0.value = 6 // deleted
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.identifier = "s2m6"
|
|
$0.value = 8 // deleted
|
|
},
|
|
]
|
|
let start3 = [
|
|
with(TestStoreModel1()) {
|
|
$0.serverIdentifier = api3.server.identifier.rawValue
|
|
$0.identifier = "s3m1"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.serverIdentifier = api3.server.identifier.rawValue
|
|
$0.identifier = "s3m2"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.serverIdentifier = api3.server.identifier.rawValue
|
|
$0.identifier = "s3m3"
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.serverIdentifier = api3.server.identifier.rawValue
|
|
$0.identifier = "s3m4"
|
|
$0.value = 1 // not deleted
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.serverIdentifier = api3.server.identifier.rawValue
|
|
$0.identifier = "s3m5"
|
|
$0.value = 6 // deleted
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.serverIdentifier = api3.server.identifier.rawValue
|
|
$0.identifier = "s3m6"
|
|
$0.value = 8 // deleted
|
|
},
|
|
]
|
|
|
|
manager.cleanup(definitions: [
|
|
.init(orphansOf: TestStoreModel1.self),
|
|
.init(orphansOf: TestStoreModel3.self),
|
|
]).cauterize()
|
|
|
|
try testQueue.sync {
|
|
try realm.write {
|
|
realm.add(start1)
|
|
realm.add(start2)
|
|
realm.add(start3)
|
|
}
|
|
}
|
|
|
|
servers.remove(identifier: api2.server.identifier)
|
|
servers.notify()
|
|
|
|
try testQueue.sync {
|
|
let expected = Set(start1 + start3 + [start2[3]])
|
|
let present = Set<Object>(realm.objects(TestStoreModel1.self).map { $0 as Object })
|
|
.union(realm.objects(TestStoreModel3.self).map { $0 as Object })
|
|
XCTAssertEqual(present, expected)
|
|
|
|
XCTAssertEqual(
|
|
try XCTUnwrap(start2[3] as? TestStoreModel3).serverIdentifier,
|
|
api1.server.identifier.rawValue
|
|
)
|
|
}
|
|
|
|
XCTAssertEqual(Set(TestStoreModel1.lastWillDeleteIds.flatMap { $0 }), Set([
|
|
"s2m1", "s2m2", "s2m3",
|
|
]))
|
|
XCTAssertEqual(Set(TestStoreModel3.lastWillDeleteIds.flatMap { $0 }), Set([
|
|
"s2m5", "s2m6",
|
|
]))
|
|
}
|
|
|
|
func testFetchInvokesDefinition() {
|
|
let (fetchPromise1, fetchSeal1) = Promise<Void>.pending()
|
|
let (fetchPromise2, fetchSeal2) = Promise<Void>.pending()
|
|
|
|
var fetchApi1 = [HomeAssistantAPI]()
|
|
var fetchApi2 = [HomeAssistantAPI]()
|
|
|
|
let promise = manager.fetch(definitions: [
|
|
.init(update: { api, queue, manager -> Promise<Void> in
|
|
fetchApi1.append(api)
|
|
XCTAssertEqual(queue, self.testQueue)
|
|
XCTAssertTrue(manager === self.manager)
|
|
return fetchPromise1
|
|
}),
|
|
.init(update: { api, queue, manager -> Promise<Void> in
|
|
fetchApi2.append(api)
|
|
XCTAssertEqual(queue, self.testQueue)
|
|
XCTAssertTrue(manager === self.manager)
|
|
return fetchPromise2
|
|
}),
|
|
], apis: [api1, api2])
|
|
|
|
XCTAssertFalse(promise.isResolved)
|
|
fetchSeal1.fulfill(())
|
|
XCTAssertFalse(promise.isResolved)
|
|
fetchSeal2.fulfill(())
|
|
XCTAssertNoThrow(try hang(promise))
|
|
|
|
XCTAssertEqual(fetchApi1.map(\.server), [api1.server, api2.server])
|
|
XCTAssertEqual(fetchApi2.map(\.server), [api1.server, api2.server])
|
|
}
|
|
|
|
func testSubscribeSubscribes() {
|
|
let handlers1_1: [HAMockCancellable] = Array((0 ... 1).map { _ in HAMockCancellable({}) })
|
|
let handlers2_1: [HAMockCancellable] = Array((0 ... 1).map { _ in HAMockCancellable({}) })
|
|
let handlers1_2: [HAMockCancellable] = Array((0 ... 1).map { _ in HAMockCancellable({}) })
|
|
let handlers2_2: [HAMockCancellable] = Array((0 ... 1).map { _ in HAMockCancellable({}) })
|
|
let handlers1_3: [HAMockCancellable] = Array((0 ... 1).map { _ in HAMockCancellable({}) })
|
|
let handlers2_3: [HAMockCancellable] = Array((0 ... 1).map { _ in HAMockCancellable({}) })
|
|
|
|
var handlers1Iterator = handlers1_1.makeIterator()
|
|
var handlers2Iterator = handlers2_1.makeIterator()
|
|
|
|
var handlers1APIs = [(HAConnection, Server)]()
|
|
var handlers2APIs = [(HAConnection, Server)]()
|
|
|
|
let definitions: [LegacyModelManager.SubscribeDefinition] = [
|
|
.init(subscribe: { connection, server, queue, manager -> [HACancellable] in
|
|
XCTAssertEqual(queue, self.testQueue)
|
|
XCTAssertTrue(manager === self.manager)
|
|
handlers1APIs.append((connection, server))
|
|
return [handlers1Iterator.next()!]
|
|
}),
|
|
.init(subscribe: { connection, server, queue, manager -> [HACancellable] in
|
|
XCTAssertEqual(queue, self.testQueue)
|
|
XCTAssertTrue(manager === self.manager)
|
|
handlers2APIs.append((connection, server))
|
|
return [handlers2Iterator.next()!]
|
|
}),
|
|
]
|
|
|
|
manager.subscribe(definitions: definitions, isAppInForeground: { true })
|
|
|
|
func verify(apis: [HomeAssistantAPI]) {
|
|
XCTAssertEqual(handlers1APIs.map(\.1), apis.map(\.server))
|
|
XCTAssertEqual(handlers2APIs.map(\.1), apis.map(\.server))
|
|
XCTAssertEqual(
|
|
handlers1APIs.map(\.0).map(ObjectIdentifier.init(_:)),
|
|
apis.compactMap(\.connection).map(ObjectIdentifier.init(_:))
|
|
)
|
|
}
|
|
|
|
verify(apis: [api1, api2])
|
|
|
|
XCTAssertTrue(handlers1_1.allSatisfy { !$0.wasCancelled })
|
|
XCTAssertTrue(handlers2_1.allSatisfy { !$0.wasCancelled })
|
|
|
|
handlers1Iterator = handlers1_2.makeIterator()
|
|
handlers2Iterator = handlers2_2.makeIterator()
|
|
|
|
handlers1APIs.removeAll()
|
|
handlers2APIs.removeAll()
|
|
|
|
manager.subscribe(definitions: definitions, isAppInForeground: { true })
|
|
|
|
XCTAssertTrue(handlers1_1.allSatisfy(\.wasCancelled))
|
|
XCTAssertTrue(handlers2_1.allSatisfy(\.wasCancelled))
|
|
|
|
XCTAssertTrue(handlers1_2.allSatisfy { !$0.wasCancelled })
|
|
XCTAssertTrue(handlers2_2.allSatisfy { !$0.wasCancelled })
|
|
|
|
verify(apis: [api1, api2])
|
|
|
|
servers.remove(identifier: api1.server.identifier)
|
|
let new = servers.addFake()
|
|
let newApi = FakeHomeAssistantAPI(server: new)
|
|
Current.cachedApis[new.identifier] = newApi
|
|
|
|
handlers1Iterator = handlers1_3.makeIterator()
|
|
handlers2Iterator = handlers2_3.makeIterator()
|
|
|
|
handlers1APIs.removeAll()
|
|
handlers2APIs.removeAll()
|
|
|
|
servers.notify()
|
|
|
|
XCTAssertTrue(handlers1_2.allSatisfy(\.wasCancelled))
|
|
XCTAssertTrue(handlers2_2.allSatisfy(\.wasCancelled))
|
|
|
|
verify(apis: [api2, newApi])
|
|
}
|
|
|
|
func testStoreWithoutModels() throws {
|
|
try testQueue.sync {
|
|
try hang(manager.store(type: TestStoreModel1.self, from: api1.server, sourceModels: []))
|
|
XCTAssertTrue(realm.objects(TestStoreModel1.self).isEmpty)
|
|
}
|
|
}
|
|
|
|
func testStoreWithModelLackingPrimaryKey() throws {
|
|
func doStore() throws {
|
|
try testQueue.sync {
|
|
try hang(manager.store(type: TestStoreModel2.self, from: api1.server, sourceModels: []))
|
|
}
|
|
}
|
|
|
|
XCTAssertThrowsError(try doStore()) { error in
|
|
XCTAssertEqual(error as? LegacyModelManager.StoreError, .missingPrimaryKey)
|
|
}
|
|
}
|
|
|
|
func testStoreWithoutExistingObjects() throws {
|
|
try testQueue.sync {
|
|
let sources1: [TestStoreSource1] = [
|
|
.init(id: "id1s1", value: "val1"),
|
|
.init(id: "id2s1", value: "val2"),
|
|
]
|
|
let sources2: [TestStoreSource1] = [
|
|
.init(id: "id1s2", value: "val1"),
|
|
.init(id: "id2s2", value: "val2"),
|
|
]
|
|
|
|
try hang(manager.store(type: TestStoreModel1.self, from: api1.server, sourceModels: sources1))
|
|
try hang(manager.store(type: TestStoreModel1.self, from: api2.server, sourceModels: sources2))
|
|
let models = realm.objects(TestStoreModel1.self).sorted(byKeyPath: #keyPath(TestStoreModel1.identifier))
|
|
XCTAssertEqual(models.count, 4)
|
|
XCTAssertEqual(models[0].identifier, "s1/id1s1")
|
|
XCTAssertEqual(models[0].serverIdentifier, api1.server.identifier.rawValue)
|
|
XCTAssertEqual(models[0].value, "val1")
|
|
XCTAssertEqual(models[1].identifier, "s1/id2s1")
|
|
XCTAssertEqual(models[1].serverIdentifier, api1.server.identifier.rawValue)
|
|
XCTAssertEqual(models[1].value, "val2")
|
|
XCTAssertEqual(models[2].identifier, "s2/id1s2")
|
|
XCTAssertEqual(models[2].serverIdentifier, api2.server.identifier.rawValue)
|
|
XCTAssertEqual(models[2].value, "val1")
|
|
XCTAssertEqual(models[3].identifier, "s2/id2s2")
|
|
XCTAssertEqual(models[3].serverIdentifier, api2.server.identifier.rawValue)
|
|
XCTAssertEqual(models[3].value, "val2")
|
|
XCTAssertEqual(Set(TestStoreModel1.lastDidUpdates.flatMap { $0 }), Set(models))
|
|
}
|
|
}
|
|
|
|
func testStoreUpdatesAndDeletes() throws {
|
|
let start1 = [
|
|
with(TestStoreModel1()) {
|
|
$0.identifier = "s1/start_id1s1"
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.value = "start_val1"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.identifier = "s1/start_id2s1"
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.value = "start_val2"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.identifier = "s1/start_id3s1"
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.value = "start_val3"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.identifier = "s1/start_id4s1"
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.value = "start_val4"
|
|
},
|
|
]
|
|
let start2 = [
|
|
with(TestStoreModel1()) {
|
|
$0.identifier = "s2/start_id1s2"
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.value = "start_val1"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.identifier = "s2/start_id2s2"
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.value = "start_val2"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.identifier = "s2/start_id3s2"
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.value = "start_val3"
|
|
},
|
|
with(TestStoreModel1()) {
|
|
$0.identifier = "s2/start_id4s2"
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.value = "start_val4"
|
|
},
|
|
]
|
|
|
|
let insertedSources1 = [
|
|
TestStoreSource1(id: "ins_id1s1", value: "ins_val1"),
|
|
TestStoreSource1(id: "ins_id2s1", value: "ins_val2"),
|
|
]
|
|
let insertedSources2 = [
|
|
TestStoreSource1(id: "ins_id1s2", value: "ins_val1"),
|
|
TestStoreSource1(id: "ins_id2s2", value: "ins_val2"),
|
|
]
|
|
|
|
let updatedSources1 = [
|
|
TestStoreSource1(id: "start_id1s1", value: "start_val1-2"),
|
|
TestStoreSource1(id: "start_id2s1", value: "start_val2-2"),
|
|
]
|
|
let updatedSources2 = [
|
|
TestStoreSource1(id: "start_id1s2", value: "start_val1-2"),
|
|
TestStoreSource1(id: "start_id2s2", value: "start_val2-2"),
|
|
]
|
|
|
|
try testQueue.sync {
|
|
try realm.write {
|
|
realm.add(start1)
|
|
realm.add(start2)
|
|
}
|
|
|
|
try hang(manager.store(
|
|
type: TestStoreModel1.self,
|
|
from: api1.server,
|
|
sourceModels: insertedSources1 + updatedSources1
|
|
))
|
|
try hang(manager.store(
|
|
type: TestStoreModel1.self,
|
|
from: api2.server,
|
|
sourceModels: insertedSources2 + updatedSources2
|
|
))
|
|
let models = realm.objects(TestStoreModel1.self).sorted(byKeyPath: #keyPath(TestStoreModel1.value))
|
|
XCTAssertEqual(models.count, 8)
|
|
|
|
// inserted
|
|
XCTAssertEqual(models[0].identifier, "s1/ins_id1s1")
|
|
XCTAssertEqual(models[0].value, "ins_val1")
|
|
XCTAssertEqual(models[1].identifier, "s2/ins_id1s2")
|
|
XCTAssertEqual(models[1].value, "ins_val1")
|
|
XCTAssertEqual(models[2].identifier, "s1/ins_id2s1")
|
|
XCTAssertEqual(models[2].value, "ins_val2")
|
|
XCTAssertEqual(models[3].identifier, "s2/ins_id2s2")
|
|
XCTAssertEqual(models[3].value, "ins_val2")
|
|
|
|
// updated
|
|
XCTAssertEqual(models[4].identifier, "s1/start_id1s1")
|
|
XCTAssertEqual(models[4].value, "start_val1-2")
|
|
XCTAssertEqual(models[5].identifier, "s2/start_id1s2")
|
|
XCTAssertEqual(models[5].value, "start_val1-2")
|
|
XCTAssertEqual(models[6].identifier, "s1/start_id2s1")
|
|
XCTAssertEqual(models[6].value, "start_val2-2")
|
|
XCTAssertEqual(models[7].identifier, "s2/start_id2s2")
|
|
XCTAssertEqual(models[7].value, "start_val2-2")
|
|
|
|
// deleted
|
|
XCTAssertEqual(Set(TestStoreModel1.lastWillDeleteIds.flatMap { $0 }), Set([
|
|
"s1/start_id3s1",
|
|
"s1/start_id4s1",
|
|
"s2/start_id3s2",
|
|
"s2/start_id4s2",
|
|
]))
|
|
}
|
|
}
|
|
|
|
func testIneligibleNotDeleted() throws {
|
|
let start1 = [
|
|
with(TestStoreModel3()) {
|
|
$0.identifier = "s1/start_id1s1"
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.value = 10 // eligible
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.identifier = "s1/start_id2s1"
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.value = 1 // not eligible
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.identifier = "s1/start_id3s1"
|
|
$0.serverIdentifier = api1.server.identifier.rawValue
|
|
$0.value = 100 // eligible, will be deleted
|
|
},
|
|
]
|
|
let start2 = [
|
|
with(TestStoreModel3()) {
|
|
$0.identifier = "s2/start_id1s2"
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.value = 10 // eligible
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.identifier = "s2/start_id2s2"
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.value = 1 // not eligible
|
|
},
|
|
with(TestStoreModel3()) {
|
|
$0.identifier = "s2/start_id3s2"
|
|
$0.serverIdentifier = api2.server.identifier.rawValue
|
|
$0.value = 100 // eligible, will be deleted
|
|
},
|
|
]
|
|
|
|
let insertedSources1 = [
|
|
TestStoreSource2(id: "ins_id1s1", value: 100),
|
|
]
|
|
let insertedSources2 = [
|
|
TestStoreSource2(id: "ins_id1s2", value: 100),
|
|
]
|
|
|
|
let updatedSources1 = [
|
|
TestStoreSource2(id: "start_id1s1", value: 4),
|
|
]
|
|
let updatedSources2 = [
|
|
TestStoreSource2(id: "start_id1s2", value: 4),
|
|
]
|
|
|
|
try testQueue.sync {
|
|
try realm.write {
|
|
realm.add(start1)
|
|
realm.add(start2)
|
|
}
|
|
|
|
try hang(manager.store(
|
|
type: TestStoreModel3.self,
|
|
from: api1.server,
|
|
sourceModels: insertedSources1 + updatedSources1
|
|
))
|
|
try hang(manager.store(
|
|
type: TestStoreModel3.self,
|
|
from: api2.server,
|
|
sourceModels: insertedSources2 + updatedSources2
|
|
))
|
|
let models = realm.objects(TestStoreModel3.self).sorted(byKeyPath: #keyPath(TestStoreModel3.value))
|
|
XCTAssertEqual(models.count, 6)
|
|
|
|
XCTAssertEqual(models[0].identifier, "s1/start_id2s1")
|
|
XCTAssertEqual(models[0].value, 1)
|
|
XCTAssertEqual(models[1].identifier, "s2/start_id2s2")
|
|
XCTAssertEqual(models[1].value, 1)
|
|
XCTAssertEqual(models[2].identifier, "s1/start_id1s1")
|
|
XCTAssertEqual(models[2].value, 4)
|
|
XCTAssertEqual(models[3].identifier, "s2/start_id1s2")
|
|
XCTAssertEqual(models[3].value, 4)
|
|
XCTAssertEqual(models[4].identifier, "s1/ins_id1s1")
|
|
XCTAssertEqual(models[4].value, 100)
|
|
XCTAssertEqual(models[5].identifier, "s2/ins_id1s2")
|
|
XCTAssertEqual(models[5].value, 100)
|
|
}
|
|
}
|
|
|
|
func testUpdateFalseSkipsNewCreation() throws {
|
|
try testQueue.sync {
|
|
let sources: [TestStoreSource1] = [
|
|
.init(id: "id1", value: "val1"),
|
|
.init(id: "id2", value: "val2"),
|
|
]
|
|
|
|
TestStoreModel1.updateFalseIds = ["id2"]
|
|
|
|
try hang(manager.store(type: TestStoreModel1.self, from: api1.server, sourceModels: sources))
|
|
let models = realm.objects(TestStoreModel1.self).sorted(byKeyPath: #keyPath(TestStoreModel1.identifier))
|
|
XCTAssertEqual(models.count, 1)
|
|
XCTAssertEqual(models[0].identifier, "s1/id1")
|
|
XCTAssertEqual(models[0].value, "val1")
|
|
XCTAssertEqual(Set(TestStoreModel1.lastDidUpdates.flatMap { $0 }), Set(models))
|
|
}
|
|
}
|
|
}
|
|
|
|
class TestDeleteModel1: Object {
|
|
@objc dynamic var identifier: String = UUID().uuidString
|
|
@objc dynamic var createdAt: Date
|
|
|
|
init(_ createdAt: Date) {
|
|
self.createdAt = createdAt
|
|
}
|
|
|
|
override required init() {
|
|
self.createdAt = Date()
|
|
super.init()
|
|
}
|
|
|
|
override class func primaryKey() -> String? {
|
|
#keyPath(TestDeleteModel1.identifier)
|
|
}
|
|
}
|
|
|
|
class TestDeleteModel2: Object {
|
|
@objc dynamic var identifier: String = UUID().uuidString
|
|
@objc dynamic var createdAt: Date
|
|
|
|
init(_ createdAt: Date) {
|
|
self.createdAt = createdAt
|
|
}
|
|
|
|
override required init() {
|
|
self.createdAt = Date()
|
|
super.init()
|
|
}
|
|
|
|
override class func primaryKey() -> String? {
|
|
#keyPath(TestDeleteModel2.identifier)
|
|
}
|
|
}
|
|
|
|
class TestDeleteModel3: Object {
|
|
@objc dynamic var identifier: String = UUID().uuidString
|
|
@objc dynamic var createdAt: Date
|
|
|
|
init(_ createdAt: Date) {
|
|
self.createdAt = createdAt
|
|
super.init()
|
|
}
|
|
|
|
override required init() {
|
|
self.createdAt = Date()
|
|
super.init()
|
|
}
|
|
|
|
override class func primaryKey() -> String? {
|
|
#keyPath(TestDeleteModel3.identifier)
|
|
}
|
|
}
|
|
|
|
final class TestStoreModel1: Object, UpdatableModel {
|
|
static var updateFalseIds = [String]()
|
|
|
|
static var lastDidUpdates: [[TestStoreModel1]] = []
|
|
static var lastWillDeleteIds: [[String]] = []
|
|
static func didUpdate(objects: [TestStoreModel1], server: Server, realm: Realm) {
|
|
lastDidUpdates.append(objects)
|
|
}
|
|
|
|
static func willDelete(objects: [TestStoreModel1], server: Server?, realm: Realm) {
|
|
lastWillDeleteIds.append(objects.compactMap(\.identifier))
|
|
}
|
|
|
|
@objc dynamic var identifier: String?
|
|
@objc dynamic var serverIdentifier: String?
|
|
@objc dynamic var value: String?
|
|
|
|
static func primaryKey(sourceIdentifier: String, serverIdentifier: String) -> String {
|
|
serverIdentifier + "/" + sourceIdentifier
|
|
}
|
|
|
|
override class func primaryKey() -> String? {
|
|
#keyPath(TestStoreModel1.identifier)
|
|
}
|
|
|
|
static func serverIdentifierKey() -> String {
|
|
#keyPath(TestStoreModel1.serverIdentifier)
|
|
}
|
|
|
|
func update(
|
|
with object: TestStoreSource1,
|
|
server: Server,
|
|
using realm: Realm
|
|
) -> Bool {
|
|
if self.realm == nil {
|
|
serverIdentifier = server.identifier.rawValue
|
|
} else {
|
|
XCTAssertEqual(serverIdentifier, server.identifier.rawValue)
|
|
}
|
|
value = object.value
|
|
|
|
if Self.updateFalseIds.contains(object.id) {
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
struct TestStoreSource1: UpdatableModelSource {
|
|
var primaryKey: String { id }
|
|
|
|
var id: String = UUID().uuidString
|
|
var value: String?
|
|
}
|
|
|
|
final class TestStoreModel2: Object, UpdatableModel {
|
|
static func didUpdate(objects: [TestStoreModel2], server: Server, realm: Realm) {}
|
|
|
|
static func willDelete(objects: [TestStoreModel2], server: Server?, realm: Realm) {}
|
|
|
|
@objc dynamic var identifier: String?
|
|
@objc dynamic var serverIdentifier: String?
|
|
|
|
static func primaryKey(sourceIdentifier: String, serverIdentifier: String) -> String {
|
|
serverIdentifier + "/" + sourceIdentifier
|
|
}
|
|
|
|
override class func primaryKey() -> String? {
|
|
nil
|
|
}
|
|
|
|
static func serverIdentifierKey() -> String {
|
|
#keyPath(TestStoreModel2.serverIdentifier)
|
|
}
|
|
|
|
func update(
|
|
with object: TestStoreSource1,
|
|
server: Server,
|
|
using realm: Realm
|
|
) -> Bool {
|
|
XCTFail("not expected to be called in error scenario")
|
|
return false
|
|
}
|
|
}
|
|
|
|
struct TestStoreSource2: UpdatableModelSource {
|
|
var primaryKey: String { id }
|
|
|
|
var id: String = UUID().uuidString
|
|
var value: Int = 0
|
|
}
|
|
|
|
final class TestStoreModel3: Object, UpdatableModel {
|
|
static func didUpdate(objects: [TestStoreModel3], server: Server, realm: Realm) {}
|
|
|
|
static var lastWillDeleteIds: [[String]] = []
|
|
static func willDelete(objects: [TestStoreModel3], server: Server?, realm: Realm) {
|
|
lastWillDeleteIds.append(objects.compactMap(\.identifier))
|
|
}
|
|
|
|
static var updateEligiblePredicate: NSPredicate {
|
|
.init(format: "value > 5")
|
|
}
|
|
|
|
@objc dynamic var identifier: String?
|
|
@objc dynamic var serverIdentifier: String?
|
|
@objc dynamic var value: Int = 0
|
|
|
|
static func primaryKey(sourceIdentifier: String, serverIdentifier: String) -> String {
|
|
serverIdentifier + "/" + sourceIdentifier
|
|
}
|
|
|
|
override class func primaryKey() -> String? {
|
|
"identifier"
|
|
}
|
|
|
|
static func serverIdentifierKey() -> String {
|
|
#keyPath(TestStoreModel3.serverIdentifier)
|
|
}
|
|
|
|
func update(
|
|
with object: TestStoreSource2,
|
|
server: Server,
|
|
using realm: Realm
|
|
) -> Bool {
|
|
if self.realm == nil {
|
|
serverIdentifier = server.identifier.rawValue
|
|
} else {
|
|
XCTAssertEqual(serverIdentifier, server.identifier.rawValue)
|
|
}
|
|
value = object.value
|
|
return true
|
|
}
|
|
}
|
|
|
|
private class FakeHomeAssistantAPI: HomeAssistantAPI {}
|