diff --git a/RocketData.xcodeproj/project.pbxproj b/RocketData.xcodeproj/project.pbxproj index 57454a2..d11c209 100644 --- a/RocketData.xcodeproj/project.pbxproj +++ b/RocketData.xcodeproj/project.pbxproj @@ -11,7 +11,7 @@ 611562DB1CC16B490001F5CE /* CollectionChangeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611562DA1CC16B490001F5CE /* CollectionChangeTests.swift */; }; 6117DDF81CD7A8EB002F57C1 /* BatchDataProviderListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117DDE51CD7A8EB002F57C1 /* BatchDataProviderListener.swift */; }; 6117DDFA1CD7A8EB002F57C1 /* CacheDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117DDE71CD7A8EB002F57C1 /* CacheDelegate.swift */; }; - 6117DDFB1CD7A8EB002F57C1 /* ChangeClock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117DDE81CD7A8EB002F57C1 /* ChangeClock.swift */; }; + 6117DDFB1CD7A8EB002F57C1 /* ChangeTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117DDE81CD7A8EB002F57C1 /* ChangeTime.swift */; }; 6117DDFC1CD7A8EB002F57C1 /* CollectionChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117DDE91CD7A8EB002F57C1 /* CollectionChange.swift */; }; 6117DDFD1CD7A8EB002F57C1 /* CollectionDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117DDEA1CD7A8EB002F57C1 /* CollectionDataProvider.swift */; }; 6117DDFE1CD7A8EB002F57C1 /* ConsistencyContextWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117DDEB1CD7A8EB002F57C1 /* ConsistencyContextWrapper.swift */; }; @@ -66,7 +66,7 @@ 611562DA1CC16B490001F5CE /* CollectionChangeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionChangeTests.swift; sourceTree = ""; }; 6117DDE51CD7A8EB002F57C1 /* BatchDataProviderListener.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatchDataProviderListener.swift; sourceTree = ""; }; 6117DDE71CD7A8EB002F57C1 /* CacheDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheDelegate.swift; sourceTree = ""; }; - 6117DDE81CD7A8EB002F57C1 /* ChangeClock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangeClock.swift; sourceTree = ""; }; + 6117DDE81CD7A8EB002F57C1 /* ChangeTime.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangeTime.swift; sourceTree = ""; }; 6117DDE91CD7A8EB002F57C1 /* CollectionChange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionChange.swift; sourceTree = ""; }; 6117DDEA1CD7A8EB002F57C1 /* CollectionDataProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionDataProvider.swift; sourceTree = ""; }; 6117DDEB1CD7A8EB002F57C1 /* ConsistencyContextWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConsistencyContextWrapper.swift; sourceTree = ""; }; @@ -147,7 +147,7 @@ 6117DDF41CD7A8EB002F57C1 /* ParsingHelpers.swift */, 6117DDF01CD7A8EB002F57C1 /* Errors.swift */, 6117DDF21CD7A8EB002F57C1 /* Logger.swift */, - 6117DDE81CD7A8EB002F57C1 /* ChangeClock.swift */, + 6117DDE81CD7A8EB002F57C1 /* ChangeTime.swift */, 6117DDEB1CD7A8EB002F57C1 /* ConsistencyContextWrapper.swift */, 6117DDED1CD7A8EB002F57C1 /* DataHolder.swift */, 6117DDF61CD7A8EB002F57C1 /* SharedCollectionManager.swift */, @@ -458,7 +458,7 @@ 6117DE041CD7A8EB002F57C1 /* Logger.swift in Sources */, 6117DDF81CD7A8EB002F57C1 /* BatchDataProviderListener.swift in Sources */, 6117DE051CD7A8EB002F57C1 /* Model.swift in Sources */, - 6117DDFB1CD7A8EB002F57C1 /* ChangeClock.swift in Sources */, + 6117DDFB1CD7A8EB002F57C1 /* ChangeTime.swift in Sources */, 6117DDFD1CD7A8EB002F57C1 /* CollectionDataProvider.swift in Sources */, 6117DDFA1CD7A8EB002F57C1 /* CacheDelegate.swift in Sources */, 6117DE091CD7A8EB002F57C1 /* WeakSharedCollectionArray.swift in Sources */, diff --git a/RocketData/ChangeClock.swift b/RocketData/ChangeTime.swift similarity index 76% rename from RocketData/ChangeClock.swift rename to RocketData/ChangeTime.swift index 82f5d26..a04a475 100644 --- a/RocketData/ChangeClock.swift +++ b/RocketData/ChangeTime.swift @@ -12,11 +12,13 @@ import Foundation /** This class keeps a global clock which is used to record when changes happen. Whenever you create a new ChangeTime, it will be after any previous times and before any future times. - It is not thread-safe. You must call it on the main thread. + This is a thread safe class. You can call it from any thread. */ struct ChangeTime: Equatable { /// Keeps track of the last time we updated private static var lastTime = 1 + /// We use this serial queue to sync on different threads + private static let queue = DispatchQueue(label: "com.rocketdata.changeTime") fileprivate let time: Int @@ -24,9 +26,13 @@ struct ChangeTime: Equatable { Creates a new ChangeTime instance. This is guarenteed to be after any previous times created. */ init() { - Log.sharedInstance.assert(Thread.isMainThread, "The ChangeClock was accessed on a different thread than the main thread. This probably means you are accessing something in the library that is not thread-safe on a different thread. This can cause race conditions.") - self.time = ChangeTime.lastTime - ChangeTime.lastTime += 1 + var time = 0 + ChangeTime.queue.sync { + time = ChangeTime.lastTime + ChangeTime.lastTime += 1 + } + Log.sharedInstance.assert(time != 0, "Dispatch sync is behaving incorrectly. This is a bug.") + self.time = time } /** diff --git a/RocketDataTests/DataModelManagerTests.swift b/RocketDataTests/DataModelManagerTests.swift index 5293f6b..3f0eb3b 100644 --- a/RocketDataTests/DataModelManagerTests.swift +++ b/RocketDataTests/DataModelManagerTests.swift @@ -10,6 +10,8 @@ import XCTest import RocketData class DataModelManagerTests: RocketDataTestCase { + + // MARK: Update Model func testUpdateModelWithCache() { let cacheExpectation = expectation(description: "Wait for cache") @@ -67,6 +69,41 @@ class DataModelManagerTests: RocketDataTestCase { XCTAssertEqual(dataProvider[0], newModel) } + func testUpdateModelDifferentThread() { + let cacheExpectation = expectation(description: "Wait for cache") + let cache = ExpectCacheDelegate() + let dataModelManager = DataModelManager(cacheDelegate: cache) + let dataProvider = CollectionDataProvider(dataModelManager: dataModelManager) + + let initialModel = ParentModel(id: 1, name: "initial", requiredChild: ChildModel(), otherChildren: []) + let newModel = ParentModel(id: 1, name: "new", requiredChild: ChildModel(), otherChildren: []) + + let delegate = ClosureCollectionDataProviderDelegate() { (collectionChanges, context) in + XCTAssertEqual(context as? String, "context") + } + dataProvider.delegate = delegate + + cache.setModelCalled = { model, key, context in + XCTAssertEqual(model as? ParentModel, newModel) + XCTAssertEqual(context as? String, "context") + XCTAssertEqual(key, "ParentModel:1") + cacheExpectation.fulfill() + } + + dataProvider.setData([initialModel], cacheKey: nil, context: "wrong") + + DispatchQueue.global(qos: .default).async { + dataModelManager.updateModel(newModel, context: "context") + } + + waitForExpectations(timeout: 10, handler: nil) + waitForConsistencyManagerToFlush(dataModelManager.consistencyManager) + + XCTAssertEqual(dataProvider[0], newModel) + } + + // MARK: Update Models + func testUpdateModelsWithCache() { let cacheExpectation = expectation(description: "Wait for cache") let cache = ExpectCacheDelegate() @@ -133,6 +170,47 @@ class DataModelManagerTests: RocketDataTestCase { XCTAssertEqual(dataProvider[1], otherNewModel) } + func testUpdateModelsDifferentThread() { + let cacheExpectation = expectation(description: "Wait for cache") + let cache = ExpectCacheDelegate() + let dataModelManager = DataModelManager(cacheDelegate: cache) + let dataProvider = CollectionDataProvider(dataModelManager: dataModelManager) + + let initialModel = ParentModel(id: 1, name: "initial", requiredChild: ChildModel(), otherChildren: []) + let otherInitialModel = ParentModel(id: 2, name: "initial", requiredChild: ChildModel(), otherChildren: []) + let newModel = ParentModel(id: 1, name: "new", requiredChild: ChildModel(), otherChildren: []) + let otherNewModel = ParentModel(id: 2, name: "new", requiredChild: ChildModel(), otherChildren: []) + + let delegate = ClosureCollectionDataProviderDelegate() { (collectionChanges, context) in + XCTAssertEqual(context as? String, "context") + } + dataProvider.delegate = delegate + + var setModelCalled = 0 + cache.setModelCalled = { model, key, context in + setModelCalled += 1 + XCTAssertEqual(model as? ParentModel, setModelCalled == 1 ? newModel : otherNewModel) + XCTAssertEqual(context as? String, "context") + XCTAssertEqual(key, "ParentModel:\(setModelCalled)") + if setModelCalled == 2 { + cacheExpectation.fulfill() + } + } + + dataProvider.setData([initialModel, otherInitialModel], cacheKey: nil, context: "wrong") + DispatchQueue.global(qos: .default).async { + dataModelManager.updateModels([newModel, otherNewModel], context: "context") + } + + waitForExpectations(timeout: 10, handler: nil) + waitForConsistencyManagerToFlush(dataModelManager.consistencyManager) + + XCTAssertEqual(dataProvider[0], newModel) + XCTAssertEqual(dataProvider[1], otherNewModel) + } + + // MARK: Compilation Tests + /** Previously, this test failed because of https://bugs.swift.org/browse/SR-3038 This test passes if it compiles.