From 729b8d176c75ecc0cbbd137cc6811116a64e310a Mon Sep 17 00:00:00 2001 From: Gil Date: Wed, 31 Jan 2018 11:23:55 -0800 Subject: Move all Firestore Objective-C to Objective-C++ (#734) * Move all Firestore files to Objective-C++ * Update project file references * Don't use module imports from Objective-C++ * Use extern "C" for C-accessible globals * Work around more stringent type checking in Objective-C++ * NSMutableDictionary ivars aren't implicitly casted to NSDictionary * FSTMaybeDocument callback can't be passed a function that accepts FSTDocument * NSComparisonResult can't be multiplied by -1 without casting * Add a #include where needed * Avoid using C++ keywords as variables * Remove #if __cplusplus guards --- .../Example/Firestore.xcodeproj/project.pbxproj | 666 ++++++------ .../Tests/API/FIRCollectionReferenceTests.m | 43 - .../Tests/API/FIRCollectionReferenceTests.mm | 43 + .../Example/Tests/API/FIRDocumentReferenceTests.m | 43 - .../Example/Tests/API/FIRDocumentReferenceTests.mm | 43 + .../Example/Tests/API/FIRDocumentSnapshotTests.m | 59 -- .../Example/Tests/API/FIRDocumentSnapshotTests.mm | 59 ++ Firestore/Example/Tests/API/FIRFieldPathTests.m | 46 - Firestore/Example/Tests/API/FIRFieldPathTests.mm | 46 + Firestore/Example/Tests/API/FIRFieldValueTests.m | 46 - Firestore/Example/Tests/API/FIRFieldValueTests.mm | 46 + Firestore/Example/Tests/API/FIRGeoPointTests.m | 68 -- Firestore/Example/Tests/API/FIRGeoPointTests.mm | 68 ++ .../Example/Tests/API/FIRQuerySnapshotTests.m | 58 -- .../Example/Tests/API/FIRQuerySnapshotTests.mm | 58 ++ Firestore/Example/Tests/API/FIRQueryTests.m | 85 -- Firestore/Example/Tests/API/FIRQueryTests.mm | 85 ++ .../Example/Tests/API/FIRSnapshotMetadataTests.m | 52 - .../Example/Tests/API/FIRSnapshotMetadataTests.mm | 52 + Firestore/Example/Tests/API/FSTAPIHelpers.m | 115 --- Firestore/Example/Tests/API/FSTAPIHelpers.mm | 115 +++ .../Example/Tests/Core/FSTDatabaseInfoTests.m | 59 -- .../Example/Tests/Core/FSTDatabaseInfoTests.mm | 59 ++ .../Example/Tests/Core/FSTEventManagerTests.m | 163 --- .../Example/Tests/Core/FSTEventManagerTests.mm | 163 +++ .../Example/Tests/Core/FSTQueryListenerTests.m | 487 --------- .../Example/Tests/Core/FSTQueryListenerTests.mm | 487 +++++++++ Firestore/Example/Tests/Core/FSTQueryTests.m | 566 ---------- Firestore/Example/Tests/Core/FSTQueryTests.mm | 566 ++++++++++ Firestore/Example/Tests/Core/FSTTimestampTests.m | 88 -- Firestore/Example/Tests/Core/FSTTimestampTests.mm | 88 ++ Firestore/Example/Tests/Core/FSTViewSnapshotTest.m | 141 --- .../Example/Tests/Core/FSTViewSnapshotTest.mm | 141 +++ Firestore/Example/Tests/Core/FSTViewTests.m | 618 ----------- Firestore/Example/Tests/Core/FSTViewTests.mm | 618 +++++++++++ .../Example/Tests/Integration/API/FIRCursorTests.m | 195 ---- .../Tests/Integration/API/FIRCursorTests.mm | 195 ++++ .../Tests/Integration/API/FIRDatabaseTests.m | 964 ----------------- .../Tests/Integration/API/FIRDatabaseTests.mm | 963 +++++++++++++++++ .../Example/Tests/Integration/API/FIRFieldsTests.m | 223 ---- .../Tests/Integration/API/FIRFieldsTests.mm | 223 ++++ .../Integration/API/FIRListenerRegistrationTests.m | 131 --- .../API/FIRListenerRegistrationTests.mm | 131 +++ .../Example/Tests/Integration/API/FIRQueryTests.m | 301 ------ .../Example/Tests/Integration/API/FIRQueryTests.mm | 301 ++++++ .../Integration/API/FIRServerTimestampTests.m | 318 ------ .../Integration/API/FIRServerTimestampTests.mm | 318 ++++++ .../Example/Tests/Integration/API/FIRTypeTests.m | 75 -- .../Example/Tests/Integration/API/FIRTypeTests.mm | 75 ++ .../Tests/Integration/API/FIRValidationTests.m | 615 ----------- .../Tests/Integration/API/FIRValidationTests.mm | 615 +++++++++++ .../Tests/Integration/API/FIRWriteBatchTests.m | 336 ------ .../Tests/Integration/API/FIRWriteBatchTests.mm | 336 ++++++ .../Example/Tests/Integration/FSTDatastoreTests.m | 241 ----- .../Example/Tests/Integration/FSTDatastoreTests.mm | 241 +++++ .../Example/Tests/Integration/FSTSmokeTests.m | 129 --- .../Example/Tests/Integration/FSTSmokeTests.mm | 129 +++ .../Example/Tests/Integration/FSTStreamTests.m | 312 ------ .../Example/Tests/Integration/FSTStreamTests.mm | 312 ++++++ .../Tests/Integration/FSTTransactionTests.m | 541 ---------- .../Tests/Integration/FSTTransactionTests.mm | 541 ++++++++++ .../Tests/Local/FSTEagerGarbageCollectorTests.m | 111 -- .../Tests/Local/FSTEagerGarbageCollectorTests.mm | 111 ++ .../Tests/Local/FSTLevelDBLocalStoreTests.m | 45 - .../Tests/Local/FSTLevelDBLocalStoreTests.mm | 45 + .../Tests/Local/FSTLevelDBQueryCacheTests.m | 54 - .../Tests/Local/FSTLevelDBQueryCacheTests.mm | 54 + .../Example/Tests/Local/FSTLocalSerializerTests.m | 183 ---- .../Example/Tests/Local/FSTLocalSerializerTests.mm | 183 ++++ Firestore/Example/Tests/Local/FSTLocalStoreTests.m | 794 -------------- .../Example/Tests/Local/FSTLocalStoreTests.mm | 794 ++++++++++++++ .../Example/Tests/Local/FSTMemoryLocalStoreTests.m | 44 - .../Tests/Local/FSTMemoryLocalStoreTests.mm | 44 + .../Tests/Local/FSTMemoryMutationQueueTests.m | 42 - .../Tests/Local/FSTMemoryMutationQueueTests.mm | 42 + .../Example/Tests/Local/FSTMemoryQueryCacheTests.m | 54 - .../Tests/Local/FSTMemoryQueryCacheTests.mm | 54 + .../Local/FSTMemoryRemoteDocumentCacheTests.m | 49 - .../Local/FSTMemoryRemoteDocumentCacheTests.mm | 49 + .../Example/Tests/Local/FSTMutationQueueTests.m | 511 --------- .../Example/Tests/Local/FSTMutationQueueTests.mm | 511 +++++++++ .../Tests/Local/FSTPersistenceTestHelpers.m | 77 -- .../Tests/Local/FSTPersistenceTestHelpers.mm | 77 ++ Firestore/Example/Tests/Local/FSTQueryCacheTests.m | 433 -------- .../Example/Tests/Local/FSTQueryCacheTests.mm | 433 ++++++++ .../Example/Tests/Local/FSTReferenceSetTests.m | 85 -- .../Example/Tests/Local/FSTReferenceSetTests.mm | 85 ++ .../Tests/Local/FSTRemoteDocumentCacheTests.m | 151 --- .../Tests/Local/FSTRemoteDocumentCacheTests.mm | 151 +++ .../Local/FSTRemoteDocumentChangeBufferTests.m | 113 -- .../Local/FSTRemoteDocumentChangeBufferTests.mm | 113 ++ Firestore/Example/Tests/Model/FSTDatabaseIDTests.m | 45 - .../Example/Tests/Model/FSTDatabaseIDTests.mm | 45 + .../Example/Tests/Model/FSTDocumentKeyTests.m | 60 -- .../Example/Tests/Model/FSTDocumentKeyTests.mm | 60 ++ .../Example/Tests/Model/FSTDocumentSetTests.m | 142 --- .../Example/Tests/Model/FSTDocumentSetTests.mm | 142 +++ Firestore/Example/Tests/Model/FSTDocumentTests.m | 94 -- Firestore/Example/Tests/Model/FSTDocumentTests.mm | 94 ++ Firestore/Example/Tests/Model/FSTFieldValueTests.m | 582 ----------- .../Example/Tests/Model/FSTFieldValueTests.mm | 582 +++++++++++ Firestore/Example/Tests/Model/FSTMutationTests.m | 231 ----- Firestore/Example/Tests/Model/FSTMutationTests.mm | 231 +++++ Firestore/Example/Tests/Model/FSTPathTests.m | 196 ---- Firestore/Example/Tests/Model/FSTPathTests.mm | 196 ++++ Firestore/Example/Tests/Remote/FSTDatastoreTests.m | 58 -- .../Example/Tests/Remote/FSTDatastoreTests.mm | 58 ++ .../Example/Tests/Remote/FSTRemoteEventTests.m | 556 ---------- .../Example/Tests/Remote/FSTRemoteEventTests.mm | 556 ++++++++++ .../Example/Tests/Remote/FSTSerializerBetaTests.m | 801 --------------- .../Example/Tests/Remote/FSTSerializerBetaTests.mm | 800 ++++++++++++++ .../Example/Tests/Remote/FSTWatchChange+Testing.m | 54 - .../Example/Tests/Remote/FSTWatchChange+Testing.mm | 54 + .../Example/Tests/Remote/FSTWatchChangeTests.m | 66 -- .../Example/Tests/Remote/FSTWatchChangeTests.mm | 66 ++ .../Example/Tests/SpecTests/FSTLevelDBSpecTests.m | 43 - .../Example/Tests/SpecTests/FSTLevelDBSpecTests.mm | 43 + .../Example/Tests/SpecTests/FSTMemorySpecTests.m | 42 - .../Example/Tests/SpecTests/FSTMemorySpecTests.mm | 42 + .../Example/Tests/SpecTests/FSTMockDatastore.m | 380 ------- .../Example/Tests/SpecTests/FSTMockDatastore.mm | 380 +++++++ Firestore/Example/Tests/SpecTests/FSTSpecTests.m | 665 ------------ Firestore/Example/Tests/SpecTests/FSTSpecTests.mm | 665 ++++++++++++ .../Tests/SpecTests/FSTSyncEngineTestDriver.m | 317 ------ .../Tests/SpecTests/FSTSyncEngineTestDriver.mm | 322 ++++++ Firestore/Example/Tests/Util/FSTAssertTests.m | 105 -- Firestore/Example/Tests/Util/FSTAssertTests.mm | 105 ++ Firestore/Example/Tests/Util/FSTEventAccumulator.m | 93 -- .../Example/Tests/Util/FSTEventAccumulator.mm | 93 ++ Firestore/Example/Tests/Util/FSTHelpers.m | 350 ------- Firestore/Example/Tests/Util/FSTHelpers.mm | 352 +++++++ .../Example/Tests/Util/FSTTestDispatchQueue.m | 61 -- .../Example/Tests/Util/FSTTestDispatchQueue.mm | 61 ++ Firestore/Example/Tests/Util/XCTestCase+Await.m | 46 - Firestore/Example/Tests/Util/XCTestCase+Await.mm | 46 + Firestore/Source/API/FIRDocumentChange.m | 129 --- Firestore/Source/API/FIRDocumentChange.mm | 129 +++ Firestore/Source/API/FIRDocumentReference.m | 311 ------ Firestore/Source/API/FIRDocumentReference.mm | 311 ++++++ Firestore/Source/API/FIRDocumentSnapshot.m | 252 ----- Firestore/Source/API/FIRDocumentSnapshot.mm | 252 +++++ Firestore/Source/API/FIRFieldPath.m | 101 -- Firestore/Source/API/FIRFieldPath.mm | 101 ++ Firestore/Source/API/FIRFieldValue.m | 96 -- Firestore/Source/API/FIRFieldValue.mm | 96 ++ Firestore/Source/API/FIRFirestore.m | 317 ------ Firestore/Source/API/FIRFirestore.mm | 317 ++++++ Firestore/Source/API/FIRFirestoreSettings.m | 92 -- Firestore/Source/API/FIRFirestoreSettings.mm | 92 ++ Firestore/Source/API/FIRFirestoreVersion.m | 29 - Firestore/Source/API/FIRFirestoreVersion.mm | 29 + Firestore/Source/API/FIRListenerRegistration.m | 59 -- Firestore/Source/API/FIRListenerRegistration.mm | 59 ++ Firestore/Source/API/FIRQuery.m | 633 ------------ Firestore/Source/API/FIRQuery.mm | 633 ++++++++++++ Firestore/Source/API/FIRQuerySnapshot.m | 151 --- Firestore/Source/API/FIRQuerySnapshot.mm | 151 +++ Firestore/Source/API/FIRSetOptions.m | 63 -- Firestore/Source/API/FIRSetOptions.mm | 63 ++ Firestore/Source/API/FIRSnapshotMetadata.m | 70 -- Firestore/Source/API/FIRSnapshotMetadata.mm | 70 ++ Firestore/Source/API/FIRSnapshotOptions.m | 72 -- Firestore/Source/API/FIRSnapshotOptions.mm | 72 ++ Firestore/Source/API/FIRTransaction.m | 148 --- Firestore/Source/API/FIRTransaction.mm | 148 +++ Firestore/Source/API/FIRWriteBatch.m | 121 --- Firestore/Source/API/FIRWriteBatch.mm | 121 +++ Firestore/Source/API/FSTUserDataConverter.m | 598 ----------- Firestore/Source/API/FSTUserDataConverter.mm | 598 +++++++++++ Firestore/Source/Auth/FSTCredentialsProvider.m | 164 --- Firestore/Source/Auth/FSTCredentialsProvider.mm | 164 +++ .../Source/Auth/FSTEmptyCredentialsProvider.m | 47 - .../Source/Auth/FSTEmptyCredentialsProvider.mm | 47 + Firestore/Source/Auth/FSTUser.m | 68 -- Firestore/Source/Auth/FSTUser.mm | 68 ++ Firestore/Source/Core/FSTDatabaseInfo.m | 70 -- Firestore/Source/Core/FSTDatabaseInfo.mm | 70 ++ Firestore/Source/Core/FSTEventManager.m | 335 ------ Firestore/Source/Core/FSTEventManager.mm | 335 ++++++ Firestore/Source/Core/FSTFirestoreClient.m | 298 ------ Firestore/Source/Core/FSTFirestoreClient.mm | 298 ++++++ Firestore/Source/Core/FSTListenSequence.m | 50 - Firestore/Source/Core/FSTListenSequence.mm | 50 + Firestore/Source/Core/FSTQuery.m | 759 -------------- Firestore/Source/Core/FSTQuery.mm | 771 ++++++++++++++ Firestore/Source/Core/FSTSnapshotVersion.m | 80 -- Firestore/Source/Core/FSTSnapshotVersion.mm | 80 ++ Firestore/Source/Core/FSTTransaction.m | 250 ----- Firestore/Source/Core/FSTTransaction.mm | 250 +++++ Firestore/Source/Core/FSTView.m | 479 --------- Firestore/Source/Core/FSTView.mm | 479 +++++++++ Firestore/Source/Core/FSTViewSnapshot.m | 231 ----- Firestore/Source/Core/FSTViewSnapshot.mm | 231 +++++ Firestore/Source/Local/FSTEagerGarbageCollector.m | 89 -- Firestore/Source/Local/FSTEagerGarbageCollector.mm | 89 ++ Firestore/Source/Local/FSTLevelDB.h | 14 +- Firestore/Source/Local/FSTLevelDBKey.h | 4 - Firestore/Source/Local/FSTLevelDBMigrations.h | 8 +- Firestore/Source/Local/FSTLevelDBMutationQueue.h | 11 +- Firestore/Source/Local/FSTLevelDBQueryCache.h | 15 +- .../Source/Local/FSTLevelDBRemoteDocumentCache.h | 11 +- Firestore/Source/Local/FSTLocalDocumentsView.m | 182 ---- Firestore/Source/Local/FSTLocalDocumentsView.mm | 182 ++++ Firestore/Source/Local/FSTLocalSerializer.m | 211 ---- Firestore/Source/Local/FSTLocalSerializer.mm | 213 ++++ Firestore/Source/Local/FSTLocalViewChanges.m | 76 -- Firestore/Source/Local/FSTLocalViewChanges.mm | 76 ++ Firestore/Source/Local/FSTLocalWriteResult.m | 43 - Firestore/Source/Local/FSTLocalWriteResult.mm | 43 + Firestore/Source/Local/FSTMemoryPersistence.m | 107 -- Firestore/Source/Local/FSTMemoryPersistence.mm | 107 ++ Firestore/Source/Local/FSTMemoryQueryCache.m | 140 --- Firestore/Source/Local/FSTMemoryQueryCache.mm | 140 +++ .../Source/Local/FSTMemoryRemoteDocumentCache.m | 84 -- .../Source/Local/FSTMemoryRemoteDocumentCache.mm | 84 ++ Firestore/Source/Local/FSTNoOpGarbageCollector.m | 45 - Firestore/Source/Local/FSTNoOpGarbageCollector.mm | 45 + Firestore/Source/Local/FSTQueryData.m | 98 -- Firestore/Source/Local/FSTQueryData.mm | 98 ++ Firestore/Source/Local/FSTReferenceSet.m | 135 --- Firestore/Source/Local/FSTReferenceSet.mm | 135 +++ .../Source/Local/FSTRemoteDocumentChangeBuffer.m | 88 -- .../Source/Local/FSTRemoteDocumentChangeBuffer.mm | 88 ++ Firestore/Source/Local/FSTWriteGroup.h | 13 +- Firestore/Source/Local/FSTWriteGroupTracker.m | 52 - Firestore/Source/Local/FSTWriteGroupTracker.mm | 52 + Firestore/Source/Local/StringView.h | 4 - Firestore/Source/Model/FSTDatabaseID.m | 90 -- Firestore/Source/Model/FSTDatabaseID.mm | 90 ++ Firestore/Source/Model/FSTDocument.m | 139 --- Firestore/Source/Model/FSTDocument.mm | 139 +++ Firestore/Source/Model/FSTDocumentDictionary.m | 42 - Firestore/Source/Model/FSTDocumentDictionary.mm | 42 + Firestore/Source/Model/FSTDocumentKey.m | 105 -- Firestore/Source/Model/FSTDocumentKey.mm | 105 ++ Firestore/Source/Model/FSTDocumentKeySet.m | 31 - Firestore/Source/Model/FSTDocumentKeySet.mm | 31 + Firestore/Source/Model/FSTDocumentSet.m | 197 ---- Firestore/Source/Model/FSTDocumentSet.mm | 197 ++++ .../Source/Model/FSTDocumentVersionDictionary.m | 37 - .../Source/Model/FSTDocumentVersionDictionary.mm | 37 + Firestore/Source/Model/FSTMutation.m | 593 ----------- Firestore/Source/Model/FSTMutation.mm | 593 +++++++++++ Firestore/Source/Model/FSTMutationBatch.m | 178 ---- Firestore/Source/Model/FSTMutationBatch.mm | 178 ++++ Firestore/Source/Model/FSTPath.m | 356 ------- Firestore/Source/Model/FSTPath.mm | 356 +++++++ Firestore/Source/Remote/FSTBufferedWriter.m | 134 --- Firestore/Source/Remote/FSTBufferedWriter.mm | 134 +++ Firestore/Source/Remote/FSTDatastore.m | 336 ------ Firestore/Source/Remote/FSTDatastore.mm | 336 ++++++ Firestore/Source/Remote/FSTExistenceFilter.m | 53 - Firestore/Source/Remote/FSTExistenceFilter.mm | 53 + Firestore/Source/Remote/FSTRemoteEvent.m | 516 ---------- Firestore/Source/Remote/FSTRemoteEvent.mm | 528 ++++++++++ Firestore/Source/Remote/FSTRemoteStore.m | 710 ------------- Firestore/Source/Remote/FSTRemoteStore.mm | 712 +++++++++++++ Firestore/Source/Remote/FSTSerializerBeta.m | 1084 ------------------- Firestore/Source/Remote/FSTSerializerBeta.mm | 1086 ++++++++++++++++++++ Firestore/Source/Remote/FSTStream.m | 787 -------------- Firestore/Source/Remote/FSTStream.mm | 787 ++++++++++++++ Firestore/Source/Remote/FSTWatchChange.m | 150 --- Firestore/Source/Remote/FSTWatchChange.mm | 150 +++ Firestore/Source/Util/FSTAsyncQueryListener.m | 50 - Firestore/Source/Util/FSTAsyncQueryListener.mm | 50 + Firestore/Source/Util/FSTDispatchQueue.m | 80 -- Firestore/Source/Util/FSTDispatchQueue.mm | 80 ++ Firestore/Source/Util/FSTLogger.h | 8 - Firestore/Source/Util/FSTLogger.m | 41 - Firestore/Source/Util/FSTLogger.mm | 41 + Firestore/Source/Util/FSTUsageValidation.h | 8 - Firestore/Source/Util/FSTUsageValidation.m | 30 - Firestore/Source/Util/FSTUsageValidation.mm | 30 + 273 files changed, 28391 insertions(+), 28432 deletions(-) delete mode 100644 Firestore/Example/Tests/API/FIRCollectionReferenceTests.m create mode 100644 Firestore/Example/Tests/API/FIRCollectionReferenceTests.mm delete mode 100644 Firestore/Example/Tests/API/FIRDocumentReferenceTests.m create mode 100644 Firestore/Example/Tests/API/FIRDocumentReferenceTests.mm delete mode 100644 Firestore/Example/Tests/API/FIRDocumentSnapshotTests.m create mode 100644 Firestore/Example/Tests/API/FIRDocumentSnapshotTests.mm delete mode 100644 Firestore/Example/Tests/API/FIRFieldPathTests.m create mode 100644 Firestore/Example/Tests/API/FIRFieldPathTests.mm delete mode 100644 Firestore/Example/Tests/API/FIRFieldValueTests.m create mode 100644 Firestore/Example/Tests/API/FIRFieldValueTests.mm delete mode 100644 Firestore/Example/Tests/API/FIRGeoPointTests.m create mode 100644 Firestore/Example/Tests/API/FIRGeoPointTests.mm delete mode 100644 Firestore/Example/Tests/API/FIRQuerySnapshotTests.m create mode 100644 Firestore/Example/Tests/API/FIRQuerySnapshotTests.mm delete mode 100644 Firestore/Example/Tests/API/FIRQueryTests.m create mode 100644 Firestore/Example/Tests/API/FIRQueryTests.mm delete mode 100644 Firestore/Example/Tests/API/FIRSnapshotMetadataTests.m create mode 100644 Firestore/Example/Tests/API/FIRSnapshotMetadataTests.mm delete mode 100644 Firestore/Example/Tests/API/FSTAPIHelpers.m create mode 100644 Firestore/Example/Tests/API/FSTAPIHelpers.mm delete mode 100644 Firestore/Example/Tests/Core/FSTDatabaseInfoTests.m create mode 100644 Firestore/Example/Tests/Core/FSTDatabaseInfoTests.mm delete mode 100644 Firestore/Example/Tests/Core/FSTEventManagerTests.m create mode 100644 Firestore/Example/Tests/Core/FSTEventManagerTests.mm delete mode 100644 Firestore/Example/Tests/Core/FSTQueryListenerTests.m create mode 100644 Firestore/Example/Tests/Core/FSTQueryListenerTests.mm delete mode 100644 Firestore/Example/Tests/Core/FSTQueryTests.m create mode 100644 Firestore/Example/Tests/Core/FSTQueryTests.mm delete mode 100644 Firestore/Example/Tests/Core/FSTTimestampTests.m create mode 100644 Firestore/Example/Tests/Core/FSTTimestampTests.mm delete mode 100644 Firestore/Example/Tests/Core/FSTViewSnapshotTest.m create mode 100644 Firestore/Example/Tests/Core/FSTViewSnapshotTest.mm delete mode 100644 Firestore/Example/Tests/Core/FSTViewTests.m create mode 100644 Firestore/Example/Tests/Core/FSTViewTests.mm delete mode 100644 Firestore/Example/Tests/Integration/API/FIRCursorTests.m create mode 100644 Firestore/Example/Tests/Integration/API/FIRCursorTests.mm delete mode 100644 Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m create mode 100644 Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm delete mode 100644 Firestore/Example/Tests/Integration/API/FIRFieldsTests.m create mode 100644 Firestore/Example/Tests/Integration/API/FIRFieldsTests.mm delete mode 100644 Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m create mode 100644 Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.mm delete mode 100644 Firestore/Example/Tests/Integration/API/FIRQueryTests.m create mode 100644 Firestore/Example/Tests/Integration/API/FIRQueryTests.mm delete mode 100644 Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m create mode 100644 Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm delete mode 100644 Firestore/Example/Tests/Integration/API/FIRTypeTests.m create mode 100644 Firestore/Example/Tests/Integration/API/FIRTypeTests.mm delete mode 100644 Firestore/Example/Tests/Integration/API/FIRValidationTests.m create mode 100644 Firestore/Example/Tests/Integration/API/FIRValidationTests.mm delete mode 100644 Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m create mode 100644 Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm delete mode 100644 Firestore/Example/Tests/Integration/FSTDatastoreTests.m create mode 100644 Firestore/Example/Tests/Integration/FSTDatastoreTests.mm delete mode 100644 Firestore/Example/Tests/Integration/FSTSmokeTests.m create mode 100644 Firestore/Example/Tests/Integration/FSTSmokeTests.mm delete mode 100644 Firestore/Example/Tests/Integration/FSTStreamTests.m create mode 100644 Firestore/Example/Tests/Integration/FSTStreamTests.mm delete mode 100644 Firestore/Example/Tests/Integration/FSTTransactionTests.m create mode 100644 Firestore/Example/Tests/Integration/FSTTransactionTests.mm delete mode 100644 Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m create mode 100644 Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.mm delete mode 100644 Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.m create mode 100644 Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.mm delete mode 100644 Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.m create mode 100644 Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.mm delete mode 100644 Firestore/Example/Tests/Local/FSTLocalSerializerTests.m create mode 100644 Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm delete mode 100644 Firestore/Example/Tests/Local/FSTLocalStoreTests.m create mode 100644 Firestore/Example/Tests/Local/FSTLocalStoreTests.mm delete mode 100644 Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.m create mode 100644 Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.mm delete mode 100644 Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.m create mode 100644 Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm delete mode 100644 Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.m create mode 100644 Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.mm delete mode 100644 Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.m create mode 100644 Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.mm delete mode 100644 Firestore/Example/Tests/Local/FSTMutationQueueTests.m create mode 100644 Firestore/Example/Tests/Local/FSTMutationQueueTests.mm delete mode 100644 Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.m create mode 100644 Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.mm delete mode 100644 Firestore/Example/Tests/Local/FSTQueryCacheTests.m create mode 100644 Firestore/Example/Tests/Local/FSTQueryCacheTests.mm delete mode 100644 Firestore/Example/Tests/Local/FSTReferenceSetTests.m create mode 100644 Firestore/Example/Tests/Local/FSTReferenceSetTests.mm delete mode 100644 Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m create mode 100644 Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.mm delete mode 100644 Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.m create mode 100644 Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.mm delete mode 100644 Firestore/Example/Tests/Model/FSTDatabaseIDTests.m create mode 100644 Firestore/Example/Tests/Model/FSTDatabaseIDTests.mm delete mode 100644 Firestore/Example/Tests/Model/FSTDocumentKeyTests.m create mode 100644 Firestore/Example/Tests/Model/FSTDocumentKeyTests.mm delete mode 100644 Firestore/Example/Tests/Model/FSTDocumentSetTests.m create mode 100644 Firestore/Example/Tests/Model/FSTDocumentSetTests.mm delete mode 100644 Firestore/Example/Tests/Model/FSTDocumentTests.m create mode 100644 Firestore/Example/Tests/Model/FSTDocumentTests.mm delete mode 100644 Firestore/Example/Tests/Model/FSTFieldValueTests.m create mode 100644 Firestore/Example/Tests/Model/FSTFieldValueTests.mm delete mode 100644 Firestore/Example/Tests/Model/FSTMutationTests.m create mode 100644 Firestore/Example/Tests/Model/FSTMutationTests.mm delete mode 100644 Firestore/Example/Tests/Model/FSTPathTests.m create mode 100644 Firestore/Example/Tests/Model/FSTPathTests.mm delete mode 100644 Firestore/Example/Tests/Remote/FSTDatastoreTests.m create mode 100644 Firestore/Example/Tests/Remote/FSTDatastoreTests.mm delete mode 100644 Firestore/Example/Tests/Remote/FSTRemoteEventTests.m create mode 100644 Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm delete mode 100644 Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m create mode 100644 Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm delete mode 100644 Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m create mode 100644 Firestore/Example/Tests/Remote/FSTWatchChange+Testing.mm delete mode 100644 Firestore/Example/Tests/Remote/FSTWatchChangeTests.m create mode 100644 Firestore/Example/Tests/Remote/FSTWatchChangeTests.mm delete mode 100644 Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.m create mode 100644 Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.mm delete mode 100644 Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m create mode 100644 Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.mm delete mode 100644 Firestore/Example/Tests/SpecTests/FSTMockDatastore.m create mode 100644 Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm delete mode 100644 Firestore/Example/Tests/SpecTests/FSTSpecTests.m create mode 100644 Firestore/Example/Tests/SpecTests/FSTSpecTests.mm delete mode 100644 Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m create mode 100644 Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm delete mode 100644 Firestore/Example/Tests/Util/FSTAssertTests.m create mode 100644 Firestore/Example/Tests/Util/FSTAssertTests.mm delete mode 100644 Firestore/Example/Tests/Util/FSTEventAccumulator.m create mode 100644 Firestore/Example/Tests/Util/FSTEventAccumulator.mm delete mode 100644 Firestore/Example/Tests/Util/FSTHelpers.m create mode 100644 Firestore/Example/Tests/Util/FSTHelpers.mm delete mode 100644 Firestore/Example/Tests/Util/FSTTestDispatchQueue.m create mode 100644 Firestore/Example/Tests/Util/FSTTestDispatchQueue.mm delete mode 100644 Firestore/Example/Tests/Util/XCTestCase+Await.m create mode 100644 Firestore/Example/Tests/Util/XCTestCase+Await.mm delete mode 100644 Firestore/Source/API/FIRDocumentChange.m create mode 100644 Firestore/Source/API/FIRDocumentChange.mm delete mode 100644 Firestore/Source/API/FIRDocumentReference.m create mode 100644 Firestore/Source/API/FIRDocumentReference.mm delete mode 100644 Firestore/Source/API/FIRDocumentSnapshot.m create mode 100644 Firestore/Source/API/FIRDocumentSnapshot.mm delete mode 100644 Firestore/Source/API/FIRFieldPath.m create mode 100644 Firestore/Source/API/FIRFieldPath.mm delete mode 100644 Firestore/Source/API/FIRFieldValue.m create mode 100644 Firestore/Source/API/FIRFieldValue.mm delete mode 100644 Firestore/Source/API/FIRFirestore.m create mode 100644 Firestore/Source/API/FIRFirestore.mm delete mode 100644 Firestore/Source/API/FIRFirestoreSettings.m create mode 100644 Firestore/Source/API/FIRFirestoreSettings.mm delete mode 100644 Firestore/Source/API/FIRFirestoreVersion.m create mode 100644 Firestore/Source/API/FIRFirestoreVersion.mm delete mode 100644 Firestore/Source/API/FIRListenerRegistration.m create mode 100644 Firestore/Source/API/FIRListenerRegistration.mm delete mode 100644 Firestore/Source/API/FIRQuery.m create mode 100644 Firestore/Source/API/FIRQuery.mm delete mode 100644 Firestore/Source/API/FIRQuerySnapshot.m create mode 100644 Firestore/Source/API/FIRQuerySnapshot.mm delete mode 100644 Firestore/Source/API/FIRSetOptions.m create mode 100644 Firestore/Source/API/FIRSetOptions.mm delete mode 100644 Firestore/Source/API/FIRSnapshotMetadata.m create mode 100644 Firestore/Source/API/FIRSnapshotMetadata.mm delete mode 100644 Firestore/Source/API/FIRSnapshotOptions.m create mode 100644 Firestore/Source/API/FIRSnapshotOptions.mm delete mode 100644 Firestore/Source/API/FIRTransaction.m create mode 100644 Firestore/Source/API/FIRTransaction.mm delete mode 100644 Firestore/Source/API/FIRWriteBatch.m create mode 100644 Firestore/Source/API/FIRWriteBatch.mm delete mode 100644 Firestore/Source/API/FSTUserDataConverter.m create mode 100644 Firestore/Source/API/FSTUserDataConverter.mm delete mode 100644 Firestore/Source/Auth/FSTCredentialsProvider.m create mode 100644 Firestore/Source/Auth/FSTCredentialsProvider.mm delete mode 100644 Firestore/Source/Auth/FSTEmptyCredentialsProvider.m create mode 100644 Firestore/Source/Auth/FSTEmptyCredentialsProvider.mm delete mode 100644 Firestore/Source/Auth/FSTUser.m create mode 100644 Firestore/Source/Auth/FSTUser.mm delete mode 100644 Firestore/Source/Core/FSTDatabaseInfo.m create mode 100644 Firestore/Source/Core/FSTDatabaseInfo.mm delete mode 100644 Firestore/Source/Core/FSTEventManager.m create mode 100644 Firestore/Source/Core/FSTEventManager.mm delete mode 100644 Firestore/Source/Core/FSTFirestoreClient.m create mode 100644 Firestore/Source/Core/FSTFirestoreClient.mm delete mode 100644 Firestore/Source/Core/FSTListenSequence.m create mode 100644 Firestore/Source/Core/FSTListenSequence.mm delete mode 100644 Firestore/Source/Core/FSTQuery.m create mode 100644 Firestore/Source/Core/FSTQuery.mm delete mode 100644 Firestore/Source/Core/FSTSnapshotVersion.m create mode 100644 Firestore/Source/Core/FSTSnapshotVersion.mm delete mode 100644 Firestore/Source/Core/FSTTransaction.m create mode 100644 Firestore/Source/Core/FSTTransaction.mm delete mode 100644 Firestore/Source/Core/FSTView.m create mode 100644 Firestore/Source/Core/FSTView.mm delete mode 100644 Firestore/Source/Core/FSTViewSnapshot.m create mode 100644 Firestore/Source/Core/FSTViewSnapshot.mm delete mode 100644 Firestore/Source/Local/FSTEagerGarbageCollector.m create mode 100644 Firestore/Source/Local/FSTEagerGarbageCollector.mm delete mode 100644 Firestore/Source/Local/FSTLocalDocumentsView.m create mode 100644 Firestore/Source/Local/FSTLocalDocumentsView.mm delete mode 100644 Firestore/Source/Local/FSTLocalSerializer.m create mode 100644 Firestore/Source/Local/FSTLocalSerializer.mm delete mode 100644 Firestore/Source/Local/FSTLocalViewChanges.m create mode 100644 Firestore/Source/Local/FSTLocalViewChanges.mm delete mode 100644 Firestore/Source/Local/FSTLocalWriteResult.m create mode 100644 Firestore/Source/Local/FSTLocalWriteResult.mm delete mode 100644 Firestore/Source/Local/FSTMemoryPersistence.m create mode 100644 Firestore/Source/Local/FSTMemoryPersistence.mm delete mode 100644 Firestore/Source/Local/FSTMemoryQueryCache.m create mode 100644 Firestore/Source/Local/FSTMemoryQueryCache.mm delete mode 100644 Firestore/Source/Local/FSTMemoryRemoteDocumentCache.m create mode 100644 Firestore/Source/Local/FSTMemoryRemoteDocumentCache.mm delete mode 100644 Firestore/Source/Local/FSTNoOpGarbageCollector.m create mode 100644 Firestore/Source/Local/FSTNoOpGarbageCollector.mm delete mode 100644 Firestore/Source/Local/FSTQueryData.m create mode 100644 Firestore/Source/Local/FSTQueryData.mm delete mode 100644 Firestore/Source/Local/FSTReferenceSet.m create mode 100644 Firestore/Source/Local/FSTReferenceSet.mm delete mode 100644 Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.m create mode 100644 Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.mm delete mode 100644 Firestore/Source/Local/FSTWriteGroupTracker.m create mode 100644 Firestore/Source/Local/FSTWriteGroupTracker.mm delete mode 100644 Firestore/Source/Model/FSTDatabaseID.m create mode 100644 Firestore/Source/Model/FSTDatabaseID.mm delete mode 100644 Firestore/Source/Model/FSTDocument.m create mode 100644 Firestore/Source/Model/FSTDocument.mm delete mode 100644 Firestore/Source/Model/FSTDocumentDictionary.m create mode 100644 Firestore/Source/Model/FSTDocumentDictionary.mm delete mode 100644 Firestore/Source/Model/FSTDocumentKey.m create mode 100644 Firestore/Source/Model/FSTDocumentKey.mm delete mode 100644 Firestore/Source/Model/FSTDocumentKeySet.m create mode 100644 Firestore/Source/Model/FSTDocumentKeySet.mm delete mode 100644 Firestore/Source/Model/FSTDocumentSet.m create mode 100644 Firestore/Source/Model/FSTDocumentSet.mm delete mode 100644 Firestore/Source/Model/FSTDocumentVersionDictionary.m create mode 100644 Firestore/Source/Model/FSTDocumentVersionDictionary.mm delete mode 100644 Firestore/Source/Model/FSTMutation.m create mode 100644 Firestore/Source/Model/FSTMutation.mm delete mode 100644 Firestore/Source/Model/FSTMutationBatch.m create mode 100644 Firestore/Source/Model/FSTMutationBatch.mm delete mode 100644 Firestore/Source/Model/FSTPath.m create mode 100644 Firestore/Source/Model/FSTPath.mm delete mode 100644 Firestore/Source/Remote/FSTBufferedWriter.m create mode 100644 Firestore/Source/Remote/FSTBufferedWriter.mm delete mode 100644 Firestore/Source/Remote/FSTDatastore.m create mode 100644 Firestore/Source/Remote/FSTDatastore.mm delete mode 100644 Firestore/Source/Remote/FSTExistenceFilter.m create mode 100644 Firestore/Source/Remote/FSTExistenceFilter.mm delete mode 100644 Firestore/Source/Remote/FSTRemoteEvent.m create mode 100644 Firestore/Source/Remote/FSTRemoteEvent.mm delete mode 100644 Firestore/Source/Remote/FSTRemoteStore.m create mode 100644 Firestore/Source/Remote/FSTRemoteStore.mm delete mode 100644 Firestore/Source/Remote/FSTSerializerBeta.m create mode 100644 Firestore/Source/Remote/FSTSerializerBeta.mm delete mode 100644 Firestore/Source/Remote/FSTStream.m create mode 100644 Firestore/Source/Remote/FSTStream.mm delete mode 100644 Firestore/Source/Remote/FSTWatchChange.m create mode 100644 Firestore/Source/Remote/FSTWatchChange.mm delete mode 100644 Firestore/Source/Util/FSTAsyncQueryListener.m create mode 100644 Firestore/Source/Util/FSTAsyncQueryListener.mm delete mode 100644 Firestore/Source/Util/FSTDispatchQueue.m create mode 100644 Firestore/Source/Util/FSTDispatchQueue.mm delete mode 100644 Firestore/Source/Util/FSTLogger.m create mode 100644 Firestore/Source/Util/FSTLogger.mm delete mode 100644 Firestore/Source/Util/FSTUsageValidation.m create mode 100644 Firestore/Source/Util/FSTUsageValidation.mm diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index a12c4fc..8bfee06 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -23,7 +23,6 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ - 132E32A6C1989C284BFE10B2 /* FSTLevelDBMigrationsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 132E36114FC5BA5DBE3CB260 /* FSTLevelDBMigrationsTests.mm */; }; 3B843E4C1F3A182900548890 /* remote_store_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */; }; 5436F32420008FAD006E51E3 /* string_printf_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5436F32320008FAD006E51E3 /* string_printf_test.cc */; }; 54740A571FC914BA00713A1A /* secure_random_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54740A531FC913E500713A1A /* secure_random_test.cc */; }; @@ -33,6 +32,83 @@ 548DB929200D59F600E00ABC /* comparison_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 548DB928200D59F600E00ABC /* comparison_test.cc */; }; 5491BC721FB44593008B3588 /* FSTIntegrationTestCase.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5491BC711FB44593008B3588 /* FSTIntegrationTestCase.mm */; }; 5491BC731FB44593008B3588 /* FSTIntegrationTestCase.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5491BC711FB44593008B3588 /* FSTIntegrationTestCase.mm */; }; + 5492E03120213FFC00B64F25 /* FSTLevelDBSpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02C20213FFB00B64F25 /* FSTLevelDBSpecTests.mm */; }; + 5492E03220213FFC00B64F25 /* FSTMockDatastore.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02D20213FFC00B64F25 /* FSTMockDatastore.mm */; }; + 5492E03320213FFC00B64F25 /* FSTSyncEngineTestDriver.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02E20213FFC00B64F25 /* FSTSyncEngineTestDriver.mm */; }; + 5492E03420213FFC00B64F25 /* FSTMemorySpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02F20213FFC00B64F25 /* FSTMemorySpecTests.mm */; }; + 5492E03520213FFC00B64F25 /* FSTSpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E03020213FFC00B64F25 /* FSTSpecTests.mm */; }; + 5492E03B2021401F00B64F25 /* FSTTestDispatchQueue.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0362021401E00B64F25 /* FSTTestDispatchQueue.mm */; }; + 5492E03C2021401F00B64F25 /* XCTestCase+Await.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0372021401E00B64F25 /* XCTestCase+Await.mm */; }; + 5492E03D2021401F00B64F25 /* FSTAssertTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0382021401E00B64F25 /* FSTAssertTests.mm */; }; + 5492E03E2021401F00B64F25 /* FSTEventAccumulator.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0392021401F00B64F25 /* FSTEventAccumulator.mm */; }; + 5492E03F2021401F00B64F25 /* FSTHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E03A2021401F00B64F25 /* FSTHelpers.mm */; }; + 5492E041202143E700B64F25 /* FSTEventAccumulator.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0392021401F00B64F25 /* FSTEventAccumulator.mm */; }; + 5492E0422021440500B64F25 /* FSTHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E03A2021401F00B64F25 /* FSTHelpers.mm */; }; + 5492E0432021441E00B64F25 /* FSTTestDispatchQueue.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0362021401E00B64F25 /* FSTTestDispatchQueue.mm */; }; + 5492E0442021457E00B64F25 /* XCTestCase+Await.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0372021401E00B64F25 /* XCTestCase+Await.mm */; }; + 5492E050202154AA00B64F25 /* FIRCollectionReferenceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E045202154AA00B64F25 /* FIRCollectionReferenceTests.mm */; }; + 5492E051202154AA00B64F25 /* FIRQueryTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E046202154AA00B64F25 /* FIRQueryTests.mm */; }; + 5492E052202154AB00B64F25 /* FIRGeoPointTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E048202154AA00B64F25 /* FIRGeoPointTests.mm */; }; + 5492E053202154AB00B64F25 /* FIRDocumentReferenceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E049202154AA00B64F25 /* FIRDocumentReferenceTests.mm */; }; + 5492E054202154AB00B64F25 /* FIRFieldValueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04A202154AA00B64F25 /* FIRFieldValueTests.mm */; }; + 5492E055202154AB00B64F25 /* FIRDocumentSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04B202154AA00B64F25 /* FIRDocumentSnapshotTests.mm */; }; + 5492E056202154AB00B64F25 /* FIRFieldPathTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04C202154AA00B64F25 /* FIRFieldPathTests.mm */; }; + 5492E057202154AB00B64F25 /* FIRSnapshotMetadataTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04D202154AA00B64F25 /* FIRSnapshotMetadataTests.mm */; }; + 5492E058202154AB00B64F25 /* FSTAPIHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04E202154AA00B64F25 /* FSTAPIHelpers.mm */; }; + 5492E059202154AB00B64F25 /* FIRQuerySnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04F202154AA00B64F25 /* FIRQuerySnapshotTests.mm */; }; + 5492E062202154B900B64F25 /* FSTTimestampTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E05B202154B800B64F25 /* FSTTimestampTests.mm */; }; + 5492E063202154B900B64F25 /* FSTViewSnapshotTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E05C202154B800B64F25 /* FSTViewSnapshotTest.mm */; }; + 5492E064202154B900B64F25 /* FSTQueryListenerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E05D202154B900B64F25 /* FSTQueryListenerTests.mm */; }; + 5492E065202154B900B64F25 /* FSTViewTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E05E202154B900B64F25 /* FSTViewTests.mm */; }; + 5492E066202154B900B64F25 /* FSTDatabaseInfoTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E05F202154B900B64F25 /* FSTDatabaseInfoTests.mm */; }; + 5492E067202154B900B64F25 /* FSTEventManagerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E060202154B900B64F25 /* FSTEventManagerTests.mm */; }; + 5492E068202154B900B64F25 /* FSTQueryTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E061202154B900B64F25 /* FSTQueryTests.mm */; }; + 5492E072202154D600B64F25 /* FIRQueryTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E069202154D500B64F25 /* FIRQueryTests.mm */; }; + 5492E073202154D600B64F25 /* FIRFieldsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06A202154D500B64F25 /* FIRFieldsTests.mm */; }; + 5492E074202154D600B64F25 /* FIRListenerRegistrationTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06B202154D500B64F25 /* FIRListenerRegistrationTests.mm */; }; + 5492E075202154D600B64F25 /* FIRDatabaseTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06C202154D500B64F25 /* FIRDatabaseTests.mm */; }; + 5492E076202154D600B64F25 /* FIRValidationTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06D202154D600B64F25 /* FIRValidationTests.mm */; }; + 5492E077202154D600B64F25 /* FIRServerTimestampTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06E202154D600B64F25 /* FIRServerTimestampTests.mm */; }; + 5492E078202154D600B64F25 /* FIRWriteBatchTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06F202154D600B64F25 /* FIRWriteBatchTests.mm */; }; + 5492E079202154D600B64F25 /* FIRCursorTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E070202154D600B64F25 /* FIRCursorTests.mm */; }; + 5492E07A202154D600B64F25 /* FIRTypeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E071202154D600B64F25 /* FIRTypeTests.mm */; }; + 5492E07F202154EC00B64F25 /* FSTTransactionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E07B202154EB00B64F25 /* FSTTransactionTests.mm */; }; + 5492E080202154EC00B64F25 /* FSTSmokeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E07C202154EB00B64F25 /* FSTSmokeTests.mm */; }; + 5492E081202154EC00B64F25 /* FSTStreamTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E07D202154EB00B64F25 /* FSTStreamTests.mm */; }; + 5492E082202154EC00B64F25 /* FSTDatastoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E07E202154EC00B64F25 /* FSTDatastoreTests.mm */; }; + 5492E09D2021552D00B64F25 /* FSTLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0832021552A00B64F25 /* FSTLocalStoreTests.mm */; }; + 5492E09E2021552D00B64F25 /* FSTEagerGarbageCollectorTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0842021552A00B64F25 /* FSTEagerGarbageCollectorTests.mm */; }; + 5492E09F2021552D00B64F25 /* FSTLevelDBMigrationsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0862021552A00B64F25 /* FSTLevelDBMigrationsTests.mm */; }; + 5492E0A02021552D00B64F25 /* FSTLevelDBMutationQueueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0872021552A00B64F25 /* FSTLevelDBMutationQueueTests.mm */; }; + 5492E0A12021552D00B64F25 /* FSTMemoryLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0882021552A00B64F25 /* FSTMemoryLocalStoreTests.mm */; }; + 5492E0A22021552D00B64F25 /* FSTQueryCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0892021552A00B64F25 /* FSTQueryCacheTests.mm */; }; + 5492E0A32021552D00B64F25 /* FSTLocalSerializerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E08A2021552A00B64F25 /* FSTLocalSerializerTests.mm */; }; + 5492E0A42021552D00B64F25 /* FSTMemoryQueryCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E08B2021552B00B64F25 /* FSTMemoryQueryCacheTests.mm */; }; + 5492E0A52021552D00B64F25 /* FSTMemoryRemoteDocumentCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E08C2021552B00B64F25 /* FSTMemoryRemoteDocumentCacheTests.mm */; }; + 5492E0A62021552D00B64F25 /* FSTPersistenceTestHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E08D2021552B00B64F25 /* FSTPersistenceTestHelpers.mm */; }; + 5492E0A72021552D00B64F25 /* FSTLevelDBKeyTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E08E2021552B00B64F25 /* FSTLevelDBKeyTests.mm */; }; + 5492E0A82021552D00B64F25 /* FSTLevelDBLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E08F2021552B00B64F25 /* FSTLevelDBLocalStoreTests.mm */; }; + 5492E0A92021552D00B64F25 /* FSTRemoteDocumentChangeBufferTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0902021552B00B64F25 /* FSTRemoteDocumentChangeBufferTests.mm */; }; + 5492E0AA2021552D00B64F25 /* FSTLevelDBRemoteDocumentCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0922021552B00B64F25 /* FSTLevelDBRemoteDocumentCacheTests.mm */; }; + 5492E0AB2021552D00B64F25 /* StringViewTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0932021552B00B64F25 /* StringViewTests.mm */; }; + 5492E0AC2021552D00B64F25 /* FSTMutationQueueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0962021552C00B64F25 /* FSTMutationQueueTests.mm */; }; + 5492E0AD2021552D00B64F25 /* FSTMemoryMutationQueueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0972021552C00B64F25 /* FSTMemoryMutationQueueTests.mm */; }; + 5492E0AE2021552D00B64F25 /* FSTLevelDBQueryCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0982021552C00B64F25 /* FSTLevelDBQueryCacheTests.mm */; }; + 5492E0AF2021552D00B64F25 /* FSTReferenceSetTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E09A2021552C00B64F25 /* FSTReferenceSetTests.mm */; }; + 5492E0B02021552D00B64F25 /* FSTWriteGroupTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E09B2021552C00B64F25 /* FSTWriteGroupTests.mm */; }; + 5492E0B12021552D00B64F25 /* FSTRemoteDocumentCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E09C2021552D00B64F25 /* FSTRemoteDocumentCacheTests.mm */; }; + 5492E0B92021555100B64F25 /* FSTDocumentKeyTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0B22021555000B64F25 /* FSTDocumentKeyTests.mm */; }; + 5492E0BA2021555100B64F25 /* FSTDocumentSetTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0B32021555100B64F25 /* FSTDocumentSetTests.mm */; }; + 5492E0BB2021555100B64F25 /* FSTDatabaseIDTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0B42021555100B64F25 /* FSTDatabaseIDTests.mm */; }; + 5492E0BC2021555100B64F25 /* FSTPathTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0B52021555100B64F25 /* FSTPathTests.mm */; }; + 5492E0BD2021555100B64F25 /* FSTDocumentTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0B62021555100B64F25 /* FSTDocumentTests.mm */; }; + 5492E0BE2021555100B64F25 /* FSTMutationTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0B72021555100B64F25 /* FSTMutationTests.mm */; }; + 5492E0BF2021555100B64F25 /* FSTFieldValueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0B82021555100B64F25 /* FSTFieldValueTests.mm */; }; + 5492E0C62021557E00B64F25 /* FSTWatchChange+Testing.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0C02021557E00B64F25 /* FSTWatchChange+Testing.mm */; }; + 5492E0C72021557E00B64F25 /* FSTSerializerBetaTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0C12021557E00B64F25 /* FSTSerializerBetaTests.mm */; }; + 5492E0C82021557E00B64F25 /* FSTDatastoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0C22021557E00B64F25 /* FSTDatastoreTests.mm */; }; + 5492E0C92021557E00B64F25 /* FSTRemoteEventTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0C32021557E00B64F25 /* FSTRemoteEventTests.mm */; }; + 5492E0CA2021557E00B64F25 /* FSTWatchChangeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0C52021557E00B64F25 /* FSTWatchChangeTests.mm */; }; 54C2294F1FECABAE007D065B /* log_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54C2294E1FECABAE007D065B /* log_test.cc */; }; 54DA12A61F315EE100DD57A1 /* collection_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129C1F315EE100DD57A1 /* collection_spec_test.json */; }; 54DA12A71F315EE100DD57A1 /* existence_filter_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */; }; @@ -44,11 +120,6 @@ 54DA12AD1F315EE100DD57A1 /* persistence_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A31F315EE100DD57A1 /* persistence_spec_test.json */; }; 54DA12AE1F315EE100DD57A1 /* resume_token_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A41F315EE100DD57A1 /* resume_token_spec_test.json */; }; 54DA12AF1F315EE100DD57A1 /* write_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A51F315EE100DD57A1 /* write_spec_test.json */; }; - 54DA12B11F315F3800DD57A1 /* FIRValidationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 54DA12B01F315F3800DD57A1 /* FIRValidationTests.m */; }; - 54E928241F33953300C1953E /* FSTEventAccumulator.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */; }; - 54E928251F33953400C1953E /* FSTEventAccumulator.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */; }; - 54E9282C1F339CAD00C1953E /* XCTestCase+Await.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */; }; - 54E9282D1F339CAD00C1953E /* XCTestCase+Await.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */; }; 6003F58E195388D20070C39A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; 6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58F195388D20070C39A /* CoreGraphics.framework */; }; 6003F592195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; @@ -61,98 +132,30 @@ 6003F5B1195388D20070C39A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; 6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; }; - 61E1D8B11FCF6C5700753285 /* StringViewTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 61E1D8AF1FCF6AF500753285 /* StringViewTests.mm */; }; 6ED54761B845349D43DB6B78 /* Pods_Firestore_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */; }; 71719F9F1E33DC2100824A3D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */; }; 873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */; }; AB356EF7200EA5EB0089B766 /* field_value_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB356EF6200EA5EB0089B766 /* field_value_test.cc */; }; AB380CFB2019388600D97691 /* target_id_generator_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380CF82019382300D97691 /* target_id_generator_test.cc */; }; AB380CFE201A2F4500D97691 /* string_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380CFC201A2EE200D97691 /* string_util_test.cc */; }; - AB382F7C1FE02A1F007CA955 /* FIRDocumentReferenceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AB382F7B1FE02A1F007CA955 /* FIRDocumentReferenceTests.m */; }; - AB382F7E1FE03059007CA955 /* FIRFieldPathTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AB382F7D1FE03059007CA955 /* FIRFieldPathTests.m */; }; + AB380D02201BC69F00D97691 /* bits_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380D01201BC69F00D97691 /* bits_test.cc */; }; + AB380D04201BC6E400D97691 /* ordered_code_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380D03201BC6E400D97691 /* ordered_code_test.cc */; }; AB7BAB342012B519001E0872 /* geo_point_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB7BAB332012B519001E0872 /* geo_point_test.cc */; }; - AB9945261FE2D71100DFC1E6 /* FIRCollectionReferenceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AB9945251FE2D71100DFC1E6 /* FIRCollectionReferenceTests.m */; }; - AB9945281FE2DE0C00DFC1E6 /* FIRSnapshotMetadataTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AB9945271FE2DE0C00DFC1E6 /* FIRSnapshotMetadataTests.m */; }; - AB99452A1FE2F9EB00DFC1E6 /* FIRDocumentSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AB9945291FE2F9EB00DFC1E6 /* FIRDocumentSnapshotTests.m */; }; - AB99452C1FE3018D00DFC1E6 /* FIRQuerySnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AB99452B1FE3018D00DFC1E6 /* FIRQuerySnapshotTests.m */; }; - AB99452E1FE30AC800DFC1E6 /* FIRFieldValueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AB99452D1FE30AC800DFC1E6 /* FIRFieldValueTests.m */; }; - ABAEEF4F1FD5F8B100C966CB /* FIRQueryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ABAEEF4E1FD5F8B100C966CB /* FIRQueryTests.m */; }; - ABF341051FE860CA00C48322 /* FSTAPIHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = ABF341021FE8593500C48322 /* FSTAPIHelpers.m */; }; + ABE6637A201FA81900ED349A /* database_id_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB71064B201FA60300344F18 /* database_id_test.cc */; }; ABF6506C201131F8005F2C74 /* timestamp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ABF6506B201131F8005F2C74 /* timestamp_test.cc */; }; AFE6114F0D4DAECBA7B7C089 /* Pods_Firestore_IntegrationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */; }; C4E749275AD0FBDF9F4716A8 /* Pods_SwiftBuildTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */; }; - D5B2532E4676014F57A7EAB9 /* FSTStreamTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D5B25C0D4AADFCA3ADB883E4 /* FSTStreamTests.m */; }; - D5B25474286C9800CE42B8C2 /* FSTTestDispatchQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = D5B25292CED31B81FDED0411 /* FSTTestDispatchQueue.m */; }; - D5B259FDEE8094E8D710C5BF /* FSTTestDispatchQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = D5B25292CED31B81FDED0411 /* FSTTestDispatchQueue.m */; }; - DE03B2C91F2149D600A30B9C /* FSTTransactionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C61F0D48AC0013853F /* FSTTransactionTests.m */; }; DE03B2D41F2149D600A30B9C /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F5AF195388D20070C39A /* XCTest.framework */; }; DE03B2D51F2149D600A30B9C /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; DE03B2D61F2149D600A30B9C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; DE03B2D71F2149D600A30B9C /* Pods_Firestore_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */; }; DE03B2DD1F2149D600A30B9C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; }; - DE03B2EC1F214BA200A30B9C /* FSTDatastoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C41F0D48AC0013853F /* FSTDatastoreTests.m */; }; - DE03B2ED1F214BA200A30B9C /* FSTSmokeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C51F0D48AC0013853F /* FSTSmokeTests.m */; }; - DE03B2EE1F214BAA00A30B9C /* FIRWriteBatchTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFE0F471F1F960A0071599A /* FIRWriteBatchTests.m */; }; - DE03B2EF1F214BAA00A30B9C /* FIRCursorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BD1F0D48AC0013853F /* FIRCursorTests.m */; }; - DE03B2F01F214BAA00A30B9C /* FIRDatabaseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BE1F0D48AC0013853F /* FIRDatabaseTests.m */; }; - DE03B2F11F214BAA00A30B9C /* FIRFieldsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BF1F0D48AC0013853F /* FIRFieldsTests.m */; }; - DE03B2F21F214BAA00A30B9C /* FIRListenerRegistrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C01F0D48AC0013853F /* FIRListenerRegistrationTests.m */; }; - DE03B2F31F214BAA00A30B9C /* FIRQueryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C11F0D48AC0013853F /* FIRQueryTests.m */; }; - DE03B2F41F214BAA00A30B9C /* FIRServerTimestampTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C21F0D48AC0013853F /* FIRServerTimestampTests.m */; }; - DE03B2F51F214BAA00A30B9C /* FIRTypeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C31F0D48AC0013853F /* FIRTypeTests.m */; }; - DE03B35E1F21586C00A30B9C /* FSTHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1891F0D48AC0013853F /* FSTHelpers.m */; }; DE03B3631F215E1A00A30B9C /* CAcert.pem in Resources */ = {isa = PBXBuildFile; fileRef = DE03B3621F215E1600A30B9C /* CAcert.pem */; }; DE0761F81F2FE68D003233AF /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE0761F61F2FE68D003233AF /* main.swift */; }; DE2EF0851F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF07E1F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m */; }; DE2EF0861F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0801F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m */; }; DE2EF0871F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0821F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m */; }; DE2EF0881F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0841F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m */; }; - DE51B1CC1F0D48C00013853F /* FIRGeoPointTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1841F0D48AC0013853F /* FIRGeoPointTests.m */; }; - DE51B1CD1F0D48CD0013853F /* FSTDatabaseInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1A91F0D48AC0013853F /* FSTDatabaseInfoTests.m */; }; - DE51B1CE1F0D48CD0013853F /* FSTEventManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AA1F0D48AC0013853F /* FSTEventManagerTests.m */; }; - DE51B1CF1F0D48CD0013853F /* FSTQueryListenerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AB1F0D48AC0013853F /* FSTQueryListenerTests.m */; }; - DE51B1D01F0D48CD0013853F /* FSTQueryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AC1F0D48AC0013853F /* FSTQueryTests.m */; }; - DE51B1D21F0D48CD0013853F /* FSTTimestampTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AF1F0D48AC0013853F /* FSTTimestampTests.m */; }; - DE51B1D31F0D48CD0013853F /* FSTViewSnapshotTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B01F0D48AC0013853F /* FSTViewSnapshotTest.m */; }; - DE51B1D41F0D48CD0013853F /* FSTViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B11F0D48AC0013853F /* FSTViewTests.m */; }; - DE51B1D91F0D490D0013853F /* FSTEagerGarbageCollectorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1631F0D48AC0013853F /* FSTEagerGarbageCollectorTests.m */; }; - DE51B1DA1F0D490D0013853F /* FSTLevelDBLocalStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1651F0D48AC0013853F /* FSTLevelDBLocalStoreTests.m */; }; - DE51B1DB1F0D490D0013853F /* FSTLevelDBQueryCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1671F0D48AC0013853F /* FSTLevelDBQueryCacheTests.m */; }; - DE51B1DC1F0D490D0013853F /* FSTLocalSerializerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1691F0D48AC0013853F /* FSTLocalSerializerTests.m */; }; - DE51B1DD1F0D490D0013853F /* FSTLocalStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16B1F0D48AC0013853F /* FSTLocalStoreTests.m */; }; - DE51B1DE1F0D490D0013853F /* FSTMemoryLocalStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16C1F0D48AC0013853F /* FSTMemoryLocalStoreTests.m */; }; - DE51B1DF1F0D490D0013853F /* FSTMemoryMutationQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16D1F0D48AC0013853F /* FSTMemoryMutationQueueTests.m */; }; - DE51B1E01F0D490D0013853F /* FSTMemoryQueryCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16E1F0D48AC0013853F /* FSTMemoryQueryCacheTests.m */; }; - DE51B1E11F0D490D0013853F /* FSTMemoryRemoteDocumentCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16F1F0D48AC0013853F /* FSTMemoryRemoteDocumentCacheTests.m */; }; - DE51B1E21F0D490D0013853F /* FSTMutationQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1711F0D48AC0013853F /* FSTMutationQueueTests.m */; }; - DE51B1E31F0D490D0013853F /* FSTPersistenceTestHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1731F0D48AC0013853F /* FSTPersistenceTestHelpers.m */; }; - DE51B1E41F0D490D0013853F /* FSTQueryCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1751F0D48AC0013853F /* FSTQueryCacheTests.m */; }; - DE51B1E51F0D490D0013853F /* FSTReferenceSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1761F0D48AC0013853F /* FSTReferenceSetTests.m */; }; - DE51B1E61F0D490D0013853F /* FSTRemoteDocumentCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1781F0D48AC0013853F /* FSTRemoteDocumentCacheTests.m */; }; - DE51B1E71F0D490D0013853F /* FSTRemoteDocumentChangeBufferTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1791F0D48AC0013853F /* FSTRemoteDocumentChangeBufferTests.m */; }; - DE51B1E81F0D490D0013853F /* FSTLevelDBKeyTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1641F0D48AC0013853F /* FSTLevelDBKeyTests.mm */; }; - DE51B1E91F0D490D0013853F /* FSTLevelDBMutationQueueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1661F0D48AC0013853F /* FSTLevelDBMutationQueueTests.mm */; }; - DE51B1EA1F0D490D0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1681F0D48AC0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm */; }; - DE51B1EB1F0D490D0013853F /* FSTWriteGroupTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17A1F0D48AC0013853F /* FSTWriteGroupTests.mm */; }; - DE51B1EC1F0D49140013853F /* FSTDatabaseIDTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17C1F0D48AC0013853F /* FSTDatabaseIDTests.m */; }; - DE51B1ED1F0D49140013853F /* FSTDocumentKeyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17D1F0D48AC0013853F /* FSTDocumentKeyTests.m */; }; - DE51B1EE1F0D49140013853F /* FSTDocumentSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17E1F0D48AC0013853F /* FSTDocumentSetTests.m */; }; - DE51B1EF1F0D49140013853F /* FSTDocumentTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17F1F0D48AC0013853F /* FSTDocumentTests.m */; }; - DE51B1F01F0D49140013853F /* FSTFieldValueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1801F0D48AC0013853F /* FSTFieldValueTests.m */; }; - DE51B1F11F0D49140013853F /* FSTMutationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1811F0D48AC0013853F /* FSTMutationTests.m */; }; - DE51B1F21F0D49140013853F /* FSTPathTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1821F0D48AC0013853F /* FSTPathTests.m */; }; - DE51B1F31F0D491B0013853F /* FSTDatastoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B31F0D48AC0013853F /* FSTDatastoreTests.m */; }; - DE51B1F41F0D491B0013853F /* FSTRemoteEventTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B41F0D48AC0013853F /* FSTRemoteEventTests.m */; }; - DE51B1F61F0D491B0013853F /* FSTSerializerBetaTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B61F0D48AC0013853F /* FSTSerializerBetaTests.m */; }; - DE51B1F81F0D491F0013853F /* FSTWatchChange+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B91F0D48AC0013853F /* FSTWatchChange+Testing.m */; }; - DE51B1F91F0D491F0013853F /* FSTWatchChangeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BA1F0D48AC0013853F /* FSTWatchChangeTests.m */; }; - DE51B1FA1F0D492C0013853F /* FSTLevelDBSpecTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1941F0D48AC0013853F /* FSTLevelDBSpecTests.m */; }; - DE51B1FB1F0D492C0013853F /* FSTMemorySpecTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1951F0D48AC0013853F /* FSTMemorySpecTests.m */; }; - DE51B1FC1F0D492C0013853F /* FSTMockDatastore.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1971F0D48AC0013853F /* FSTMockDatastore.m */; }; - DE51B1FD1F0D492C0013853F /* FSTSpecTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1991F0D48AC0013853F /* FSTSpecTests.m */; }; - DE51B1FE1F0D492C0013853F /* FSTSyncEngineTestDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B19B1F0D48AC0013853F /* FSTSyncEngineTestDriver.m */; }; - DE51B1FF1F0D493A0013853F /* FSTAssertTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1861F0D48AC0013853F /* FSTAssertTests.m */; }; - DE51B2011F0D493E0013853F /* FSTHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1891F0D48AC0013853F /* FSTHelpers.m */; }; F104BBD69BC3F0796E3A77C1 /* Pods_Firestore_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */; }; /* End PBXBuildFile section */ @@ -197,7 +200,6 @@ /* Begin PBXFileReference section */ 04DF37A117F88A9891379ED6 /* Pods-Firestore_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests.release.xcconfig"; sourceTree = ""; }; 12F4357299652983A615F886 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; - 132E36114FC5BA5DBE3CB260 /* FSTLevelDBMigrationsTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBMigrationsTests.mm; sourceTree = ""; }; 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftBuildTest.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = remote_store_spec_test.json; sourceTree = ""; }; 42491D7DC8C8CD245CC22B93 /* Pods-SwiftBuildTest.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftBuildTest.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest.debug.xcconfig"; sourceTree = ""; }; @@ -209,6 +211,87 @@ 548DB926200D590300E00ABC /* assert_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = assert_test.cc; path = ../../core/test/firebase/firestore/util/assert_test.cc; sourceTree = ""; }; 548DB928200D59F600E00ABC /* comparison_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = comparison_test.cc; path = ../../core/test/firebase/firestore/util/comparison_test.cc; sourceTree = ""; }; 5491BC711FB44593008B3588 /* FSTIntegrationTestCase.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTIntegrationTestCase.mm; sourceTree = ""; }; + 5492E02C20213FFB00B64F25 /* FSTLevelDBSpecTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBSpecTests.mm; sourceTree = ""; }; + 5492E02D20213FFC00B64F25 /* FSTMockDatastore.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTMockDatastore.mm; sourceTree = ""; }; + 5492E02E20213FFC00B64F25 /* FSTSyncEngineTestDriver.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTSyncEngineTestDriver.mm; sourceTree = ""; }; + 5492E02F20213FFC00B64F25 /* FSTMemorySpecTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTMemorySpecTests.mm; sourceTree = ""; }; + 5492E03020213FFC00B64F25 /* FSTSpecTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTSpecTests.mm; sourceTree = ""; }; + 5492E0362021401E00B64F25 /* FSTTestDispatchQueue.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTTestDispatchQueue.mm; sourceTree = ""; }; + 5492E0372021401E00B64F25 /* XCTestCase+Await.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "XCTestCase+Await.mm"; sourceTree = ""; }; + 5492E0382021401E00B64F25 /* FSTAssertTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTAssertTests.mm; sourceTree = ""; }; + 5492E0392021401F00B64F25 /* FSTEventAccumulator.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTEventAccumulator.mm; sourceTree = ""; }; + 5492E03A2021401F00B64F25 /* FSTHelpers.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTHelpers.mm; sourceTree = ""; }; + 5492E045202154AA00B64F25 /* FIRCollectionReferenceTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRCollectionReferenceTests.mm; sourceTree = ""; }; + 5492E046202154AA00B64F25 /* FIRQueryTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRQueryTests.mm; sourceTree = ""; }; + 5492E047202154AA00B64F25 /* FSTAPIHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTAPIHelpers.h; sourceTree = ""; }; + 5492E048202154AA00B64F25 /* FIRGeoPointTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRGeoPointTests.mm; sourceTree = ""; }; + 5492E049202154AA00B64F25 /* FIRDocumentReferenceTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRDocumentReferenceTests.mm; sourceTree = ""; }; + 5492E04A202154AA00B64F25 /* FIRFieldValueTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRFieldValueTests.mm; sourceTree = ""; }; + 5492E04B202154AA00B64F25 /* FIRDocumentSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRDocumentSnapshotTests.mm; sourceTree = ""; }; + 5492E04C202154AA00B64F25 /* FIRFieldPathTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRFieldPathTests.mm; sourceTree = ""; }; + 5492E04D202154AA00B64F25 /* FIRSnapshotMetadataTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRSnapshotMetadataTests.mm; sourceTree = ""; }; + 5492E04E202154AA00B64F25 /* FSTAPIHelpers.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTAPIHelpers.mm; sourceTree = ""; }; + 5492E04F202154AA00B64F25 /* FIRQuerySnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRQuerySnapshotTests.mm; sourceTree = ""; }; + 5492E05A202154B800B64F25 /* FSTSyncEngine+Testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "FSTSyncEngine+Testing.h"; sourceTree = ""; }; + 5492E05B202154B800B64F25 /* FSTTimestampTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTTimestampTests.mm; sourceTree = ""; }; + 5492E05C202154B800B64F25 /* FSTViewSnapshotTest.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTViewSnapshotTest.mm; sourceTree = ""; }; + 5492E05D202154B900B64F25 /* FSTQueryListenerTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTQueryListenerTests.mm; sourceTree = ""; }; + 5492E05E202154B900B64F25 /* FSTViewTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTViewTests.mm; sourceTree = ""; }; + 5492E05F202154B900B64F25 /* FSTDatabaseInfoTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTDatabaseInfoTests.mm; sourceTree = ""; }; + 5492E060202154B900B64F25 /* FSTEventManagerTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTEventManagerTests.mm; sourceTree = ""; }; + 5492E061202154B900B64F25 /* FSTQueryTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTQueryTests.mm; sourceTree = ""; }; + 5492E069202154D500B64F25 /* FIRQueryTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRQueryTests.mm; sourceTree = ""; }; + 5492E06A202154D500B64F25 /* FIRFieldsTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRFieldsTests.mm; sourceTree = ""; }; + 5492E06B202154D500B64F25 /* FIRListenerRegistrationTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRListenerRegistrationTests.mm; sourceTree = ""; }; + 5492E06C202154D500B64F25 /* FIRDatabaseTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRDatabaseTests.mm; sourceTree = ""; }; + 5492E06D202154D600B64F25 /* FIRValidationTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRValidationTests.mm; sourceTree = ""; }; + 5492E06E202154D600B64F25 /* FIRServerTimestampTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRServerTimestampTests.mm; sourceTree = ""; }; + 5492E06F202154D600B64F25 /* FIRWriteBatchTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRWriteBatchTests.mm; sourceTree = ""; }; + 5492E070202154D600B64F25 /* FIRCursorTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRCursorTests.mm; sourceTree = ""; }; + 5492E071202154D600B64F25 /* FIRTypeTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRTypeTests.mm; sourceTree = ""; }; + 5492E07B202154EB00B64F25 /* FSTTransactionTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTTransactionTests.mm; sourceTree = ""; }; + 5492E07C202154EB00B64F25 /* FSTSmokeTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTSmokeTests.mm; sourceTree = ""; }; + 5492E07D202154EB00B64F25 /* FSTStreamTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTStreamTests.mm; sourceTree = ""; }; + 5492E07E202154EC00B64F25 /* FSTDatastoreTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTDatastoreTests.mm; sourceTree = ""; }; + 5492E0832021552A00B64F25 /* FSTLocalStoreTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLocalStoreTests.mm; sourceTree = ""; }; + 5492E0842021552A00B64F25 /* FSTEagerGarbageCollectorTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTEagerGarbageCollectorTests.mm; sourceTree = ""; }; + 5492E0852021552A00B64F25 /* FSTRemoteDocumentCacheTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTRemoteDocumentCacheTests.h; sourceTree = ""; }; + 5492E0862021552A00B64F25 /* FSTLevelDBMigrationsTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBMigrationsTests.mm; sourceTree = ""; }; + 5492E0872021552A00B64F25 /* FSTLevelDBMutationQueueTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBMutationQueueTests.mm; sourceTree = ""; }; + 5492E0882021552A00B64F25 /* FSTMemoryLocalStoreTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTMemoryLocalStoreTests.mm; sourceTree = ""; }; + 5492E0892021552A00B64F25 /* FSTQueryCacheTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTQueryCacheTests.mm; sourceTree = ""; }; + 5492E08A2021552A00B64F25 /* FSTLocalSerializerTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLocalSerializerTests.mm; sourceTree = ""; }; + 5492E08B2021552B00B64F25 /* FSTMemoryQueryCacheTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTMemoryQueryCacheTests.mm; sourceTree = ""; }; + 5492E08C2021552B00B64F25 /* FSTMemoryRemoteDocumentCacheTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTMemoryRemoteDocumentCacheTests.mm; sourceTree = ""; }; + 5492E08D2021552B00B64F25 /* FSTPersistenceTestHelpers.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTPersistenceTestHelpers.mm; sourceTree = ""; }; + 5492E08E2021552B00B64F25 /* FSTLevelDBKeyTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBKeyTests.mm; sourceTree = ""; }; + 5492E08F2021552B00B64F25 /* FSTLevelDBLocalStoreTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBLocalStoreTests.mm; sourceTree = ""; }; + 5492E0902021552B00B64F25 /* FSTRemoteDocumentChangeBufferTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTRemoteDocumentChangeBufferTests.mm; sourceTree = ""; }; + 5492E0912021552B00B64F25 /* FSTLocalStoreTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTLocalStoreTests.h; sourceTree = ""; }; + 5492E0922021552B00B64F25 /* FSTLevelDBRemoteDocumentCacheTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBRemoteDocumentCacheTests.mm; sourceTree = ""; }; + 5492E0932021552B00B64F25 /* StringViewTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = StringViewTests.mm; sourceTree = ""; }; + 5492E0942021552C00B64F25 /* FSTMutationQueueTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTMutationQueueTests.h; sourceTree = ""; }; + 5492E0952021552C00B64F25 /* FSTQueryCacheTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTQueryCacheTests.h; sourceTree = ""; }; + 5492E0962021552C00B64F25 /* FSTMutationQueueTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTMutationQueueTests.mm; sourceTree = ""; }; + 5492E0972021552C00B64F25 /* FSTMemoryMutationQueueTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTMemoryMutationQueueTests.mm; sourceTree = ""; }; + 5492E0982021552C00B64F25 /* FSTLevelDBQueryCacheTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBQueryCacheTests.mm; sourceTree = ""; }; + 5492E0992021552C00B64F25 /* FSTPersistenceTestHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTPersistenceTestHelpers.h; sourceTree = ""; }; + 5492E09A2021552C00B64F25 /* FSTReferenceSetTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTReferenceSetTests.mm; sourceTree = ""; }; + 5492E09B2021552C00B64F25 /* FSTWriteGroupTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTWriteGroupTests.mm; sourceTree = ""; }; + 5492E09C2021552D00B64F25 /* FSTRemoteDocumentCacheTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTRemoteDocumentCacheTests.mm; sourceTree = ""; }; + 5492E0B22021555000B64F25 /* FSTDocumentKeyTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTDocumentKeyTests.mm; sourceTree = ""; }; + 5492E0B32021555100B64F25 /* FSTDocumentSetTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTDocumentSetTests.mm; sourceTree = ""; }; + 5492E0B42021555100B64F25 /* FSTDatabaseIDTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTDatabaseIDTests.mm; sourceTree = ""; }; + 5492E0B52021555100B64F25 /* FSTPathTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTPathTests.mm; sourceTree = ""; }; + 5492E0B62021555100B64F25 /* FSTDocumentTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTDocumentTests.mm; sourceTree = ""; }; + 5492E0B72021555100B64F25 /* FSTMutationTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTMutationTests.mm; sourceTree = ""; }; + 5492E0B82021555100B64F25 /* FSTFieldValueTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTFieldValueTests.mm; sourceTree = ""; }; + 5492E0C02021557E00B64F25 /* FSTWatchChange+Testing.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "FSTWatchChange+Testing.mm"; sourceTree = ""; }; + 5492E0C12021557E00B64F25 /* FSTSerializerBetaTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTSerializerBetaTests.mm; sourceTree = ""; }; + 5492E0C22021557E00B64F25 /* FSTDatastoreTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTDatastoreTests.mm; sourceTree = ""; }; + 5492E0C32021557E00B64F25 /* FSTRemoteEventTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTRemoteEventTests.mm; sourceTree = ""; }; + 5492E0C42021557E00B64F25 /* FSTWatchChange+Testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "FSTWatchChange+Testing.h"; sourceTree = ""; }; + 5492E0C52021557E00B64F25 /* FSTWatchChangeTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTWatchChangeTests.mm; sourceTree = ""; }; 54C2294E1FECABAE007D065B /* log_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = log_test.cc; path = ../../core/test/firebase/firestore/util/log_test.cc; sourceTree = ""; }; 54DA129C1F315EE100DD57A1 /* collection_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = collection_spec_test.json; sourceTree = ""; }; 54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = existence_filter_spec_test.json; sourceTree = ""; }; @@ -220,12 +303,9 @@ 54DA12A31F315EE100DD57A1 /* persistence_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = persistence_spec_test.json; sourceTree = ""; }; 54DA12A41F315EE100DD57A1 /* resume_token_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = resume_token_spec_test.json; sourceTree = ""; }; 54DA12A51F315EE100DD57A1 /* write_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = write_spec_test.json; sourceTree = ""; }; - 54DA12B01F315F3800DD57A1 /* FIRValidationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRValidationTests.m; sourceTree = ""; }; 54E9281C1F33950B00C1953E /* FSTEventAccumulator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTEventAccumulator.h; sourceTree = ""; }; - 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FSTEventAccumulator.m; sourceTree = ""; }; 54E9281E1F33950B00C1953E /* FSTIntegrationTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTIntegrationTestCase.h; sourceTree = ""; }; 54E9282A1F339CAD00C1953E /* XCTestCase+Await.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTestCase+Await.h"; sourceTree = ""; }; - 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCTestCase+Await.m"; sourceTree = ""; }; 6003F58A195388D20070C39A /* Firestore_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Firestore_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6003F58D195388D20070C39A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 6003F58F195388D20070C39A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; @@ -242,7 +322,6 @@ 6003F5AF195388D20070C39A /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 6003F5B7195388D20070C39A /* Tests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Tests-Info.plist"; sourceTree = ""; }; 6003F5B9195388D20070C39A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - 61E1D8AF1FCF6AF500753285 /* StringViewTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = StringViewTests.mm; sourceTree = ""; }; 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 71719F9E1E33DC2100824A3D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -253,24 +332,15 @@ AB356EF6200EA5EB0089B766 /* field_value_test.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = field_value_test.cc; sourceTree = ""; }; AB380CF82019382300D97691 /* target_id_generator_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = target_id_generator_test.cc; sourceTree = ""; }; AB380CFC201A2EE200D97691 /* string_util_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = string_util_test.cc; path = ../../core/test/firebase/firestore/util/string_util_test.cc; sourceTree = ""; }; - AB382F7B1FE02A1F007CA955 /* FIRDocumentReferenceTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRDocumentReferenceTests.m; sourceTree = ""; }; - AB382F7D1FE03059007CA955 /* FIRFieldPathTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRFieldPathTests.m; sourceTree = ""; }; + AB380D01201BC69F00D97691 /* bits_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = bits_test.cc; path = ../../core/test/firebase/firestore/util/bits_test.cc; sourceTree = ""; }; + AB380D03201BC6E400D97691 /* ordered_code_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = ordered_code_test.cc; path = ../../core/test/firebase/firestore/util/ordered_code_test.cc; sourceTree = ""; }; + AB71064B201FA60300344F18 /* database_id_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = database_id_test.cc; sourceTree = ""; }; AB7BAB332012B519001E0872 /* geo_point_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = geo_point_test.cc; path = ../../core/test/firebase/firestore/geo_point_test.cc; sourceTree = ""; }; - AB9945251FE2D71100DFC1E6 /* FIRCollectionReferenceTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRCollectionReferenceTests.m; sourceTree = ""; }; - AB9945271FE2DE0C00DFC1E6 /* FIRSnapshotMetadataTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRSnapshotMetadataTests.m; sourceTree = ""; }; - AB9945291FE2F9EB00DFC1E6 /* FIRDocumentSnapshotTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRDocumentSnapshotTests.m; sourceTree = ""; }; - AB99452B1FE3018D00DFC1E6 /* FIRQuerySnapshotTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRQuerySnapshotTests.m; sourceTree = ""; }; - AB99452D1FE30AC800DFC1E6 /* FIRFieldValueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRFieldValueTests.m; sourceTree = ""; }; - ABAEEF4E1FD5F8B100C966CB /* FIRQueryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRQueryTests.m; sourceTree = ""; }; - ABF341011FE858B500C48322 /* FSTAPIHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTAPIHelpers.h; sourceTree = ""; }; - ABF341021FE8593500C48322 /* FSTAPIHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTAPIHelpers.m; sourceTree = ""; }; ABF6506B201131F8005F2C74 /* timestamp_test.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = timestamp_test.cc; sourceTree = ""; }; B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_IntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CE00BABB5A3AAB44A4C209E2 /* Pods-Firestore_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests.debug.xcconfig"; sourceTree = ""; }; D3CC3DC5338DCAF43A211155 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; - D5B25292CED31B81FDED0411 /* FSTTestDispatchQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FSTTestDispatchQueue.m; sourceTree = ""; }; D5B259DAA9149B80D6245B57 /* FSTTestDispatchQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTTestDispatchQueue.h; sourceTree = ""; }; - D5B25C0D4AADFCA3ADB883E4 /* FSTStreamTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FSTStreamTests.m; sourceTree = ""; }; DB17FEDFB80770611A935A60 /* Pods-Firestore_IntegrationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests.release.xcconfig"; sourceTree = ""; }; DE03B2E91F2149D600A30B9C /* Firestore_IntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firestore_IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DE03B3621F215E1600A30B9C /* CAcert.pem */ = {isa = PBXFileReference; lastKnownFileType = text; path = CAcert.pem; sourceTree = ""; }; @@ -283,75 +353,11 @@ DE2EF0821F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "FSTImmutableSortedSet+Testing.m"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.m"; sourceTree = ""; }; DE2EF0831F3D0B6E003D0CDC /* FSTLLRBValueNode+Test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FSTLLRBValueNode+Test.h"; path = "../../third_party/Immutable/Tests/FSTLLRBValueNode+Test.h"; sourceTree = ""; }; DE2EF0841F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FSTTreeSortedDictionaryTests.m; path = ../../third_party/Immutable/Tests/FSTTreeSortedDictionaryTests.m; sourceTree = ""; }; - DE51B1631F0D48AC0013853F /* FSTEagerGarbageCollectorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTEagerGarbageCollectorTests.m; sourceTree = ""; }; - DE51B1641F0D48AC0013853F /* FSTLevelDBKeyTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBKeyTests.mm; sourceTree = ""; }; - DE51B1651F0D48AC0013853F /* FSTLevelDBLocalStoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLevelDBLocalStoreTests.m; sourceTree = ""; }; - DE51B1661F0D48AC0013853F /* FSTLevelDBMutationQueueTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBMutationQueueTests.mm; sourceTree = ""; }; - DE51B1671F0D48AC0013853F /* FSTLevelDBQueryCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLevelDBQueryCacheTests.m; sourceTree = ""; }; - DE51B1681F0D48AC0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBRemoteDocumentCacheTests.mm; sourceTree = ""; }; - DE51B1691F0D48AC0013853F /* FSTLocalSerializerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLocalSerializerTests.m; sourceTree = ""; }; - DE51B16A1F0D48AC0013853F /* FSTLocalStoreTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTLocalStoreTests.h; sourceTree = ""; }; - DE51B16B1F0D48AC0013853F /* FSTLocalStoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLocalStoreTests.m; sourceTree = ""; }; - DE51B16C1F0D48AC0013853F /* FSTMemoryLocalStoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryLocalStoreTests.m; sourceTree = ""; }; - DE51B16D1F0D48AC0013853F /* FSTMemoryMutationQueueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryMutationQueueTests.m; sourceTree = ""; }; - DE51B16E1F0D48AC0013853F /* FSTMemoryQueryCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryQueryCacheTests.m; sourceTree = ""; }; - DE51B16F1F0D48AC0013853F /* FSTMemoryRemoteDocumentCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryRemoteDocumentCacheTests.m; sourceTree = ""; }; - DE51B1701F0D48AC0013853F /* FSTMutationQueueTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTMutationQueueTests.h; sourceTree = ""; }; - DE51B1711F0D48AC0013853F /* FSTMutationQueueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMutationQueueTests.m; sourceTree = ""; }; - DE51B1721F0D48AC0013853F /* FSTPersistenceTestHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTPersistenceTestHelpers.h; sourceTree = ""; }; - DE51B1731F0D48AC0013853F /* FSTPersistenceTestHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTPersistenceTestHelpers.m; sourceTree = ""; }; - DE51B1741F0D48AC0013853F /* FSTQueryCacheTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTQueryCacheTests.h; sourceTree = ""; }; - DE51B1751F0D48AC0013853F /* FSTQueryCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTQueryCacheTests.m; sourceTree = ""; }; - DE51B1761F0D48AC0013853F /* FSTReferenceSetTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTReferenceSetTests.m; sourceTree = ""; }; - DE51B1771F0D48AC0013853F /* FSTRemoteDocumentCacheTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTRemoteDocumentCacheTests.h; sourceTree = ""; }; - DE51B1781F0D48AC0013853F /* FSTRemoteDocumentCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTRemoteDocumentCacheTests.m; sourceTree = ""; }; - DE51B1791F0D48AC0013853F /* FSTRemoteDocumentChangeBufferTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTRemoteDocumentChangeBufferTests.m; sourceTree = ""; }; - DE51B17A1F0D48AC0013853F /* FSTWriteGroupTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTWriteGroupTests.mm; sourceTree = ""; }; - DE51B17C1F0D48AC0013853F /* FSTDatabaseIDTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatabaseIDTests.m; sourceTree = ""; }; - DE51B17D1F0D48AC0013853F /* FSTDocumentKeyTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDocumentKeyTests.m; sourceTree = ""; }; - DE51B17E1F0D48AC0013853F /* FSTDocumentSetTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDocumentSetTests.m; sourceTree = ""; }; - DE51B17F1F0D48AC0013853F /* FSTDocumentTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDocumentTests.m; sourceTree = ""; }; - DE51B1801F0D48AC0013853F /* FSTFieldValueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTFieldValueTests.m; sourceTree = ""; }; - DE51B1811F0D48AC0013853F /* FSTMutationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMutationTests.m; sourceTree = ""; }; - DE51B1821F0D48AC0013853F /* FSTPathTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTPathTests.m; sourceTree = ""; }; - DE51B1841F0D48AC0013853F /* FIRGeoPointTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRGeoPointTests.m; sourceTree = ""; }; - DE51B1861F0D48AC0013853F /* FSTAssertTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTAssertTests.m; sourceTree = ""; }; DE51B1881F0D48AC0013853F /* FSTHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTHelpers.h; sourceTree = ""; }; - DE51B1891F0D48AC0013853F /* FSTHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTHelpers.m; sourceTree = ""; }; - DE51B1941F0D48AC0013853F /* FSTLevelDBSpecTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLevelDBSpecTests.m; sourceTree = ""; }; - DE51B1951F0D48AC0013853F /* FSTMemorySpecTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemorySpecTests.m; sourceTree = ""; }; DE51B1961F0D48AC0013853F /* FSTMockDatastore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTMockDatastore.h; sourceTree = ""; }; - DE51B1971F0D48AC0013853F /* FSTMockDatastore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMockDatastore.m; sourceTree = ""; }; DE51B1981F0D48AC0013853F /* FSTSpecTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTSpecTests.h; sourceTree = ""; }; - DE51B1991F0D48AC0013853F /* FSTSpecTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSpecTests.m; sourceTree = ""; }; DE51B19A1F0D48AC0013853F /* FSTSyncEngineTestDriver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTSyncEngineTestDriver.h; sourceTree = ""; }; - DE51B19B1F0D48AC0013853F /* FSTSyncEngineTestDriver.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSyncEngineTestDriver.m; sourceTree = ""; }; DE51B1A71F0D48AC0013853F /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; - DE51B1A91F0D48AC0013853F /* FSTDatabaseInfoTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatabaseInfoTests.m; sourceTree = ""; }; - DE51B1AA1F0D48AC0013853F /* FSTEventManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTEventManagerTests.m; sourceTree = ""; }; - DE51B1AB1F0D48AC0013853F /* FSTQueryListenerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTQueryListenerTests.m; sourceTree = ""; }; - DE51B1AC1F0D48AC0013853F /* FSTQueryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTQueryTests.m; sourceTree = ""; }; - DE51B1AD1F0D48AC0013853F /* FSTSyncEngine+Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FSTSyncEngine+Testing.h"; sourceTree = ""; }; - DE51B1AF1F0D48AC0013853F /* FSTTimestampTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTTimestampTests.m; sourceTree = ""; }; - DE51B1B01F0D48AC0013853F /* FSTViewSnapshotTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTViewSnapshotTest.m; sourceTree = ""; }; - DE51B1B11F0D48AC0013853F /* FSTViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTViewTests.m; sourceTree = ""; }; - DE51B1B31F0D48AC0013853F /* FSTDatastoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatastoreTests.m; sourceTree = ""; }; - DE51B1B41F0D48AC0013853F /* FSTRemoteEventTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTRemoteEventTests.m; sourceTree = ""; }; - DE51B1B61F0D48AC0013853F /* FSTSerializerBetaTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSerializerBetaTests.m; sourceTree = ""; }; - DE51B1B81F0D48AC0013853F /* FSTWatchChange+Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FSTWatchChange+Testing.h"; sourceTree = ""; }; - DE51B1B91F0D48AC0013853F /* FSTWatchChange+Testing.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FSTWatchChange+Testing.m"; sourceTree = ""; }; - DE51B1BA1F0D48AC0013853F /* FSTWatchChangeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTWatchChangeTests.m; sourceTree = ""; }; - DE51B1BD1F0D48AC0013853F /* FIRCursorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRCursorTests.m; sourceTree = ""; }; - DE51B1BE1F0D48AC0013853F /* FIRDatabaseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRDatabaseTests.m; sourceTree = ""; }; - DE51B1BF1F0D48AC0013853F /* FIRFieldsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRFieldsTests.m; sourceTree = ""; }; - DE51B1C01F0D48AC0013853F /* FIRListenerRegistrationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRListenerRegistrationTests.m; sourceTree = ""; }; - DE51B1C11F0D48AC0013853F /* FIRQueryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRQueryTests.m; sourceTree = ""; }; - DE51B1C21F0D48AC0013853F /* FIRServerTimestampTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRServerTimestampTests.m; sourceTree = ""; }; - DE51B1C31F0D48AC0013853F /* FIRTypeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRTypeTests.m; sourceTree = ""; }; - DE51B1C41F0D48AC0013853F /* FSTDatastoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatastoreTests.m; sourceTree = ""; }; - DE51B1C51F0D48AC0013853F /* FSTSmokeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSmokeTests.m; sourceTree = ""; }; - DE51B1C61F0D48AC0013853F /* FSTTransactionTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTTransactionTests.m; sourceTree = ""; }; - DEFE0F471F1F960A0071599A /* FIRWriteBatchTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRWriteBatchTests.m; sourceTree = ""; }; F23325524BEAF8D24F78AC88 /* Pods-SwiftBuildTest.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftBuildTest.release.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -406,8 +412,10 @@ children = ( 548DB926200D590300E00ABC /* assert_test.cc */, 54740A521FC913E500713A1A /* autoid_test.cc */, + AB380D01201BC69F00D97691 /* bits_test.cc */, 548DB928200D59F600E00ABC /* comparison_test.cc */, 54C2294E1FECABAE007D065B /* log_test.cc */, + AB380D03201BC6E400D97691 /* ordered_code_test.cc */, 54740A531FC913E500713A1A /* secure_random_test.cc */, 5436F32320008FAD006E51E3 /* string_printf_test.cc */, AB380CFC201A2EE200D97691 /* string_util_test.cc */, @@ -420,7 +428,6 @@ children = ( AB380CF7201937B800D97691 /* core */, AB356EF5200E9D1A0089B766 /* model */, - 54764FAD1FAA0C650085E60A /* Port */, 54740A561FC913EB00713A1A /* util */, 54764FAE1FAA21B90085E60A /* FSTGoogleTestTests.mm */, AB7BAB332012B519001E0872 /* geo_point_test.cc */, @@ -428,13 +435,6 @@ name = GoogleTests; sourceTree = ""; }; - 54764FAD1FAA0C650085E60A /* Port */ = { - isa = PBXGroup; - children = ( - ); - name = Port; - sourceTree = ""; - }; 6003F581195388D10070C39A = { isa = PBXGroup; children = ( @@ -555,6 +555,7 @@ AB356EF5200E9D1A0089B766 /* model */ = { isa = PBXGroup; children = ( + AB71064B201FA60300344F18 /* database_id_test.cc */, AB356EF6200EA5EB0089B766 /* field_value_test.cc */, ABF6506B201131F8005F2C74 /* timestamp_test.cc */, ); @@ -596,32 +597,32 @@ DE51B1621F0D48AC0013853F /* Local */ = { isa = PBXGroup; children = ( - 61E1D8AF1FCF6AF500753285 /* StringViewTests.mm */, - DE51B16A1F0D48AC0013853F /* FSTLocalStoreTests.h */, - DE51B1701F0D48AC0013853F /* FSTMutationQueueTests.h */, - DE51B1721F0D48AC0013853F /* FSTPersistenceTestHelpers.h */, - DE51B1741F0D48AC0013853F /* FSTQueryCacheTests.h */, - DE51B1771F0D48AC0013853F /* FSTRemoteDocumentCacheTests.h */, - DE51B1631F0D48AC0013853F /* FSTEagerGarbageCollectorTests.m */, - DE51B1651F0D48AC0013853F /* FSTLevelDBLocalStoreTests.m */, - DE51B1671F0D48AC0013853F /* FSTLevelDBQueryCacheTests.m */, - DE51B1691F0D48AC0013853F /* FSTLocalSerializerTests.m */, - DE51B16B1F0D48AC0013853F /* FSTLocalStoreTests.m */, - DE51B16C1F0D48AC0013853F /* FSTMemoryLocalStoreTests.m */, - DE51B16D1F0D48AC0013853F /* FSTMemoryMutationQueueTests.m */, - DE51B16E1F0D48AC0013853F /* FSTMemoryQueryCacheTests.m */, - DE51B16F1F0D48AC0013853F /* FSTMemoryRemoteDocumentCacheTests.m */, - DE51B1711F0D48AC0013853F /* FSTMutationQueueTests.m */, - DE51B1731F0D48AC0013853F /* FSTPersistenceTestHelpers.m */, - DE51B1751F0D48AC0013853F /* FSTQueryCacheTests.m */, - DE51B1761F0D48AC0013853F /* FSTReferenceSetTests.m */, - DE51B1781F0D48AC0013853F /* FSTRemoteDocumentCacheTests.m */, - DE51B1791F0D48AC0013853F /* FSTRemoteDocumentChangeBufferTests.m */, - DE51B1641F0D48AC0013853F /* FSTLevelDBKeyTests.mm */, - DE51B1661F0D48AC0013853F /* FSTLevelDBMutationQueueTests.mm */, - DE51B1681F0D48AC0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm */, - DE51B17A1F0D48AC0013853F /* FSTWriteGroupTests.mm */, - 132E36114FC5BA5DBE3CB260 /* FSTLevelDBMigrationsTests.mm */, + 5492E0842021552A00B64F25 /* FSTEagerGarbageCollectorTests.mm */, + 5492E08E2021552B00B64F25 /* FSTLevelDBKeyTests.mm */, + 5492E08F2021552B00B64F25 /* FSTLevelDBLocalStoreTests.mm */, + 5492E0862021552A00B64F25 /* FSTLevelDBMigrationsTests.mm */, + 5492E0872021552A00B64F25 /* FSTLevelDBMutationQueueTests.mm */, + 5492E0982021552C00B64F25 /* FSTLevelDBQueryCacheTests.mm */, + 5492E0922021552B00B64F25 /* FSTLevelDBRemoteDocumentCacheTests.mm */, + 5492E08A2021552A00B64F25 /* FSTLocalSerializerTests.mm */, + 5492E0912021552B00B64F25 /* FSTLocalStoreTests.h */, + 5492E0832021552A00B64F25 /* FSTLocalStoreTests.mm */, + 5492E0882021552A00B64F25 /* FSTMemoryLocalStoreTests.mm */, + 5492E0972021552C00B64F25 /* FSTMemoryMutationQueueTests.mm */, + 5492E08B2021552B00B64F25 /* FSTMemoryQueryCacheTests.mm */, + 5492E08C2021552B00B64F25 /* FSTMemoryRemoteDocumentCacheTests.mm */, + 5492E0942021552C00B64F25 /* FSTMutationQueueTests.h */, + 5492E0962021552C00B64F25 /* FSTMutationQueueTests.mm */, + 5492E0992021552C00B64F25 /* FSTPersistenceTestHelpers.h */, + 5492E08D2021552B00B64F25 /* FSTPersistenceTestHelpers.mm */, + 5492E0952021552C00B64F25 /* FSTQueryCacheTests.h */, + 5492E0892021552A00B64F25 /* FSTQueryCacheTests.mm */, + 5492E09A2021552C00B64F25 /* FSTReferenceSetTests.mm */, + 5492E0852021552A00B64F25 /* FSTRemoteDocumentCacheTests.h */, + 5492E09C2021552D00B64F25 /* FSTRemoteDocumentCacheTests.mm */, + 5492E0902021552B00B64F25 /* FSTRemoteDocumentChangeBufferTests.mm */, + 5492E09B2021552C00B64F25 /* FSTWriteGroupTests.mm */, + 5492E0932021552B00B64F25 /* StringViewTests.mm */, ); path = Local; sourceTree = ""; @@ -629,13 +630,13 @@ DE51B17B1F0D48AC0013853F /* Model */ = { isa = PBXGroup; children = ( - DE51B17C1F0D48AC0013853F /* FSTDatabaseIDTests.m */, - DE51B17D1F0D48AC0013853F /* FSTDocumentKeyTests.m */, - DE51B17E1F0D48AC0013853F /* FSTDocumentSetTests.m */, - DE51B17F1F0D48AC0013853F /* FSTDocumentTests.m */, - DE51B1801F0D48AC0013853F /* FSTFieldValueTests.m */, - DE51B1811F0D48AC0013853F /* FSTMutationTests.m */, - DE51B1821F0D48AC0013853F /* FSTPathTests.m */, + 5492E0B42021555100B64F25 /* FSTDatabaseIDTests.mm */, + 5492E0B22021555000B64F25 /* FSTDocumentKeyTests.mm */, + 5492E0B32021555100B64F25 /* FSTDocumentSetTests.mm */, + 5492E0B62021555100B64F25 /* FSTDocumentTests.mm */, + 5492E0B82021555100B64F25 /* FSTFieldValueTests.mm */, + 5492E0B72021555100B64F25 /* FSTMutationTests.mm */, + 5492E0B52021555100B64F25 /* FSTPathTests.mm */, ); path = Model; sourceTree = ""; @@ -643,17 +644,17 @@ DE51B1831F0D48AC0013853F /* API */ = { isa = PBXGroup; children = ( - AB99452D1FE30AC800DFC1E6 /* FIRFieldValueTests.m */, - AB99452B1FE3018D00DFC1E6 /* FIRQuerySnapshotTests.m */, - AB9945291FE2F9EB00DFC1E6 /* FIRDocumentSnapshotTests.m */, - AB9945271FE2DE0C00DFC1E6 /* FIRSnapshotMetadataTests.m */, - AB9945251FE2D71100DFC1E6 /* FIRCollectionReferenceTests.m */, - AB382F7D1FE03059007CA955 /* FIRFieldPathTests.m */, - AB382F7B1FE02A1F007CA955 /* FIRDocumentReferenceTests.m */, - DE51B1841F0D48AC0013853F /* FIRGeoPointTests.m */, - ABAEEF4E1FD5F8B100C966CB /* FIRQueryTests.m */, - ABF341011FE858B500C48322 /* FSTAPIHelpers.h */, - ABF341021FE8593500C48322 /* FSTAPIHelpers.m */, + 5492E045202154AA00B64F25 /* FIRCollectionReferenceTests.mm */, + 5492E049202154AA00B64F25 /* FIRDocumentReferenceTests.mm */, + 5492E04B202154AA00B64F25 /* FIRDocumentSnapshotTests.mm */, + 5492E04C202154AA00B64F25 /* FIRFieldPathTests.mm */, + 5492E04A202154AA00B64F25 /* FIRFieldValueTests.mm */, + 5492E048202154AA00B64F25 /* FIRGeoPointTests.mm */, + 5492E04F202154AA00B64F25 /* FIRQuerySnapshotTests.mm */, + 5492E046202154AA00B64F25 /* FIRQueryTests.mm */, + 5492E04D202154AA00B64F25 /* FIRSnapshotMetadataTests.mm */, + 5492E047202154AA00B64F25 /* FSTAPIHelpers.h */, + 5492E04E202154AA00B64F25 /* FSTAPIHelpers.mm */, ); path = API; sourceTree = ""; @@ -661,17 +662,17 @@ DE51B1851F0D48AC0013853F /* Util */ = { isa = PBXGroup; children = ( + 5492E0382021401E00B64F25 /* FSTAssertTests.mm */, 54E9281C1F33950B00C1953E /* FSTEventAccumulator.h */, - 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */, + 5492E0392021401F00B64F25 /* FSTEventAccumulator.mm */, + DE51B1881F0D48AC0013853F /* FSTHelpers.h */, + 5492E03A2021401F00B64F25 /* FSTHelpers.mm */, 54E9281E1F33950B00C1953E /* FSTIntegrationTestCase.h */, 5491BC711FB44593008B3588 /* FSTIntegrationTestCase.mm */, - DE51B1861F0D48AC0013853F /* FSTAssertTests.m */, - DE51B1881F0D48AC0013853F /* FSTHelpers.h */, - DE51B1891F0D48AC0013853F /* FSTHelpers.m */, - 54E9282A1F339CAD00C1953E /* XCTestCase+Await.h */, - 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */, D5B259DAA9149B80D6245B57 /* FSTTestDispatchQueue.h */, - D5B25292CED31B81FDED0411 /* FSTTestDispatchQueue.m */, + 5492E0362021401E00B64F25 /* FSTTestDispatchQueue.mm */, + 54E9282A1F339CAD00C1953E /* XCTestCase+Await.h */, + 5492E0372021401E00B64F25 /* XCTestCase+Await.mm */, ); path = Util; sourceTree = ""; @@ -679,14 +680,14 @@ DE51B1931F0D48AC0013853F /* SpecTests */ = { isa = PBXGroup; children = ( + 5492E02C20213FFB00B64F25 /* FSTLevelDBSpecTests.mm */, + 5492E02F20213FFC00B64F25 /* FSTMemorySpecTests.mm */, DE51B1961F0D48AC0013853F /* FSTMockDatastore.h */, + 5492E02D20213FFC00B64F25 /* FSTMockDatastore.mm */, DE51B1981F0D48AC0013853F /* FSTSpecTests.h */, + 5492E03020213FFC00B64F25 /* FSTSpecTests.mm */, DE51B19A1F0D48AC0013853F /* FSTSyncEngineTestDriver.h */, - DE51B1941F0D48AC0013853F /* FSTLevelDBSpecTests.m */, - DE51B1951F0D48AC0013853F /* FSTMemorySpecTests.m */, - DE51B1971F0D48AC0013853F /* FSTMockDatastore.m */, - DE51B1991F0D48AC0013853F /* FSTSpecTests.m */, - DE51B19B1F0D48AC0013853F /* FSTSyncEngineTestDriver.m */, + 5492E02E20213FFC00B64F25 /* FSTSyncEngineTestDriver.mm */, DE51B19C1F0D48AC0013853F /* json */, ); path = SpecTests; @@ -714,14 +715,14 @@ DE51B1A81F0D48AC0013853F /* Core */ = { isa = PBXGroup; children = ( - DE51B1AD1F0D48AC0013853F /* FSTSyncEngine+Testing.h */, - DE51B1A91F0D48AC0013853F /* FSTDatabaseInfoTests.m */, - DE51B1AA1F0D48AC0013853F /* FSTEventManagerTests.m */, - DE51B1AB1F0D48AC0013853F /* FSTQueryListenerTests.m */, - DE51B1AC1F0D48AC0013853F /* FSTQueryTests.m */, - DE51B1AF1F0D48AC0013853F /* FSTTimestampTests.m */, - DE51B1B01F0D48AC0013853F /* FSTViewSnapshotTest.m */, - DE51B1B11F0D48AC0013853F /* FSTViewTests.m */, + 5492E05F202154B900B64F25 /* FSTDatabaseInfoTests.mm */, + 5492E060202154B900B64F25 /* FSTEventManagerTests.mm */, + 5492E05D202154B900B64F25 /* FSTQueryListenerTests.mm */, + 5492E061202154B900B64F25 /* FSTQueryTests.mm */, + 5492E05A202154B800B64F25 /* FSTSyncEngine+Testing.h */, + 5492E05B202154B800B64F25 /* FSTTimestampTests.mm */, + 5492E05C202154B800B64F25 /* FSTViewSnapshotTest.mm */, + 5492E05E202154B900B64F25 /* FSTViewTests.mm */, ); path = Core; sourceTree = ""; @@ -729,12 +730,12 @@ DE51B1B21F0D48AC0013853F /* Remote */ = { isa = PBXGroup; children = ( - DE51B1B31F0D48AC0013853F /* FSTDatastoreTests.m */, - DE51B1B41F0D48AC0013853F /* FSTRemoteEventTests.m */, - DE51B1B61F0D48AC0013853F /* FSTSerializerBetaTests.m */, - DE51B1B81F0D48AC0013853F /* FSTWatchChange+Testing.h */, - DE51B1B91F0D48AC0013853F /* FSTWatchChange+Testing.m */, - DE51B1BA1F0D48AC0013853F /* FSTWatchChangeTests.m */, + 5492E0C22021557E00B64F25 /* FSTDatastoreTests.mm */, + 5492E0C32021557E00B64F25 /* FSTRemoteEventTests.mm */, + 5492E0C12021557E00B64F25 /* FSTSerializerBetaTests.mm */, + 5492E0C42021557E00B64F25 /* FSTWatchChange+Testing.h */, + 5492E0C02021557E00B64F25 /* FSTWatchChange+Testing.mm */, + 5492E0C52021557E00B64F25 /* FSTWatchChangeTests.mm */, ); path = Remote; sourceTree = ""; @@ -742,13 +743,12 @@ DE51B1BB1F0D48AC0013853F /* Integration */ = { isa = PBXGroup; children = ( - DE03B3621F215E1600A30B9C /* CAcert.pem */, DE51B1BC1F0D48AC0013853F /* API */, - DE51B1C41F0D48AC0013853F /* FSTDatastoreTests.m */, - DE51B1C51F0D48AC0013853F /* FSTSmokeTests.m */, - DE51B1C61F0D48AC0013853F /* FSTTransactionTests.m */, - DE51B1C71F0D48AC0013853F /* Util */, - D5B25C0D4AADFCA3ADB883E4 /* FSTStreamTests.m */, + DE03B3621F215E1600A30B9C /* CAcert.pem */, + 5492E07E202154EC00B64F25 /* FSTDatastoreTests.mm */, + 5492E07C202154EB00B64F25 /* FSTSmokeTests.mm */, + 5492E07D202154EB00B64F25 /* FSTStreamTests.mm */, + 5492E07B202154EB00B64F25 /* FSTTransactionTests.mm */, ); path = Integration; sourceTree = ""; @@ -756,26 +756,19 @@ DE51B1BC1F0D48AC0013853F /* API */ = { isa = PBXGroup; children = ( - DE51B1BD1F0D48AC0013853F /* FIRCursorTests.m */, - DE51B1BE1F0D48AC0013853F /* FIRDatabaseTests.m */, - DE51B1BF1F0D48AC0013853F /* FIRFieldsTests.m */, - DE51B1C01F0D48AC0013853F /* FIRListenerRegistrationTests.m */, - DE51B1C11F0D48AC0013853F /* FIRQueryTests.m */, - DE51B1C21F0D48AC0013853F /* FIRServerTimestampTests.m */, - DE51B1C31F0D48AC0013853F /* FIRTypeTests.m */, - 54DA12B01F315F3800DD57A1 /* FIRValidationTests.m */, - DEFE0F471F1F960A0071599A /* FIRWriteBatchTests.m */, + 5492E070202154D600B64F25 /* FIRCursorTests.mm */, + 5492E06C202154D500B64F25 /* FIRDatabaseTests.mm */, + 5492E06A202154D500B64F25 /* FIRFieldsTests.mm */, + 5492E06B202154D500B64F25 /* FIRListenerRegistrationTests.mm */, + 5492E069202154D500B64F25 /* FIRQueryTests.mm */, + 5492E06E202154D600B64F25 /* FIRServerTimestampTests.mm */, + 5492E071202154D600B64F25 /* FIRTypeTests.mm */, + 5492E06D202154D600B64F25 /* FIRValidationTests.mm */, + 5492E06F202154D600B64F25 /* FIRWriteBatchTests.mm */, ); path = API; sourceTree = ""; }; - DE51B1C71F0D48AC0013853F /* Util */ = { - isa = PBXGroup; - children = ( - ); - path = Util; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1226,82 +1219,85 @@ buildActionMask = 2147483647; files = ( DE2EF0881F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m in Sources */, - DE51B1FD1F0D492C0013853F /* FSTSpecTests.m in Sources */, - ABAEEF4F1FD5F8B100C966CB /* FIRQueryTests.m in Sources */, - ABF341051FE860CA00C48322 /* FSTAPIHelpers.m in Sources */, - DE51B1CC1F0D48C00013853F /* FIRGeoPointTests.m in Sources */, - DE51B1E11F0D490D0013853F /* FSTMemoryRemoteDocumentCacheTests.m in Sources */, - DE51B1FF1F0D493A0013853F /* FSTAssertTests.m in Sources */, - DE51B1D31F0D48CD0013853F /* FSTViewSnapshotTest.m in Sources */, + ABE6637A201FA81900ED349A /* database_id_test.cc in Sources */, + 5492E0AF2021552D00B64F25 /* FSTReferenceSetTests.mm in Sources */, + 5492E09E2021552D00B64F25 /* FSTEagerGarbageCollectorTests.mm in Sources */, + 5492E0C62021557E00B64F25 /* FSTWatchChange+Testing.mm in Sources */, + 5492E064202154B900B64F25 /* FSTQueryListenerTests.mm in Sources */, + 5492E03320213FFC00B64F25 /* FSTSyncEngineTestDriver.mm in Sources */, AB380CFE201A2F4500D97691 /* string_util_test.cc in Sources */, - DE51B1F91F0D491F0013853F /* FSTWatchChangeTests.m in Sources */, - DE51B1F81F0D491F0013853F /* FSTWatchChange+Testing.m in Sources */, - DE51B1EB1F0D490D0013853F /* FSTWriteGroupTests.mm in Sources */, + 5492E0A42021552D00B64F25 /* FSTMemoryQueryCacheTests.mm in Sources */, + 5492E0A92021552D00B64F25 /* FSTRemoteDocumentChangeBufferTests.mm in Sources */, 54C2294F1FECABAE007D065B /* log_test.cc in Sources */, - DE51B2011F0D493E0013853F /* FSTHelpers.m in Sources */, + 5492E0CA2021557E00B64F25 /* FSTWatchChangeTests.mm in Sources */, + 5492E063202154B900B64F25 /* FSTViewSnapshotTest.mm in Sources */, + 5492E0BC2021555100B64F25 /* FSTPathTests.mm in Sources */, + 5492E0B02021552D00B64F25 /* FSTWriteGroupTests.mm in Sources */, + 5492E058202154AB00B64F25 /* FSTAPIHelpers.mm in Sources */, AB380CFB2019388600D97691 /* target_id_generator_test.cc in Sources */, - DE51B1F61F0D491B0013853F /* FSTSerializerBetaTests.m in Sources */, - DE51B1F01F0D49140013853F /* FSTFieldValueTests.m in Sources */, - AB9945281FE2DE0C00DFC1E6 /* FIRSnapshotMetadataTests.m in Sources */, + 5492E0A82021552D00B64F25 /* FSTLevelDBLocalStoreTests.mm in Sources */, 5491BC721FB44593008B3588 /* FSTIntegrationTestCase.mm in Sources */, DE2EF0861F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m in Sources */, - DE51B1DE1F0D490D0013853F /* FSTMemoryLocalStoreTests.m in Sources */, - DE51B1EC1F0D49140013853F /* FSTDatabaseIDTests.m in Sources */, - DE51B1ED1F0D49140013853F /* FSTDocumentKeyTests.m in Sources */, - DE51B1D41F0D48CD0013853F /* FSTViewTests.m in Sources */, + 5492E03120213FFC00B64F25 /* FSTLevelDBSpecTests.mm in Sources */, + 5492E0B12021552D00B64F25 /* FSTRemoteDocumentCacheTests.mm in Sources */, + 5492E0BA2021555100B64F25 /* FSTDocumentSetTests.mm in Sources */, 54740A581FC914F000713A1A /* autoid_test.cc in Sources */, - DE51B1F41F0D491B0013853F /* FSTRemoteEventTests.m in Sources */, 548DB927200D590300E00ABC /* assert_test.cc in Sources */, - 54E928241F33953300C1953E /* FSTEventAccumulator.m in Sources */, + 5492E0A62021552D00B64F25 /* FSTPersistenceTestHelpers.mm in Sources */, + 5492E066202154B900B64F25 /* FSTDatabaseInfoTests.mm in Sources */, + 5492E0A12021552D00B64F25 /* FSTMemoryLocalStoreTests.mm in Sources */, 5436F32420008FAD006E51E3 /* string_printf_test.cc in Sources */, - DE51B1EF1F0D49140013853F /* FSTDocumentTests.m in Sources */, - DE51B1DC1F0D490D0013853F /* FSTLocalSerializerTests.m in Sources */, - AB99452A1FE2F9EB00DFC1E6 /* FIRDocumentSnapshotTests.m in Sources */, - DE51B1E71F0D490D0013853F /* FSTRemoteDocumentChangeBufferTests.m in Sources */, - DE51B1E51F0D490D0013853F /* FSTReferenceSetTests.m in Sources */, - DE51B1EA1F0D490D0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm in Sources */, - DE51B1D21F0D48CD0013853F /* FSTTimestampTests.m in Sources */, - DE51B1EE1F0D49140013853F /* FSTDocumentSetTests.m in Sources */, + 5492E067202154B900B64F25 /* FSTEventManagerTests.mm in Sources */, + 5492E0BF2021555100B64F25 /* FSTFieldValueTests.mm in Sources */, + 5492E055202154AB00B64F25 /* FIRDocumentSnapshotTests.mm in Sources */, + 5492E03E2021401F00B64F25 /* FSTEventAccumulator.mm in Sources */, DE2EF0851F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m in Sources */, - DE51B1F11F0D49140013853F /* FSTMutationTests.m in Sources */, - DE51B1FB1F0D492C0013853F /* FSTMemorySpecTests.m in Sources */, - DE51B1DB1F0D490D0013853F /* FSTLevelDBQueryCacheTests.m in Sources */, + 5492E0AA2021552D00B64F25 /* FSTLevelDBRemoteDocumentCacheTests.mm in Sources */, + 5492E0AC2021552D00B64F25 /* FSTMutationQueueTests.mm in Sources */, + 5492E056202154AB00B64F25 /* FIRFieldPathTests.mm in Sources */, + 5492E03220213FFC00B64F25 /* FSTMockDatastore.mm in Sources */, AB356EF7200EA5EB0089B766 /* field_value_test.cc in Sources */, - 54E9282C1F339CAD00C1953E /* XCTestCase+Await.m in Sources */, - AB99452E1FE30AC800DFC1E6 /* FIRFieldValueTests.m in Sources */, AB7BAB342012B519001E0872 /* geo_point_test.cc in Sources */, - DE51B1DF1F0D490D0013853F /* FSTMemoryMutationQueueTests.m in Sources */, - DE51B1F31F0D491B0013853F /* FSTDatastoreTests.m in Sources */, - DE51B1D01F0D48CD0013853F /* FSTQueryTests.m in Sources */, + 5492E0AD2021552D00B64F25 /* FSTMemoryMutationQueueTests.mm in Sources */, + 5492E051202154AA00B64F25 /* FIRQueryTests.mm in Sources */, + 5492E054202154AB00B64F25 /* FIRFieldValueTests.mm in Sources */, + 5492E09F2021552D00B64F25 /* FSTLevelDBMigrationsTests.mm in Sources */, + 5492E053202154AB00B64F25 /* FIRDocumentReferenceTests.mm in Sources */, + 5492E09D2021552D00B64F25 /* FSTLocalStoreTests.mm in Sources */, + 5492E0A32021552D00B64F25 /* FSTLocalSerializerTests.mm in Sources */, + 5492E0A72021552D00B64F25 /* FSTLevelDBKeyTests.mm in Sources */, + 5492E0A22021552D00B64F25 /* FSTQueryCacheTests.mm in Sources */, + 5492E0A52021552D00B64F25 /* FSTMemoryRemoteDocumentCacheTests.mm in Sources */, + 5492E0BD2021555100B64F25 /* FSTDocumentTests.mm in Sources */, + 5492E0B92021555100B64F25 /* FSTDocumentKeyTests.mm in Sources */, DE2EF0871F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m in Sources */, - DE51B1E01F0D490D0013853F /* FSTMemoryQueryCacheTests.m in Sources */, - DE51B1E91F0D490D0013853F /* FSTLevelDBMutationQueueTests.mm in Sources */, + 5492E0BB2021555100B64F25 /* FSTDatabaseIDTests.mm in Sources */, + 5492E0C82021557E00B64F25 /* FSTDatastoreTests.mm in Sources */, + 5492E065202154B900B64F25 /* FSTViewTests.mm in Sources */, + 5492E03C2021401F00B64F25 /* XCTestCase+Await.mm in Sources */, 54764FAF1FAA21B90085E60A /* FSTGoogleTestTests.mm in Sources */, - DE51B1E61F0D490D0013853F /* FSTRemoteDocumentCacheTests.m in Sources */, - 61E1D8B11FCF6C5700753285 /* StringViewTests.mm in Sources */, - DE51B1D91F0D490D0013853F /* FSTEagerGarbageCollectorTests.m in Sources */, - DE51B1E21F0D490D0013853F /* FSTMutationQueueTests.m in Sources */, - DE51B1E81F0D490D0013853F /* FSTLevelDBKeyTests.mm in Sources */, - AB9945261FE2D71100DFC1E6 /* FIRCollectionReferenceTests.m in Sources */, - DE51B1E31F0D490D0013853F /* FSTPersistenceTestHelpers.m in Sources */, - AB382F7C1FE02A1F007CA955 /* FIRDocumentReferenceTests.m in Sources */, + AB380D04201BC6E400D97691 /* ordered_code_test.cc in Sources */, + 5492E03F2021401F00B64F25 /* FSTHelpers.mm in Sources */, + 5492E068202154B900B64F25 /* FSTQueryTests.mm in Sources */, + 5492E0AB2021552D00B64F25 /* StringViewTests.mm in Sources */, + 5492E0C92021557E00B64F25 /* FSTRemoteEventTests.mm in Sources */, ABF6506C201131F8005F2C74 /* timestamp_test.cc in Sources */, - DE51B1CF1F0D48CD0013853F /* FSTQueryListenerTests.m in Sources */, - DE51B1DA1F0D490D0013853F /* FSTLevelDBLocalStoreTests.m in Sources */, - DE51B1FA1F0D492C0013853F /* FSTLevelDBSpecTests.m in Sources */, - DE51B1FE1F0D492C0013853F /* FSTSyncEngineTestDriver.m in Sources */, - DE51B1FC1F0D492C0013853F /* FSTMockDatastore.m in Sources */, - DE51B1CE1F0D48CD0013853F /* FSTEventManagerTests.m in Sources */, - DE51B1E41F0D490D0013853F /* FSTQueryCacheTests.m in Sources */, - DE51B1CD1F0D48CD0013853F /* FSTDatabaseInfoTests.m in Sources */, - AB382F7E1FE03059007CA955 /* FIRFieldPathTests.m in Sources */, + 5492E0AE2021552D00B64F25 /* FSTLevelDBQueryCacheTests.mm in Sources */, + 5492E059202154AB00B64F25 /* FIRQuerySnapshotTests.mm in Sources */, + 5492E050202154AA00B64F25 /* FIRCollectionReferenceTests.mm in Sources */, + 5492E0A02021552D00B64F25 /* FSTLevelDBMutationQueueTests.mm in Sources */, + 5492E03420213FFC00B64F25 /* FSTMemorySpecTests.mm in Sources */, + AB380D02201BC69F00D97691 /* bits_test.cc in Sources */, 548DB929200D59F600E00ABC /* comparison_test.cc in Sources */, - DE51B1F21F0D49140013853F /* FSTPathTests.m in Sources */, - AB99452C1FE3018D00DFC1E6 /* FIRQuerySnapshotTests.m in Sources */, + 5492E03D2021401F00B64F25 /* FSTAssertTests.mm in Sources */, + 5492E062202154B900B64F25 /* FSTTimestampTests.mm in Sources */, + 5492E052202154AB00B64F25 /* FIRGeoPointTests.mm in Sources */, + 5492E0C72021557E00B64F25 /* FSTSerializerBetaTests.mm in Sources */, + 5492E03520213FFC00B64F25 /* FSTSpecTests.mm in Sources */, + 5492E03B2021401F00B64F25 /* FSTTestDispatchQueue.mm in Sources */, + 5492E057202154AB00B64F25 /* FIRSnapshotMetadataTests.mm in Sources */, 54740A571FC914BA00713A1A /* secure_random_test.cc in Sources */, - DE51B1DD1F0D490D0013853F /* FSTLocalStoreTests.m in Sources */, - D5B25474286C9800CE42B8C2 /* FSTTestDispatchQueue.m in Sources */, - 132E32A6C1989C284BFE10B2 /* FSTLevelDBMigrationsTests.mm in Sources */, + 5492E0BE2021555100B64F25 /* FSTMutationTests.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1309,24 +1305,24 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DE03B2EE1F214BAA00A30B9C /* FIRWriteBatchTests.m in Sources */, - DE03B2F01F214BAA00A30B9C /* FIRDatabaseTests.m in Sources */, + 5492E076202154D600B64F25 /* FIRValidationTests.mm in Sources */, + 5492E072202154D600B64F25 /* FIRQueryTests.mm in Sources */, 5491BC731FB44593008B3588 /* FSTIntegrationTestCase.mm in Sources */, - DE03B2F41F214BAA00A30B9C /* FIRServerTimestampTests.m in Sources */, - DE03B2F11F214BAA00A30B9C /* FIRFieldsTests.m in Sources */, - 54E9282D1F339CAD00C1953E /* XCTestCase+Await.m in Sources */, - DE03B2EC1F214BA200A30B9C /* FSTDatastoreTests.m in Sources */, - 54E928251F33953400C1953E /* FSTEventAccumulator.m in Sources */, - DE03B2ED1F214BA200A30B9C /* FSTSmokeTests.m in Sources */, - DE03B2F31F214BAA00A30B9C /* FIRQueryTests.m in Sources */, - DE03B35E1F21586C00A30B9C /* FSTHelpers.m in Sources */, - DE03B2F51F214BAA00A30B9C /* FIRTypeTests.m in Sources */, - DE03B2EF1F214BAA00A30B9C /* FIRCursorTests.m in Sources */, - DE03B2F21F214BAA00A30B9C /* FIRListenerRegistrationTests.m in Sources */, - DE03B2C91F2149D600A30B9C /* FSTTransactionTests.m in Sources */, - 54DA12B11F315F3800DD57A1 /* FIRValidationTests.m in Sources */, - D5B2532E4676014F57A7EAB9 /* FSTStreamTests.m in Sources */, - D5B259FDEE8094E8D710C5BF /* FSTTestDispatchQueue.m in Sources */, + 5492E0442021457E00B64F25 /* XCTestCase+Await.mm in Sources */, + 5492E07A202154D600B64F25 /* FIRTypeTests.mm in Sources */, + 5492E0422021440500B64F25 /* FSTHelpers.mm in Sources */, + 5492E041202143E700B64F25 /* FSTEventAccumulator.mm in Sources */, + 5492E080202154EC00B64F25 /* FSTSmokeTests.mm in Sources */, + 5492E077202154D600B64F25 /* FIRServerTimestampTests.mm in Sources */, + 5492E081202154EC00B64F25 /* FSTStreamTests.mm in Sources */, + 5492E074202154D600B64F25 /* FIRListenerRegistrationTests.mm in Sources */, + 5492E082202154EC00B64F25 /* FSTDatastoreTests.mm in Sources */, + 5492E079202154D600B64F25 /* FIRCursorTests.mm in Sources */, + 5492E073202154D600B64F25 /* FIRFieldsTests.mm in Sources */, + 5492E07F202154EC00B64F25 /* FSTTransactionTests.mm in Sources */, + 5492E075202154D600B64F25 /* FIRDatabaseTests.mm in Sources */, + 5492E078202154D600B64F25 /* FIRWriteBatchTests.mm in Sources */, + 5492E0432021441E00B64F25 /* FSTTestDispatchQueue.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Firestore/Example/Tests/API/FIRCollectionReferenceTests.m b/Firestore/Example/Tests/API/FIRCollectionReferenceTests.m deleted file mode 100644 index 547078f..0000000 --- a/Firestore/Example/Tests/API/FIRCollectionReferenceTests.m +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#import - -#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRCollectionReferenceTests : XCTestCase -@end - -@implementation FIRCollectionReferenceTests - -- (void)testEquals { - FIRCollectionReference *referenceFoo = FSTTestCollectionRef(@"foo"); - FIRCollectionReference *referenceFooDup = FSTTestCollectionRef(@"foo"); - FIRCollectionReference *referenceBar = FSTTestCollectionRef(@"bar"); - XCTAssertEqualObjects(referenceFoo, referenceFooDup); - XCTAssertNotEqualObjects(referenceFoo, referenceBar); - - XCTAssertEqual([referenceFoo hash], [referenceFooDup hash]); - XCTAssertNotEqual([referenceFoo hash], [referenceBar hash]); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRCollectionReferenceTests.mm b/Firestore/Example/Tests/API/FIRCollectionReferenceTests.mm new file mode 100644 index 0000000..547078f --- /dev/null +++ b/Firestore/Example/Tests/API/FIRCollectionReferenceTests.mm @@ -0,0 +1,43 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import + +#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRCollectionReferenceTests : XCTestCase +@end + +@implementation FIRCollectionReferenceTests + +- (void)testEquals { + FIRCollectionReference *referenceFoo = FSTTestCollectionRef(@"foo"); + FIRCollectionReference *referenceFooDup = FSTTestCollectionRef(@"foo"); + FIRCollectionReference *referenceBar = FSTTestCollectionRef(@"bar"); + XCTAssertEqualObjects(referenceFoo, referenceFooDup); + XCTAssertNotEqualObjects(referenceFoo, referenceBar); + + XCTAssertEqual([referenceFoo hash], [referenceFooDup hash]); + XCTAssertNotEqual([referenceFoo hash], [referenceBar hash]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRDocumentReferenceTests.m b/Firestore/Example/Tests/API/FIRDocumentReferenceTests.m deleted file mode 100644 index cc2b431..0000000 --- a/Firestore/Example/Tests/API/FIRDocumentReferenceTests.m +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#import - -#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRDocumentReferenceTests : XCTestCase -@end - -@implementation FIRDocumentReferenceTests - -- (void)testEquals { - FIRDocumentReference *referenceFoo = FSTTestDocRef(@"rooms/foo"); - FIRDocumentReference *referenceFooDup = FSTTestDocRef(@"rooms/foo"); - FIRDocumentReference *referenceBar = FSTTestDocRef(@"rooms/bar"); - XCTAssertEqualObjects(referenceFoo, referenceFooDup); - XCTAssertNotEqualObjects(referenceFoo, referenceBar); - - XCTAssertEqual([referenceFoo hash], [referenceFooDup hash]); - XCTAssertNotEqual([referenceFoo hash], [referenceBar hash]); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRDocumentReferenceTests.mm b/Firestore/Example/Tests/API/FIRDocumentReferenceTests.mm new file mode 100644 index 0000000..cc2b431 --- /dev/null +++ b/Firestore/Example/Tests/API/FIRDocumentReferenceTests.mm @@ -0,0 +1,43 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import + +#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRDocumentReferenceTests : XCTestCase +@end + +@implementation FIRDocumentReferenceTests + +- (void)testEquals { + FIRDocumentReference *referenceFoo = FSTTestDocRef(@"rooms/foo"); + FIRDocumentReference *referenceFooDup = FSTTestDocRef(@"rooms/foo"); + FIRDocumentReference *referenceBar = FSTTestDocRef(@"rooms/bar"); + XCTAssertEqualObjects(referenceFoo, referenceFooDup); + XCTAssertNotEqualObjects(referenceFoo, referenceBar); + + XCTAssertEqual([referenceFoo hash], [referenceFooDup hash]); + XCTAssertNotEqual([referenceFoo hash], [referenceBar hash]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRDocumentSnapshotTests.m b/Firestore/Example/Tests/API/FIRDocumentSnapshotTests.m deleted file mode 100644 index 677d385..0000000 --- a/Firestore/Example/Tests/API/FIRDocumentSnapshotTests.m +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#import - -#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRDocumentSnapshotTests : XCTestCase -@end - -@implementation FIRDocumentSnapshotTests - -- (void)testEquals { - FIRDocumentSnapshot *base = FSTTestDocSnapshot(@"rooms/foo", 1, @{ @"a" : @1 }, NO, NO); - FIRDocumentSnapshot *baseDup = FSTTestDocSnapshot(@"rooms/foo", 1, @{ @"a" : @1 }, NO, NO); - FIRDocumentSnapshot *nilData = FSTTestDocSnapshot(@"rooms/foo", 1, nil, NO, NO); - FIRDocumentSnapshot *nilDataDup = FSTTestDocSnapshot(@"rooms/foo", 1, nil, NO, NO); - FIRDocumentSnapshot *differentPath = FSTTestDocSnapshot(@"rooms/bar", 1, @{ @"a" : @1 }, NO, NO); - FIRDocumentSnapshot *differentData = FSTTestDocSnapshot(@"rooms/bar", 1, @{ @"b" : @1 }, NO, NO); - FIRDocumentSnapshot *hasMutations = FSTTestDocSnapshot(@"rooms/bar", 1, @{ @"a" : @1 }, YES, NO); - FIRDocumentSnapshot *fromCache = FSTTestDocSnapshot(@"rooms/bar", 1, @{ @"a" : @1 }, NO, YES); - XCTAssertEqualObjects(base, baseDup); - XCTAssertEqualObjects(nilData, nilDataDup); - XCTAssertNotEqualObjects(base, nilData); - XCTAssertNotEqualObjects(nilData, base); - XCTAssertNotEqualObjects(base, differentPath); - XCTAssertNotEqualObjects(base, differentData); - XCTAssertNotEqualObjects(base, hasMutations); - XCTAssertNotEqualObjects(base, fromCache); - - XCTAssertEqual([base hash], [baseDup hash]); - XCTAssertEqual([nilData hash], [nilDataDup hash]); - XCTAssertNotEqual([base hash], [nilData hash]); - XCTAssertNotEqual([base hash], [differentPath hash]); - XCTAssertNotEqual([base hash], [differentData hash]); - XCTAssertNotEqual([base hash], [hasMutations hash]); - XCTAssertNotEqual([base hash], [fromCache hash]); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRDocumentSnapshotTests.mm b/Firestore/Example/Tests/API/FIRDocumentSnapshotTests.mm new file mode 100644 index 0000000..677d385 --- /dev/null +++ b/Firestore/Example/Tests/API/FIRDocumentSnapshotTests.mm @@ -0,0 +1,59 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import + +#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRDocumentSnapshotTests : XCTestCase +@end + +@implementation FIRDocumentSnapshotTests + +- (void)testEquals { + FIRDocumentSnapshot *base = FSTTestDocSnapshot(@"rooms/foo", 1, @{ @"a" : @1 }, NO, NO); + FIRDocumentSnapshot *baseDup = FSTTestDocSnapshot(@"rooms/foo", 1, @{ @"a" : @1 }, NO, NO); + FIRDocumentSnapshot *nilData = FSTTestDocSnapshot(@"rooms/foo", 1, nil, NO, NO); + FIRDocumentSnapshot *nilDataDup = FSTTestDocSnapshot(@"rooms/foo", 1, nil, NO, NO); + FIRDocumentSnapshot *differentPath = FSTTestDocSnapshot(@"rooms/bar", 1, @{ @"a" : @1 }, NO, NO); + FIRDocumentSnapshot *differentData = FSTTestDocSnapshot(@"rooms/bar", 1, @{ @"b" : @1 }, NO, NO); + FIRDocumentSnapshot *hasMutations = FSTTestDocSnapshot(@"rooms/bar", 1, @{ @"a" : @1 }, YES, NO); + FIRDocumentSnapshot *fromCache = FSTTestDocSnapshot(@"rooms/bar", 1, @{ @"a" : @1 }, NO, YES); + XCTAssertEqualObjects(base, baseDup); + XCTAssertEqualObjects(nilData, nilDataDup); + XCTAssertNotEqualObjects(base, nilData); + XCTAssertNotEqualObjects(nilData, base); + XCTAssertNotEqualObjects(base, differentPath); + XCTAssertNotEqualObjects(base, differentData); + XCTAssertNotEqualObjects(base, hasMutations); + XCTAssertNotEqualObjects(base, fromCache); + + XCTAssertEqual([base hash], [baseDup hash]); + XCTAssertEqual([nilData hash], [nilDataDup hash]); + XCTAssertNotEqual([base hash], [nilData hash]); + XCTAssertNotEqual([base hash], [differentPath hash]); + XCTAssertNotEqual([base hash], [differentData hash]); + XCTAssertNotEqual([base hash], [hasMutations hash]); + XCTAssertNotEqual([base hash], [fromCache hash]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRFieldPathTests.m b/Firestore/Example/Tests/API/FIRFieldPathTests.m deleted file mode 100644 index 679ea89..0000000 --- a/Firestore/Example/Tests/API/FIRFieldPathTests.m +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#import - -#import "Firestore/Source/API/FIRFieldPath+Internal.h" -#import "Firestore/Source/Model/FSTPath.h" - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRFieldPathTests : XCTestCase -@end - -@implementation FIRFieldPathTests - -- (void)testEquals { - FIRFieldPath *foo = [[FIRFieldPath alloc] initPrivate:FSTTestFieldPath(@"foo.ooo.oooo")]; - FIRFieldPath *fooDup = [[FIRFieldPath alloc] initPrivate:FSTTestFieldPath(@"foo.ooo.oooo")]; - FIRFieldPath *bar = [[FIRFieldPath alloc] initPrivate:FSTTestFieldPath(@"baa.aaa.aaar")]; - XCTAssertEqualObjects(foo, fooDup); - XCTAssertNotEqualObjects(foo, bar); - - XCTAssertEqual([foo hash], [fooDup hash]); - XCTAssertNotEqual([foo hash], [bar hash]); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRFieldPathTests.mm b/Firestore/Example/Tests/API/FIRFieldPathTests.mm new file mode 100644 index 0000000..679ea89 --- /dev/null +++ b/Firestore/Example/Tests/API/FIRFieldPathTests.mm @@ -0,0 +1,46 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import + +#import "Firestore/Source/API/FIRFieldPath+Internal.h" +#import "Firestore/Source/Model/FSTPath.h" + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRFieldPathTests : XCTestCase +@end + +@implementation FIRFieldPathTests + +- (void)testEquals { + FIRFieldPath *foo = [[FIRFieldPath alloc] initPrivate:FSTTestFieldPath(@"foo.ooo.oooo")]; + FIRFieldPath *fooDup = [[FIRFieldPath alloc] initPrivate:FSTTestFieldPath(@"foo.ooo.oooo")]; + FIRFieldPath *bar = [[FIRFieldPath alloc] initPrivate:FSTTestFieldPath(@"baa.aaa.aaar")]; + XCTAssertEqualObjects(foo, fooDup); + XCTAssertNotEqualObjects(foo, bar); + + XCTAssertEqual([foo hash], [fooDup hash]); + XCTAssertNotEqual([foo hash], [bar hash]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRFieldValueTests.m b/Firestore/Example/Tests/API/FIRFieldValueTests.m deleted file mode 100644 index becf7d6..0000000 --- a/Firestore/Example/Tests/API/FIRFieldValueTests.m +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRFieldValueTests : XCTestCase -@end - -@implementation FIRFieldValueTests - -- (void)testEquals { - FIRFieldValue *delete = [FIRFieldValue fieldValueForDelete]; - FIRFieldValue *deleteDup = [FIRFieldValue fieldValueForDelete]; - FIRFieldValue *serverTimestamp = [FIRFieldValue fieldValueForServerTimestamp]; - FIRFieldValue *serverTimestampDup = [FIRFieldValue fieldValueForServerTimestamp]; - XCTAssertEqualObjects(delete, deleteDup); - XCTAssertNotEqualObjects(delete, nil); - XCTAssertEqualObjects(serverTimestamp, serverTimestampDup); - XCTAssertNotEqualObjects(serverTimestamp, nil); - XCTAssertNotEqualObjects(delete, serverTimestamp); - - XCTAssertEqual([delete hash], [deleteDup hash]); - XCTAssertEqual([serverTimestamp hash], [serverTimestamp hash]); - XCTAssertNotEqual([delete hash], [serverTimestamp hash]); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRFieldValueTests.mm b/Firestore/Example/Tests/API/FIRFieldValueTests.mm new file mode 100644 index 0000000..575dfee --- /dev/null +++ b/Firestore/Example/Tests/API/FIRFieldValueTests.mm @@ -0,0 +1,46 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRFieldValueTests : XCTestCase +@end + +@implementation FIRFieldValueTests + +- (void)testEquals { + FIRFieldValue *deleted = [FIRFieldValue fieldValueForDelete]; + FIRFieldValue *deleteDup = [FIRFieldValue fieldValueForDelete]; + FIRFieldValue *serverTimestamp = [FIRFieldValue fieldValueForServerTimestamp]; + FIRFieldValue *serverTimestampDup = [FIRFieldValue fieldValueForServerTimestamp]; + XCTAssertEqualObjects(deleted, deleteDup); + XCTAssertNotEqualObjects(deleted, nil); + XCTAssertEqualObjects(serverTimestamp, serverTimestampDup); + XCTAssertNotEqualObjects(serverTimestamp, nil); + XCTAssertNotEqualObjects(deleted, serverTimestamp); + + XCTAssertEqual([deleted hash], [deleteDup hash]); + XCTAssertEqual([serverTimestamp hash], [serverTimestamp hash]); + XCTAssertNotEqual([deleted hash], [serverTimestamp hash]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRGeoPointTests.m b/Firestore/Example/Tests/API/FIRGeoPointTests.m deleted file mode 100644 index 4de80a8..0000000 --- a/Firestore/Example/Tests/API/FIRGeoPointTests.m +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#import - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRGeoPointTests : XCTestCase -@end - -@implementation FIRGeoPointTests - -- (void)testEquals { - FIRGeoPoint *foo = FSTTestGeoPoint(1.23, 4.56); - FIRGeoPoint *fooDup = FSTTestGeoPoint(1.23, 4.56); - FIRGeoPoint *differentLatitude = FSTTestGeoPoint(1.23, 0); - FIRGeoPoint *differentLongitude = FSTTestGeoPoint(0, 4.56); - XCTAssertEqualObjects(foo, fooDup); - XCTAssertNotEqualObjects(foo, differentLatitude); - XCTAssertNotEqualObjects(foo, differentLongitude); - - XCTAssertEqual([foo hash], [fooDup hash]); - XCTAssertNotEqual([foo hash], [differentLatitude hash]); - XCTAssertNotEqual([foo hash], [differentLongitude hash]); -} - -- (void)testComparison { - NSArray *values = @[ - @[ [[FIRGeoPoint alloc] initWithLatitude:-90 longitude:-180] ], - @[ [[FIRGeoPoint alloc] initWithLatitude:-90 longitude:0] ], - @[ [[FIRGeoPoint alloc] initWithLatitude:-90 longitude:180] ], - @[ [[FIRGeoPoint alloc] initWithLatitude:-89 longitude:-180] ], - @[ [[FIRGeoPoint alloc] initWithLatitude:-89 longitude:0] ], - @[ [[FIRGeoPoint alloc] initWithLatitude:-89 longitude:180] ], - @[ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:-180] ], - @[ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:0] ], - @[ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:180] ], - @[ [[FIRGeoPoint alloc] initWithLatitude:89 longitude:-180] ], - @[ [[FIRGeoPoint alloc] initWithLatitude:89 longitude:0] ], - @[ [[FIRGeoPoint alloc] initWithLatitude:89 longitude:180] ], - @[ [[FIRGeoPoint alloc] initWithLatitude:90 longitude:-180] ], - @[ [[FIRGeoPoint alloc] initWithLatitude:90 longitude:0] ], - @[ [[FIRGeoPoint alloc] initWithLatitude:90 longitude:180] ], - ]; - - FSTAssertComparisons(values); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRGeoPointTests.mm b/Firestore/Example/Tests/API/FIRGeoPointTests.mm new file mode 100644 index 0000000..4de80a8 --- /dev/null +++ b/Firestore/Example/Tests/API/FIRGeoPointTests.mm @@ -0,0 +1,68 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRGeoPointTests : XCTestCase +@end + +@implementation FIRGeoPointTests + +- (void)testEquals { + FIRGeoPoint *foo = FSTTestGeoPoint(1.23, 4.56); + FIRGeoPoint *fooDup = FSTTestGeoPoint(1.23, 4.56); + FIRGeoPoint *differentLatitude = FSTTestGeoPoint(1.23, 0); + FIRGeoPoint *differentLongitude = FSTTestGeoPoint(0, 4.56); + XCTAssertEqualObjects(foo, fooDup); + XCTAssertNotEqualObjects(foo, differentLatitude); + XCTAssertNotEqualObjects(foo, differentLongitude); + + XCTAssertEqual([foo hash], [fooDup hash]); + XCTAssertNotEqual([foo hash], [differentLatitude hash]); + XCTAssertNotEqual([foo hash], [differentLongitude hash]); +} + +- (void)testComparison { + NSArray *values = @[ + @[ [[FIRGeoPoint alloc] initWithLatitude:-90 longitude:-180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:-90 longitude:0] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:-90 longitude:180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:-89 longitude:-180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:-89 longitude:0] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:-89 longitude:180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:-180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:0] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:89 longitude:-180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:89 longitude:0] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:89 longitude:180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:90 longitude:-180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:90 longitude:0] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:90 longitude:180] ], + ]; + + FSTAssertComparisons(values); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRQuerySnapshotTests.m b/Firestore/Example/Tests/API/FIRQuerySnapshotTests.m deleted file mode 100644 index 067425a..0000000 --- a/Firestore/Example/Tests/API/FIRQuerySnapshotTests.m +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#import - -#import "Firestore/Source/Model/FSTPath.h" - -#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRQuerySnapshotTests : XCTestCase -@end - -@implementation FIRQuerySnapshotTests - -- (void)testEquals { - FIRQuerySnapshot *foo = FSTTestQuerySnapshot(@"foo", @{}, @{ @"a" : @{@"a" : @1} }, YES, NO); - FIRQuerySnapshot *fooDup = FSTTestQuerySnapshot(@"foo", @{}, @{ @"a" : @{@"a" : @1} }, YES, NO); - FIRQuerySnapshot *differentPath = FSTTestQuerySnapshot(@"bar", @{}, - @{ @"a" : @{@"a" : @1} }, YES, NO); - FIRQuerySnapshot *differentDoc = FSTTestQuerySnapshot(@"foo", - @{ @"a" : @{@"b" : @1} }, @{}, YES, NO); - FIRQuerySnapshot *noPendingWrites = FSTTestQuerySnapshot(@"foo", @{}, - @{ @"a" : @{@"a" : @1} }, NO, NO); - FIRQuerySnapshot *fromCache = FSTTestQuerySnapshot(@"foo", @{}, - @{ @"a" : @{@"a" : @1} }, YES, YES); - XCTAssertEqualObjects(foo, fooDup); - XCTAssertNotEqualObjects(foo, differentPath); - XCTAssertNotEqualObjects(foo, differentDoc); - XCTAssertNotEqualObjects(foo, noPendingWrites); - XCTAssertNotEqualObjects(foo, fromCache); - - XCTAssertEqual([foo hash], [fooDup hash]); - XCTAssertNotEqual([foo hash], [differentPath hash]); - XCTAssertNotEqual([foo hash], [differentDoc hash]); - XCTAssertNotEqual([foo hash], [noPendingWrites hash]); - XCTAssertNotEqual([foo hash], [fromCache hash]); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRQuerySnapshotTests.mm b/Firestore/Example/Tests/API/FIRQuerySnapshotTests.mm new file mode 100644 index 0000000..067425a --- /dev/null +++ b/Firestore/Example/Tests/API/FIRQuerySnapshotTests.mm @@ -0,0 +1,58 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import + +#import "Firestore/Source/Model/FSTPath.h" + +#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRQuerySnapshotTests : XCTestCase +@end + +@implementation FIRQuerySnapshotTests + +- (void)testEquals { + FIRQuerySnapshot *foo = FSTTestQuerySnapshot(@"foo", @{}, @{ @"a" : @{@"a" : @1} }, YES, NO); + FIRQuerySnapshot *fooDup = FSTTestQuerySnapshot(@"foo", @{}, @{ @"a" : @{@"a" : @1} }, YES, NO); + FIRQuerySnapshot *differentPath = FSTTestQuerySnapshot(@"bar", @{}, + @{ @"a" : @{@"a" : @1} }, YES, NO); + FIRQuerySnapshot *differentDoc = FSTTestQuerySnapshot(@"foo", + @{ @"a" : @{@"b" : @1} }, @{}, YES, NO); + FIRQuerySnapshot *noPendingWrites = FSTTestQuerySnapshot(@"foo", @{}, + @{ @"a" : @{@"a" : @1} }, NO, NO); + FIRQuerySnapshot *fromCache = FSTTestQuerySnapshot(@"foo", @{}, + @{ @"a" : @{@"a" : @1} }, YES, YES); + XCTAssertEqualObjects(foo, fooDup); + XCTAssertNotEqualObjects(foo, differentPath); + XCTAssertNotEqualObjects(foo, differentDoc); + XCTAssertNotEqualObjects(foo, noPendingWrites); + XCTAssertNotEqualObjects(foo, fromCache); + + XCTAssertEqual([foo hash], [fooDup hash]); + XCTAssertNotEqual([foo hash], [differentPath hash]); + XCTAssertNotEqual([foo hash], [differentDoc hash]); + XCTAssertNotEqual([foo hash], [noPendingWrites hash]); + XCTAssertNotEqual([foo hash], [fromCache hash]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRQueryTests.m b/Firestore/Example/Tests/API/FIRQueryTests.m deleted file mode 100644 index 83f90be..0000000 --- a/Firestore/Example/Tests/API/FIRQueryTests.m +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#import - -#import "Firestore/Source/API/FIRQuery+Internal.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Model/FSTPath.h" - -#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRQueryTests : XCTestCase -@end - -@implementation FIRQueryTests - -- (void)testEquals { - FIRFirestore *firestore = FSTTestFirestore(); - FIRQuery *queryFoo = [FIRQuery referenceWithQuery:FSTTestQuery(@"foo") firestore:firestore]; - FIRQuery *queryFooDup = [FIRQuery referenceWithQuery:FSTTestQuery(@"foo") firestore:firestore]; - FIRQuery *queryBar = [FIRQuery referenceWithQuery:FSTTestQuery(@"bar") firestore:firestore]; - XCTAssertEqualObjects(queryFoo, queryFooDup); - XCTAssertNotEqualObjects(queryFoo, queryBar); - XCTAssertEqualObjects([queryFoo queryWhereField:@"f" isEqualTo:@1], - [queryFoo queryWhereField:@"f" isEqualTo:@1]); - XCTAssertNotEqualObjects([queryFoo queryWhereField:@"f" isEqualTo:@1], - [queryFoo queryWhereField:@"f" isEqualTo:@2]); - - XCTAssertEqual([queryFoo hash], [queryFooDup hash]); - XCTAssertNotEqual([queryFoo hash], [queryBar hash]); - XCTAssertEqual([[queryFoo queryWhereField:@"f" isEqualTo:@1] hash], - [[queryFoo queryWhereField:@"f" isEqualTo:@1] hash]); - XCTAssertNotEqual([[queryFoo queryWhereField:@"f" isEqualTo:@1] hash], - [[queryFoo queryWhereField:@"f" isEqualTo:@2] hash]); -} - -- (void)testFilteringWithPredicate { - FIRFirestore *firestore = FSTTestFirestore(); - FIRQuery *query = [FIRQuery referenceWithQuery:FSTTestQuery(@"foo") firestore:firestore]; - FIRQuery *query1 = [query queryWhereField:@"f" isLessThanOrEqualTo:@1]; - FIRQuery *query2 = [query queryFilteredUsingPredicate:[NSPredicate predicateWithFormat:@"f<=1"]]; - FIRQuery *query3 = - [[query queryWhereField:@"f1" isLessThan:@2] queryWhereField:@"f2" isEqualTo:@3]; - FIRQuery *query4 = - [query queryFilteredUsingPredicate:[NSPredicate predicateWithFormat:@"f1<2 && f2==3"]]; - FIRQuery *query5 = - [[[[[query queryWhereField:@"f1" isLessThan:@2] queryWhereField:@"f2" isEqualTo:@3] - queryWhereField:@"f1" - isLessThanOrEqualTo:@"four"] queryWhereField:@"f1" - isGreaterThanOrEqualTo:@"five"] queryWhereField:@"f1" - isGreaterThan:@6]; - FIRQuery *query6 = [query - queryFilteredUsingPredicate: - [NSPredicate predicateWithFormat:@"f1<2 && f2==3 && f1<='four' && f1>='five' && f1>6"]]; - FIRQuery *query7 = [query - queryFilteredUsingPredicate: - [NSPredicate predicateWithFormat:@"2>f1 && 3==f2 && 'four'>=f1 && 'five'<=f1 && 6 + +#import + +#import "Firestore/Source/API/FIRQuery+Internal.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Model/FSTPath.h" + +#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRQueryTests : XCTestCase +@end + +@implementation FIRQueryTests + +- (void)testEquals { + FIRFirestore *firestore = FSTTestFirestore(); + FIRQuery *queryFoo = [FIRQuery referenceWithQuery:FSTTestQuery(@"foo") firestore:firestore]; + FIRQuery *queryFooDup = [FIRQuery referenceWithQuery:FSTTestQuery(@"foo") firestore:firestore]; + FIRQuery *queryBar = [FIRQuery referenceWithQuery:FSTTestQuery(@"bar") firestore:firestore]; + XCTAssertEqualObjects(queryFoo, queryFooDup); + XCTAssertNotEqualObjects(queryFoo, queryBar); + XCTAssertEqualObjects([queryFoo queryWhereField:@"f" isEqualTo:@1], + [queryFoo queryWhereField:@"f" isEqualTo:@1]); + XCTAssertNotEqualObjects([queryFoo queryWhereField:@"f" isEqualTo:@1], + [queryFoo queryWhereField:@"f" isEqualTo:@2]); + + XCTAssertEqual([queryFoo hash], [queryFooDup hash]); + XCTAssertNotEqual([queryFoo hash], [queryBar hash]); + XCTAssertEqual([[queryFoo queryWhereField:@"f" isEqualTo:@1] hash], + [[queryFoo queryWhereField:@"f" isEqualTo:@1] hash]); + XCTAssertNotEqual([[queryFoo queryWhereField:@"f" isEqualTo:@1] hash], + [[queryFoo queryWhereField:@"f" isEqualTo:@2] hash]); +} + +- (void)testFilteringWithPredicate { + FIRFirestore *firestore = FSTTestFirestore(); + FIRQuery *query = [FIRQuery referenceWithQuery:FSTTestQuery(@"foo") firestore:firestore]; + FIRQuery *query1 = [query queryWhereField:@"f" isLessThanOrEqualTo:@1]; + FIRQuery *query2 = [query queryFilteredUsingPredicate:[NSPredicate predicateWithFormat:@"f<=1"]]; + FIRQuery *query3 = + [[query queryWhereField:@"f1" isLessThan:@2] queryWhereField:@"f2" isEqualTo:@3]; + FIRQuery *query4 = + [query queryFilteredUsingPredicate:[NSPredicate predicateWithFormat:@"f1<2 && f2==3"]]; + FIRQuery *query5 = + [[[[[query queryWhereField:@"f1" isLessThan:@2] queryWhereField:@"f2" isEqualTo:@3] + queryWhereField:@"f1" + isLessThanOrEqualTo:@"four"] queryWhereField:@"f1" + isGreaterThanOrEqualTo:@"five"] queryWhereField:@"f1" + isGreaterThan:@6]; + FIRQuery *query6 = [query + queryFilteredUsingPredicate: + [NSPredicate predicateWithFormat:@"f1<2 && f2==3 && f1<='four' && f1>='five' && f1>6"]]; + FIRQuery *query7 = [query + queryFilteredUsingPredicate: + [NSPredicate predicateWithFormat:@"2>f1 && 3==f2 && 'four'>=f1 && 'five'<=f1 && 6 - -#import - -#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRSnapshotMetadataTests : XCTestCase -@end - -@implementation FIRSnapshotMetadataTests - -- (void)testEquals { - FIRSnapshotMetadata *foo = - [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:YES fromCache:YES]; - FIRSnapshotMetadata *fooDup = - [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:YES fromCache:YES]; - FIRSnapshotMetadata *bar = - [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:YES fromCache:NO]; - FIRSnapshotMetadata *baz = - [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:NO fromCache:YES]; - XCTAssertEqualObjects(foo, fooDup); - XCTAssertNotEqualObjects(foo, bar); - XCTAssertNotEqualObjects(foo, baz); - XCTAssertNotEqualObjects(bar, baz); - - XCTAssertEqual([foo hash], [fooDup hash]); - XCTAssertNotEqual([foo hash], [bar hash]); - XCTAssertNotEqual([foo hash], [baz hash]); - XCTAssertNotEqual([bar hash], [baz hash]); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRSnapshotMetadataTests.mm b/Firestore/Example/Tests/API/FIRSnapshotMetadataTests.mm new file mode 100644 index 0000000..a4d321b --- /dev/null +++ b/Firestore/Example/Tests/API/FIRSnapshotMetadataTests.mm @@ -0,0 +1,52 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import + +#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRSnapshotMetadataTests : XCTestCase +@end + +@implementation FIRSnapshotMetadataTests + +- (void)testEquals { + FIRSnapshotMetadata *foo = + [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:YES fromCache:YES]; + FIRSnapshotMetadata *fooDup = + [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:YES fromCache:YES]; + FIRSnapshotMetadata *bar = + [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:YES fromCache:NO]; + FIRSnapshotMetadata *baz = + [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:NO fromCache:YES]; + XCTAssertEqualObjects(foo, fooDup); + XCTAssertNotEqualObjects(foo, bar); + XCTAssertNotEqualObjects(foo, baz); + XCTAssertNotEqualObjects(bar, baz); + + XCTAssertEqual([foo hash], [fooDup hash]); + XCTAssertNotEqual([foo hash], [bar hash]); + XCTAssertNotEqual([foo hash], [baz hash]); + XCTAssertNotEqual([bar hash], [baz hash]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FSTAPIHelpers.m b/Firestore/Example/Tests/API/FSTAPIHelpers.m deleted file mode 100644 index da899b7..0000000 --- a/Firestore/Example/Tests/API/FSTAPIHelpers.m +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Example/Tests/API/FSTAPIHelpers.h" - -#import -#import -#import - -#import "Firestore/Source/API/FIRCollectionReference+Internal.h" -#import "Firestore/Source/API/FIRDocumentReference+Internal.h" -#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRQuerySnapshot+Internal.h" -#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" -#import "Firestore/Source/Model/FSTPath.h" - -NS_ASSUME_NONNULL_BEGIN - -FIRFirestore *FSTTestFirestore() { - static FIRFirestore *sharedInstance = nil; - static dispatch_once_t onceToken; - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wnonnull" - dispatch_once(&onceToken, ^{ - sharedInstance = [[FIRFirestore alloc] initWithProjectID:@"abc" - database:@"abc" - persistenceKey:@"db123" - credentialsProvider:nil - workerDispatchQueue:nil - firebaseApp:nil]; - }); -#pragma clang diagnostic pop - return sharedInstance; -} - -FIRDocumentSnapshot *FSTTestDocSnapshot(NSString *path, - FSTTestSnapshotVersion version, - NSDictionary *_Nullable data, - BOOL hasMutations, - BOOL fromCache) { - FSTDocument *doc = data ? FSTTestDoc(path, version, data, hasMutations) : nil; - return [FIRDocumentSnapshot snapshotWithFirestore:FSTTestFirestore() - documentKey:FSTTestDocKey(path) - document:doc - fromCache:fromCache]; -} - -FIRCollectionReference *FSTTestCollectionRef(NSString *path) { - return [FIRCollectionReference referenceWithPath:FSTTestPath(path) firestore:FSTTestFirestore()]; -} - -FIRDocumentReference *FSTTestDocRef(NSString *path) { - return [FIRDocumentReference referenceWithPath:FSTTestPath(path) firestore:FSTTestFirestore()]; -} - -/** A convenience method for creating a query snapshots for tests. */ -FIRQuerySnapshot *FSTTestQuerySnapshot( - NSString *path, - NSDictionary *> *oldDocs, - NSDictionary *> *docsToAdd, - BOOL hasPendingWrites, - BOOL fromCache) { - FIRSnapshotMetadata *metadata = - [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:hasPendingWrites fromCache:fromCache]; - FSTDocumentSet *oldDocuments = FSTTestDocSet(FSTDocumentComparatorByKey, @[]); - for (NSString *key in oldDocs) { - oldDocuments = [oldDocuments - documentSetByAddingDocument:FSTTestDoc([NSString stringWithFormat:@"%@/%@", path, key], 1, - oldDocs[key], hasPendingWrites)]; - } - FSTDocumentSet *newDocuments = oldDocuments; - NSArray *documentChanges = [NSArray array]; - for (NSString *key in docsToAdd) { - FSTDocument *docToAdd = FSTTestDoc([NSString stringWithFormat:@"%@/%@", path, key], 1, - docsToAdd[key], hasPendingWrites); - newDocuments = [newDocuments documentSetByAddingDocument:docToAdd]; - documentChanges = [documentChanges - arrayByAddingObject:[FSTDocumentViewChange - changeWithDocument:docToAdd - type:FSTDocumentViewChangeTypeAdded]]; - } - FSTViewSnapshot *viewSnapshot = [[FSTViewSnapshot alloc] initWithQuery:FSTTestQuery(path) - documents:newDocuments - oldDocuments:oldDocuments - documentChanges:documentChanges - fromCache:fromCache - hasPendingWrites:hasPendingWrites - syncStateChanged:YES]; - return [FIRQuerySnapshot snapshotWithFirestore:FSTTestFirestore() - originalQuery:FSTTestQuery(path) - snapshot:viewSnapshot - metadata:metadata]; -} - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FSTAPIHelpers.mm b/Firestore/Example/Tests/API/FSTAPIHelpers.mm new file mode 100644 index 0000000..da899b7 --- /dev/null +++ b/Firestore/Example/Tests/API/FSTAPIHelpers.mm @@ -0,0 +1,115 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Example/Tests/API/FSTAPIHelpers.h" + +#import +#import +#import + +#import "Firestore/Source/API/FIRCollectionReference+Internal.h" +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRQuerySnapshot+Internal.h" +#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTViewSnapshot.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" +#import "Firestore/Source/Model/FSTPath.h" + +NS_ASSUME_NONNULL_BEGIN + +FIRFirestore *FSTTestFirestore() { + static FIRFirestore *sharedInstance = nil; + static dispatch_once_t onceToken; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + dispatch_once(&onceToken, ^{ + sharedInstance = [[FIRFirestore alloc] initWithProjectID:@"abc" + database:@"abc" + persistenceKey:@"db123" + credentialsProvider:nil + workerDispatchQueue:nil + firebaseApp:nil]; + }); +#pragma clang diagnostic pop + return sharedInstance; +} + +FIRDocumentSnapshot *FSTTestDocSnapshot(NSString *path, + FSTTestSnapshotVersion version, + NSDictionary *_Nullable data, + BOOL hasMutations, + BOOL fromCache) { + FSTDocument *doc = data ? FSTTestDoc(path, version, data, hasMutations) : nil; + return [FIRDocumentSnapshot snapshotWithFirestore:FSTTestFirestore() + documentKey:FSTTestDocKey(path) + document:doc + fromCache:fromCache]; +} + +FIRCollectionReference *FSTTestCollectionRef(NSString *path) { + return [FIRCollectionReference referenceWithPath:FSTTestPath(path) firestore:FSTTestFirestore()]; +} + +FIRDocumentReference *FSTTestDocRef(NSString *path) { + return [FIRDocumentReference referenceWithPath:FSTTestPath(path) firestore:FSTTestFirestore()]; +} + +/** A convenience method for creating a query snapshots for tests. */ +FIRQuerySnapshot *FSTTestQuerySnapshot( + NSString *path, + NSDictionary *> *oldDocs, + NSDictionary *> *docsToAdd, + BOOL hasPendingWrites, + BOOL fromCache) { + FIRSnapshotMetadata *metadata = + [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:hasPendingWrites fromCache:fromCache]; + FSTDocumentSet *oldDocuments = FSTTestDocSet(FSTDocumentComparatorByKey, @[]); + for (NSString *key in oldDocs) { + oldDocuments = [oldDocuments + documentSetByAddingDocument:FSTTestDoc([NSString stringWithFormat:@"%@/%@", path, key], 1, + oldDocs[key], hasPendingWrites)]; + } + FSTDocumentSet *newDocuments = oldDocuments; + NSArray *documentChanges = [NSArray array]; + for (NSString *key in docsToAdd) { + FSTDocument *docToAdd = FSTTestDoc([NSString stringWithFormat:@"%@/%@", path, key], 1, + docsToAdd[key], hasPendingWrites); + newDocuments = [newDocuments documentSetByAddingDocument:docToAdd]; + documentChanges = [documentChanges + arrayByAddingObject:[FSTDocumentViewChange + changeWithDocument:docToAdd + type:FSTDocumentViewChangeTypeAdded]]; + } + FSTViewSnapshot *viewSnapshot = [[FSTViewSnapshot alloc] initWithQuery:FSTTestQuery(path) + documents:newDocuments + oldDocuments:oldDocuments + documentChanges:documentChanges + fromCache:fromCache + hasPendingWrites:hasPendingWrites + syncStateChanged:YES]; + return [FIRQuerySnapshot snapshotWithFirestore:FSTTestFirestore() + originalQuery:FSTTestQuery(path) + snapshot:viewSnapshot + metadata:metadata]; +} + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTDatabaseInfoTests.m b/Firestore/Example/Tests/Core/FSTDatabaseInfoTests.m deleted file mode 100644 index c7cf22a..0000000 --- a/Firestore/Example/Tests/Core/FSTDatabaseInfoTests.m +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Core/FSTDatabaseInfo.h" - -#import - -#import "Firestore/Source/Model/FSTDatabaseID.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTDatabaseInfoTests : XCTestCase -@end - -@implementation FSTDatabaseInfoTests - -- (void)testConstructor { - FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; - FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID - persistenceKey:@"pk" - host:@"h" - sslEnabled:YES]; - XCTAssertEqualObjects(databaseInfo.databaseID.projectID, @"p"); - XCTAssertEqualObjects(databaseInfo.databaseID.databaseID, @"d"); - XCTAssertEqualObjects(databaseInfo.persistenceKey, @"pk"); - XCTAssertEqualObjects(databaseInfo.host, @"h"); - XCTAssertEqual(databaseInfo.sslEnabled, YES); -} - -- (void)testDefaultDatabase { - FSTDatabaseID *databaseID = - [FSTDatabaseID databaseIDWithProject:@"p" database:kDefaultDatabaseID]; - FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID - persistenceKey:@"pk" - host:@"h" - sslEnabled:YES]; - XCTAssertEqualObjects(databaseInfo.databaseID.projectID, @"p"); - XCTAssertEqualObjects(databaseInfo.databaseID.databaseID, @"(default)"); - XCTAssertEqualObjects(databaseInfo.persistenceKey, @"pk"); - XCTAssertEqualObjects(databaseInfo.host, @"h"); - XCTAssertEqual(databaseInfo.sslEnabled, YES); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTDatabaseInfoTests.mm b/Firestore/Example/Tests/Core/FSTDatabaseInfoTests.mm new file mode 100644 index 0000000..c7cf22a --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTDatabaseInfoTests.mm @@ -0,0 +1,59 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Core/FSTDatabaseInfo.h" + +#import + +#import "Firestore/Source/Model/FSTDatabaseID.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDatabaseInfoTests : XCTestCase +@end + +@implementation FSTDatabaseInfoTests + +- (void)testConstructor { + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; + FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID + persistenceKey:@"pk" + host:@"h" + sslEnabled:YES]; + XCTAssertEqualObjects(databaseInfo.databaseID.projectID, @"p"); + XCTAssertEqualObjects(databaseInfo.databaseID.databaseID, @"d"); + XCTAssertEqualObjects(databaseInfo.persistenceKey, @"pk"); + XCTAssertEqualObjects(databaseInfo.host, @"h"); + XCTAssertEqual(databaseInfo.sslEnabled, YES); +} + +- (void)testDefaultDatabase { + FSTDatabaseID *databaseID = + [FSTDatabaseID databaseIDWithProject:@"p" database:kDefaultDatabaseID]; + FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID + persistenceKey:@"pk" + host:@"h" + sslEnabled:YES]; + XCTAssertEqualObjects(databaseInfo.databaseID.projectID, @"p"); + XCTAssertEqualObjects(databaseInfo.databaseID.databaseID, @"(default)"); + XCTAssertEqualObjects(databaseInfo.persistenceKey, @"pk"); + XCTAssertEqualObjects(databaseInfo.host, @"h"); + XCTAssertEqual(databaseInfo.sslEnabled, YES); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTEventManagerTests.m b/Firestore/Example/Tests/Core/FSTEventManagerTests.m deleted file mode 100644 index fcde17d..0000000 --- a/Firestore/Example/Tests/Core/FSTEventManagerTests.m +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Core/FSTEventManager.h" - -#import -#import - -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSyncEngine.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" -#import "Firestore/Source/Util/FSTDispatchQueue.h" - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -// FSTEventManager implements this delegate privately -@interface FSTEventManager () -@end - -@interface FSTEventManagerTests : XCTestCase -@end - -@implementation FSTEventManagerTests { - FSTDispatchQueue *_testUserQueue; -} - -- (void)setUp { - _testUserQueue = [FSTDispatchQueue queueWith:dispatch_get_main_queue()]; -} - -- (FSTQueryListener *)noopListenerForQuery:(FSTQuery *)query { - return [[FSTQueryListener alloc] - initWithQuery:query - options:[FSTListenOptions defaultOptions] - viewSnapshotHandler:^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error){ - }]; -} - -- (void)testHandlesManyListenersPerQuery { - FSTQuery *query = FSTTestQuery(@"foo/bar"); - FSTQueryListener *listener1 = [self noopListenerForQuery:query]; - FSTQueryListener *listener2 = [self noopListenerForQuery:query]; - - FSTSyncEngine *syncEngineMock = OCMStrictClassMock([FSTSyncEngine class]); - OCMExpect([syncEngineMock setDelegate:[OCMArg any]]); - FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock]; - - OCMExpect([syncEngineMock listenToQuery:query]); - [eventManager addListener:listener1]; - OCMVerifyAll((id)syncEngineMock); - - [eventManager addListener:listener2]; - [eventManager removeListener:listener2]; - - OCMExpect([syncEngineMock stopListeningToQuery:query]); - [eventManager removeListener:listener1]; - OCMVerifyAll((id)syncEngineMock); -} - -- (void)testHandlesUnlistenOnUnknownListenerGracefully { - FSTQuery *query = FSTTestQuery(@"foo/bar"); - FSTQueryListener *listener = [self noopListenerForQuery:query]; - - FSTSyncEngine *syncEngineMock = OCMStrictClassMock([FSTSyncEngine class]); - OCMExpect([syncEngineMock setDelegate:[OCMArg any]]); - FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock]; - - [eventManager removeListener:listener]; - OCMVerifyAll((id)syncEngineMock); -} - -- (FSTQueryListener *)makeMockListenerForQuery:(FSTQuery *)query - viewSnapshotHandler:(void (^)())handler { - FSTQueryListener *listener = OCMClassMock([FSTQueryListener class]); - OCMStub([listener query]).andReturn(query); - OCMStub([listener queryDidChangeViewSnapshot:[OCMArg any]]).andDo(^(NSInvocation *invocation) { - handler(); - }); - return listener; -} - -- (void)testNotifiesListenersInTheRightOrder { - FSTQuery *query1 = FSTTestQuery(@"foo/bar"); - FSTQuery *query2 = FSTTestQuery(@"bar/baz"); - NSMutableArray *eventOrder = [NSMutableArray array]; - - FSTQueryListener *listener1 = [self makeMockListenerForQuery:query1 - viewSnapshotHandler:^{ - [eventOrder addObject:@"listener1"]; - }]; - - FSTQueryListener *listener2 = [self makeMockListenerForQuery:query2 - viewSnapshotHandler:^{ - [eventOrder addObject:@"listener2"]; - }]; - - FSTQueryListener *listener3 = [self makeMockListenerForQuery:query1 - viewSnapshotHandler:^{ - [eventOrder addObject:@"listener3"]; - }]; - - FSTSyncEngine *syncEngineMock = OCMClassMock([FSTSyncEngine class]); - FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock]; - - [eventManager addListener:listener1]; - [eventManager addListener:listener2]; - [eventManager addListener:listener3]; - OCMVerify([syncEngineMock listenToQuery:query1]); - OCMVerify([syncEngineMock listenToQuery:query2]); - - FSTViewSnapshot *snapshot1 = OCMClassMock([FSTViewSnapshot class]); - OCMStub([snapshot1 query]).andReturn(query1); - FSTViewSnapshot *snapshot2 = OCMClassMock([FSTViewSnapshot class]); - OCMStub([snapshot2 query]).andReturn(query2); - - [eventManager handleViewSnapshots:@[ snapshot1, snapshot2 ]]; - - NSArray *expected = @[ @"listener1", @"listener3", @"listener2" ]; - XCTAssertEqualObjects(eventOrder, expected); -} - -- (void)testWillForwardOnlineStateChanges { - FSTQuery *query = FSTTestQuery(@"foo/bar"); - FSTQueryListener *fakeListener = OCMClassMock([FSTQueryListener class]); - NSMutableArray *events = [NSMutableArray array]; - OCMStub([fakeListener query]).andReturn(query); - OCMStub([fakeListener applyChangedOnlineState:FSTOnlineStateUnknown]) - .andDo(^(NSInvocation *invocation) { - [events addObject:@(FSTOnlineStateUnknown)]; - }); - OCMStub([fakeListener applyChangedOnlineState:FSTOnlineStateHealthy]) - .andDo(^(NSInvocation *invocation) { - [events addObject:@(FSTOnlineStateHealthy)]; - }); - - FSTSyncEngine *syncEngineMock = OCMClassMock([FSTSyncEngine class]); - OCMExpect([syncEngineMock setDelegate:[OCMArg any]]); - FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock]; - - [eventManager addListener:fakeListener]; - XCTAssertEqualObjects(events, @[ @(FSTOnlineStateUnknown) ]); - [eventManager applyChangedOnlineState:FSTOnlineStateHealthy]; - XCTAssertEqualObjects(events, (@[ @(FSTOnlineStateUnknown), @(FSTOnlineStateHealthy) ])); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTEventManagerTests.mm b/Firestore/Example/Tests/Core/FSTEventManagerTests.mm new file mode 100644 index 0000000..fcde17d --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTEventManagerTests.mm @@ -0,0 +1,163 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Core/FSTEventManager.h" + +#import +#import + +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTSyncEngine.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" +#import "Firestore/Source/Util/FSTDispatchQueue.h" + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +// FSTEventManager implements this delegate privately +@interface FSTEventManager () +@end + +@interface FSTEventManagerTests : XCTestCase +@end + +@implementation FSTEventManagerTests { + FSTDispatchQueue *_testUserQueue; +} + +- (void)setUp { + _testUserQueue = [FSTDispatchQueue queueWith:dispatch_get_main_queue()]; +} + +- (FSTQueryListener *)noopListenerForQuery:(FSTQuery *)query { + return [[FSTQueryListener alloc] + initWithQuery:query + options:[FSTListenOptions defaultOptions] + viewSnapshotHandler:^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error){ + }]; +} + +- (void)testHandlesManyListenersPerQuery { + FSTQuery *query = FSTTestQuery(@"foo/bar"); + FSTQueryListener *listener1 = [self noopListenerForQuery:query]; + FSTQueryListener *listener2 = [self noopListenerForQuery:query]; + + FSTSyncEngine *syncEngineMock = OCMStrictClassMock([FSTSyncEngine class]); + OCMExpect([syncEngineMock setDelegate:[OCMArg any]]); + FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock]; + + OCMExpect([syncEngineMock listenToQuery:query]); + [eventManager addListener:listener1]; + OCMVerifyAll((id)syncEngineMock); + + [eventManager addListener:listener2]; + [eventManager removeListener:listener2]; + + OCMExpect([syncEngineMock stopListeningToQuery:query]); + [eventManager removeListener:listener1]; + OCMVerifyAll((id)syncEngineMock); +} + +- (void)testHandlesUnlistenOnUnknownListenerGracefully { + FSTQuery *query = FSTTestQuery(@"foo/bar"); + FSTQueryListener *listener = [self noopListenerForQuery:query]; + + FSTSyncEngine *syncEngineMock = OCMStrictClassMock([FSTSyncEngine class]); + OCMExpect([syncEngineMock setDelegate:[OCMArg any]]); + FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock]; + + [eventManager removeListener:listener]; + OCMVerifyAll((id)syncEngineMock); +} + +- (FSTQueryListener *)makeMockListenerForQuery:(FSTQuery *)query + viewSnapshotHandler:(void (^)())handler { + FSTQueryListener *listener = OCMClassMock([FSTQueryListener class]); + OCMStub([listener query]).andReturn(query); + OCMStub([listener queryDidChangeViewSnapshot:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + handler(); + }); + return listener; +} + +- (void)testNotifiesListenersInTheRightOrder { + FSTQuery *query1 = FSTTestQuery(@"foo/bar"); + FSTQuery *query2 = FSTTestQuery(@"bar/baz"); + NSMutableArray *eventOrder = [NSMutableArray array]; + + FSTQueryListener *listener1 = [self makeMockListenerForQuery:query1 + viewSnapshotHandler:^{ + [eventOrder addObject:@"listener1"]; + }]; + + FSTQueryListener *listener2 = [self makeMockListenerForQuery:query2 + viewSnapshotHandler:^{ + [eventOrder addObject:@"listener2"]; + }]; + + FSTQueryListener *listener3 = [self makeMockListenerForQuery:query1 + viewSnapshotHandler:^{ + [eventOrder addObject:@"listener3"]; + }]; + + FSTSyncEngine *syncEngineMock = OCMClassMock([FSTSyncEngine class]); + FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock]; + + [eventManager addListener:listener1]; + [eventManager addListener:listener2]; + [eventManager addListener:listener3]; + OCMVerify([syncEngineMock listenToQuery:query1]); + OCMVerify([syncEngineMock listenToQuery:query2]); + + FSTViewSnapshot *snapshot1 = OCMClassMock([FSTViewSnapshot class]); + OCMStub([snapshot1 query]).andReturn(query1); + FSTViewSnapshot *snapshot2 = OCMClassMock([FSTViewSnapshot class]); + OCMStub([snapshot2 query]).andReturn(query2); + + [eventManager handleViewSnapshots:@[ snapshot1, snapshot2 ]]; + + NSArray *expected = @[ @"listener1", @"listener3", @"listener2" ]; + XCTAssertEqualObjects(eventOrder, expected); +} + +- (void)testWillForwardOnlineStateChanges { + FSTQuery *query = FSTTestQuery(@"foo/bar"); + FSTQueryListener *fakeListener = OCMClassMock([FSTQueryListener class]); + NSMutableArray *events = [NSMutableArray array]; + OCMStub([fakeListener query]).andReturn(query); + OCMStub([fakeListener applyChangedOnlineState:FSTOnlineStateUnknown]) + .andDo(^(NSInvocation *invocation) { + [events addObject:@(FSTOnlineStateUnknown)]; + }); + OCMStub([fakeListener applyChangedOnlineState:FSTOnlineStateHealthy]) + .andDo(^(NSInvocation *invocation) { + [events addObject:@(FSTOnlineStateHealthy)]; + }); + + FSTSyncEngine *syncEngineMock = OCMClassMock([FSTSyncEngine class]); + OCMExpect([syncEngineMock setDelegate:[OCMArg any]]); + FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock]; + + [eventManager addListener:fakeListener]; + XCTAssertEqualObjects(events, @[ @(FSTOnlineStateUnknown) ]); + [eventManager applyChangedOnlineState:FSTOnlineStateHealthy]; + XCTAssertEqualObjects(events, (@[ @(FSTOnlineStateUnknown), @(FSTOnlineStateHealthy) ])); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTQueryListenerTests.m b/Firestore/Example/Tests/Core/FSTQueryListenerTests.m deleted file mode 100644 index 4856b5f..0000000 --- a/Firestore/Example/Tests/Core/FSTQueryListenerTests.m +++ /dev/null @@ -1,487 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Core/FSTEventManager.h" - -#import - -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTView.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" -#import "Firestore/Source/Util/FSTAsyncQueryListener.h" -#import "Firestore/Source/Util/FSTDispatchQueue.h" - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTQueryListenerTests : XCTestCase -@property(nonatomic, strong, readonly) FSTDispatchQueue *asyncQueue; -@end - -@implementation FSTQueryListenerTests - -- (void)setUp { - _asyncQueue = [FSTDispatchQueue - queueWith:dispatch_queue_create("FSTQueryListenerTests Queue", DISPATCH_QUEUE_SERIAL)]; -} - -- (void)testRaisesCollectionEvents { - NSMutableArray *accum = [NSMutableArray array]; - NSMutableArray *otherAccum = [NSMutableArray array]; - - FSTQuery *query = FSTTestQuery(@"rooms"); - FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); - FSTDocument *doc2prime = - FSTTestDoc(@"rooms/Hades", 3, @{@"name" : @"Hades", @"owner" : @"Jonny"}, NO); - - FSTQueryListener *listener = [self listenToQuery:query accumulatingSnapshots:accum]; - FSTQueryListener *otherListener = [self listenToQuery:query accumulatingSnapshots:otherAccum]; - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2prime ], nil); - - FSTDocumentViewChange *change1 = - [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded]; - FSTDocumentViewChange *change2 = - [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]; - FSTDocumentViewChange *change3 = - [FSTDocumentViewChange changeWithDocument:doc2prime type:FSTDocumentViewChangeTypeModified]; - FSTDocumentViewChange *change4 = - [FSTDocumentViewChange changeWithDocument:doc2prime type:FSTDocumentViewChangeTypeAdded]; - - [listener queryDidChangeViewSnapshot:snap1]; - [listener queryDidChangeViewSnapshot:snap2]; - [otherListener queryDidChangeViewSnapshot:snap2]; - - XCTAssertEqualObjects(accum, (@[ snap1, snap2 ])); - XCTAssertEqualObjects(accum[0].documentChanges, (@[ change1, change2 ])); - XCTAssertEqualObjects(accum[1].documentChanges, (@[ change3 ])); - - FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc] - initWithQuery:snap2.query - documents:snap2.documents - oldDocuments:[FSTDocumentSet documentSetWithComparator:snap2.query.comparator] - documentChanges:@[ change1, change4 ] - fromCache:snap2.fromCache - hasPendingWrites:snap2.hasPendingWrites - syncStateChanged:YES]; - XCTAssertEqualObjects(otherAccum, (@[ expectedSnap2 ])); -} - -- (void)testRaisesErrorEvent { - NSMutableArray *accum = [NSMutableArray array]; - FSTQuery *query = FSTTestQuery(@"rooms/Eros"); - - FSTQueryListener *listener = [self listenToQuery:query - handler:^(FSTViewSnapshot *snapshot, NSError *error) { - [accum addObject:error]; - }]; - - NSError *testError = - [NSError errorWithDomain:@"com.google.firestore.test" code:42 userInfo:@{@"some" : @"info"}]; - [listener queryDidError:testError]; - - XCTAssertEqualObjects(accum, @[ testError ]); -} - -- (void)testRaisesEventForEmptyCollectionAfterSync { - NSMutableArray *accum = [NSMutableArray array]; - FSTQuery *query = FSTTestQuery(@"rooms"); - - FSTQueryListener *listener = [self listenToQuery:query accumulatingSnapshots:accum]; - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); - - FSTTargetChange *ackTarget = - [FSTTargetChange changeWithDocuments:@[] - currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]; - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget); - - [listener queryDidChangeViewSnapshot:snap1]; - XCTAssertEqualObjects(accum, @[]); - - [listener queryDidChangeViewSnapshot:snap2]; - XCTAssertEqualObjects(accum, @[ snap2 ]); -} - -- (void)testMutingAsyncListenerPreventsAllSubsequentEvents { - NSMutableArray *accum = [NSMutableArray array]; - - FSTQuery *query = FSTTestQuery(@"rooms/Eros"); - FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 3, @{@"name" : @"Eros"}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/Eros", 4, @{@"name" : @"Eros2"}, NO); - - __block FSTAsyncQueryListener *listener = [[FSTAsyncQueryListener alloc] - initWithDispatchQueue:self.asyncQueue - snapshotHandler:^(FSTViewSnapshot *snapshot, NSError *error) { - [accum addObject:snapshot]; - [listener mute]; - }]; - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - FSTViewSnapshot *viewSnapshot1 = FSTTestApplyChanges(view, @[ doc1 ], nil); - FSTViewSnapshot *viewSnapshot2 = FSTTestApplyChanges(view, @[ doc2 ], nil); - - FSTViewSnapshotHandler handler = listener.asyncSnapshotHandler; - handler(viewSnapshot1, nil); - handler(viewSnapshot2, nil); - - // Drain queue - XCTestExpectation *expectation = [self expectationWithDescription:@"Queue drained"]; - [self.asyncQueue dispatchAsync:^{ - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:4.0 - handler:^(NSError *_Nullable expectationError) { - if (expectationError) { - XCTFail(@"Error waiting for timeout: %@", expectationError); - } - }]; - - // We should get the first snapshot but not the second. - XCTAssertEqualObjects(accum, @[ viewSnapshot1 ]); -} - -- (void)testDoesNotRaiseEventsForMetadataChangesUnlessSpecified { - NSMutableArray *filteredAccum = [NSMutableArray array]; - NSMutableArray *fullAccum = [NSMutableArray array]; - - FSTQuery *query = FSTTestQuery(@"rooms"); - FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); - - FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES - includeDocumentMetadataChanges:NO - waitForSyncWhenOnline:NO]; - - FSTQueryListener *filteredListener = - [self listenToQuery:query accumulatingSnapshots:filteredAccum]; - FSTQueryListener *fullListener = - [self listenToQuery:query options:options accumulatingSnapshots:fullAccum]; - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); - - FSTTargetChange *ackTarget = - [FSTTargetChange changeWithDocuments:@[ doc1 ] - currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]; - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget); - FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc2 ], nil); - - [filteredListener queryDidChangeViewSnapshot:snap1]; // local event - [filteredListener queryDidChangeViewSnapshot:snap2]; // no event - [filteredListener queryDidChangeViewSnapshot:snap3]; // doc2 update - - [fullListener queryDidChangeViewSnapshot:snap1]; // local event - [fullListener queryDidChangeViewSnapshot:snap2]; // state change event - [fullListener queryDidChangeViewSnapshot:snap3]; // doc2 update - - XCTAssertEqualObjects(filteredAccum, (@[ snap1, snap3 ])); - XCTAssertEqualObjects(fullAccum, (@[ snap1, snap2, snap3 ])); -} - -- (void)testRaisesDocumentMetadataEventsOnlyWhenSpecified { - NSMutableArray *filteredAccum = [NSMutableArray array]; - NSMutableArray *fullAccum = [NSMutableArray array]; - - FSTQuery *query = FSTTestQuery(@"rooms"); - FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES); - FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); - FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); - FSTDocument *doc3 = FSTTestDoc(@"rooms/Other", 3, @{@"name" : @"Other"}, NO); - - FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO - includeDocumentMetadataChanges:YES - waitForSyncWhenOnline:NO]; - - FSTQueryListener *filteredListener = - [self listenToQuery:query accumulatingSnapshots:filteredAccum]; - FSTQueryListener *fullListener = - [self listenToQuery:query options:options accumulatingSnapshots:fullAccum]; - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil); - FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil); - - FSTDocumentViewChange *change1 = - [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded]; - FSTDocumentViewChange *change2 = - [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]; - FSTDocumentViewChange *change3 = - [FSTDocumentViewChange changeWithDocument:doc1Prime type:FSTDocumentViewChangeTypeMetadata]; - FSTDocumentViewChange *change4 = - [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded]; - - [filteredListener queryDidChangeViewSnapshot:snap1]; - [filteredListener queryDidChangeViewSnapshot:snap2]; - [filteredListener queryDidChangeViewSnapshot:snap3]; - [fullListener queryDidChangeViewSnapshot:snap1]; - [fullListener queryDidChangeViewSnapshot:snap2]; - [fullListener queryDidChangeViewSnapshot:snap3]; - - XCTAssertEqualObjects(filteredAccum, (@[ snap1, snap3 ])); - XCTAssertEqualObjects(filteredAccum[0].documentChanges, (@[ change1, change2 ])); - XCTAssertEqualObjects(filteredAccum[1].documentChanges, (@[ change4 ])); - - XCTAssertEqualObjects(fullAccum, (@[ snap1, snap2, snap3 ])); - XCTAssertEqualObjects(fullAccum[0].documentChanges, (@[ change1, change2 ])); - XCTAssertEqualObjects(fullAccum[1].documentChanges, (@[ change3 ])); - XCTAssertEqualObjects(fullAccum[2].documentChanges, (@[ change4 ])); -} - -- (void)testRaisesQueryMetadataEventsOnlyWhenHasPendingWritesOnTheQueryChanges { - NSMutableArray *fullAccum = [NSMutableArray array]; - - FSTQuery *query = FSTTestQuery(@"rooms"); - FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES); - FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, YES); - FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); - FSTDocument *doc2Prime = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); - FSTDocument *doc3 = FSTTestDoc(@"rooms/Other", 3, @{@"name" : @"Other"}, NO); - - FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES - includeDocumentMetadataChanges:NO - waitForSyncWhenOnline:NO]; - FSTQueryListener *fullListener = - [self listenToQuery:query options:options accumulatingSnapshots:fullAccum]; - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil); - FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil); - FSTViewSnapshot *snap4 = FSTTestApplyChanges(view, @[ doc2Prime ], nil); - - [fullListener queryDidChangeViewSnapshot:snap1]; - [fullListener queryDidChangeViewSnapshot:snap2]; // Emits no events. - [fullListener queryDidChangeViewSnapshot:snap3]; - [fullListener queryDidChangeViewSnapshot:snap4]; // Metadata change event. - - FSTViewSnapshot *expectedSnap4 = [[FSTViewSnapshot alloc] initWithQuery:snap4.query - documents:snap4.documents - oldDocuments:snap3.documents - documentChanges:@[] - fromCache:snap4.fromCache - hasPendingWrites:NO - syncStateChanged:snap4.syncStateChanged]; - XCTAssertEqualObjects(fullAccum, (@[ snap1, snap3, expectedSnap4 ])); -} - -- (void)testMetadataOnlyDocumentChangesAreFilteredOutWhenIncludeDocumentMetadataChangesIsFalse { - NSMutableArray *filteredAccum = [NSMutableArray array]; - - FSTQuery *query = FSTTestQuery(@"rooms"); - FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES); - FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); - FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); - FSTDocument *doc3 = FSTTestDoc(@"rooms/Other", 3, @{@"name" : @"Other"}, NO); - - FSTQueryListener *filteredListener = - [self listenToQuery:query accumulatingSnapshots:filteredAccum]; - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime, doc3 ], nil); - - FSTDocumentViewChange *change3 = - [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded]; - - [filteredListener queryDidChangeViewSnapshot:snap1]; - [filteredListener queryDidChangeViewSnapshot:snap2]; - - FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc] initWithQuery:snap2.query - documents:snap2.documents - oldDocuments:snap1.documents - documentChanges:@[ change3 ] - fromCache:snap2.isFromCache - hasPendingWrites:snap2.hasPendingWrites - syncStateChanged:snap2.syncStateChanged]; - XCTAssertEqualObjects(filteredAccum, (@[ snap1, expectedSnap2 ])); -} - -- (void)testWillWaitForSyncIfOnline { - NSMutableArray *events = [NSMutableArray array]; - - FSTQuery *query = FSTTestQuery(@"rooms"); - FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); - FSTQueryListener *listener = - [self listenToQuery:query - options:[[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO - includeDocumentMetadataChanges:NO - waitForSyncWhenOnline:YES] - accumulatingSnapshots:events]; - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil); - FSTViewSnapshot *snap3 = - FSTTestApplyChanges(view, @[], - [FSTTargetChange changeWithDocuments:@[ doc1, doc2 ] - currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); - - [listener applyChangedOnlineState:FSTOnlineStateHealthy]; // no event - [listener queryDidChangeViewSnapshot:snap1]; - [listener applyChangedOnlineState:FSTOnlineStateUnknown]; - [listener applyChangedOnlineState:FSTOnlineStateHealthy]; - [listener queryDidChangeViewSnapshot:snap2]; - [listener queryDidChangeViewSnapshot:snap3]; - - FSTDocumentViewChange *change1 = - [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded]; - FSTDocumentViewChange *change2 = - [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]; - FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc] - initWithQuery:snap3.query - documents:snap3.documents - oldDocuments:[FSTDocumentSet documentSetWithComparator:snap3.query.comparator] - documentChanges:@[ change1, change2 ] - fromCache:NO - hasPendingWrites:NO - syncStateChanged:YES]; - XCTAssertEqualObjects(events, (@[ expectedSnap ])); -} - -- (void)testWillRaiseInitialEventWhenGoingOffline { - NSMutableArray *events = [NSMutableArray array]; - - FSTQuery *query = FSTTestQuery(@"rooms"); - FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); - FSTQueryListener *listener = - [self listenToQuery:query - options:[[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO - includeDocumentMetadataChanges:NO - waitForSyncWhenOnline:YES] - accumulatingSnapshots:events]; - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil); - - [listener applyChangedOnlineState:FSTOnlineStateHealthy]; // no event - [listener queryDidChangeViewSnapshot:snap1]; // no event - [listener applyChangedOnlineState:FSTOnlineStateFailed]; // event - [listener applyChangedOnlineState:FSTOnlineStateUnknown]; // no event - [listener applyChangedOnlineState:FSTOnlineStateFailed]; // no event - [listener queryDidChangeViewSnapshot:snap2]; // another event - - FSTDocumentViewChange *change1 = - [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded]; - FSTDocumentViewChange *change2 = - [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]; - FSTViewSnapshot *expectedSnap1 = [[FSTViewSnapshot alloc] - initWithQuery:query - documents:snap1.documents - oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator] - documentChanges:@[ change1 ] - fromCache:YES - hasPendingWrites:NO - syncStateChanged:YES]; - FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc] initWithQuery:query - documents:snap2.documents - oldDocuments:snap1.documents - documentChanges:@[ change2 ] - fromCache:YES - hasPendingWrites:NO - syncStateChanged:NO]; - XCTAssertEqualObjects(events, (@[ expectedSnap1, expectedSnap2 ])); -} - -- (void)testWillRaiseInitialEventWhenGoingOfflineAndThereAreNoDocs { - NSMutableArray *events = [NSMutableArray array]; - - FSTQuery *query = FSTTestQuery(@"rooms"); - FSTQueryListener *listener = [self listenToQuery:query - options:[FSTListenOptions defaultOptions] - accumulatingSnapshots:events]; - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); - - [listener applyChangedOnlineState:FSTOnlineStateHealthy]; // no event - [listener queryDidChangeViewSnapshot:snap1]; // no event - [listener applyChangedOnlineState:FSTOnlineStateFailed]; // event - - FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc] - initWithQuery:query - documents:snap1.documents - oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator] - documentChanges:@[] - fromCache:YES - hasPendingWrites:NO - syncStateChanged:YES]; - XCTAssertEqualObjects(events, (@[ expectedSnap ])); -} - -- (void)testWillRaiseInitialEventWhenStartingOfflineAndThereAreNoDocs { - NSMutableArray *events = [NSMutableArray array]; - - FSTQuery *query = FSTTestQuery(@"rooms"); - FSTQueryListener *listener = [self listenToQuery:query - options:[FSTListenOptions defaultOptions] - accumulatingSnapshots:events]; - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); - - [listener applyChangedOnlineState:FSTOnlineStateFailed]; // no event - [listener queryDidChangeViewSnapshot:snap1]; // event - - FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc] - initWithQuery:query - documents:snap1.documents - oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator] - documentChanges:@[] - fromCache:YES - hasPendingWrites:NO - syncStateChanged:YES]; - XCTAssertEqualObjects(events, (@[ expectedSnap ])); -} - -- (FSTQueryListener *)listenToQuery:(FSTQuery *)query handler:(FSTViewSnapshotHandler)handler { - return [[FSTQueryListener alloc] initWithQuery:query - options:[FSTListenOptions defaultOptions] - viewSnapshotHandler:handler]; -} - -- (FSTQueryListener *)listenToQuery:(FSTQuery *)query - options:(FSTListenOptions *)options - accumulatingSnapshots:(NSMutableArray *)values { - return [[FSTQueryListener alloc] initWithQuery:query - options:options - viewSnapshotHandler:^(FSTViewSnapshot *snapshot, NSError *error) { - [values addObject:snapshot]; - }]; -} - -- (FSTQueryListener *)listenToQuery:(FSTQuery *)query - accumulatingSnapshots:(NSMutableArray *)values { - return [self listenToQuery:query - options:[FSTListenOptions defaultOptions] - accumulatingSnapshots:values]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm new file mode 100644 index 0000000..4856b5f --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm @@ -0,0 +1,487 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Core/FSTEventManager.h" + +#import + +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTView.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" +#import "Firestore/Source/Remote/FSTRemoteEvent.h" +#import "Firestore/Source/Util/FSTAsyncQueryListener.h" +#import "Firestore/Source/Util/FSTDispatchQueue.h" + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTQueryListenerTests : XCTestCase +@property(nonatomic, strong, readonly) FSTDispatchQueue *asyncQueue; +@end + +@implementation FSTQueryListenerTests + +- (void)setUp { + _asyncQueue = [FSTDispatchQueue + queueWith:dispatch_queue_create("FSTQueryListenerTests Queue", DISPATCH_QUEUE_SERIAL)]; +} + +- (void)testRaisesCollectionEvents { + NSMutableArray *accum = [NSMutableArray array]; + NSMutableArray *otherAccum = [NSMutableArray array]; + + FSTQuery *query = FSTTestQuery(@"rooms"); + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + FSTDocument *doc2prime = + FSTTestDoc(@"rooms/Hades", 3, @{@"name" : @"Hades", @"owner" : @"Jonny"}, NO); + + FSTQueryListener *listener = [self listenToQuery:query accumulatingSnapshots:accum]; + FSTQueryListener *otherListener = [self listenToQuery:query accumulatingSnapshots:otherAccum]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2prime ], nil); + + FSTDocumentViewChange *change1 = + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded]; + FSTDocumentViewChange *change2 = + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]; + FSTDocumentViewChange *change3 = + [FSTDocumentViewChange changeWithDocument:doc2prime type:FSTDocumentViewChangeTypeModified]; + FSTDocumentViewChange *change4 = + [FSTDocumentViewChange changeWithDocument:doc2prime type:FSTDocumentViewChangeTypeAdded]; + + [listener queryDidChangeViewSnapshot:snap1]; + [listener queryDidChangeViewSnapshot:snap2]; + [otherListener queryDidChangeViewSnapshot:snap2]; + + XCTAssertEqualObjects(accum, (@[ snap1, snap2 ])); + XCTAssertEqualObjects(accum[0].documentChanges, (@[ change1, change2 ])); + XCTAssertEqualObjects(accum[1].documentChanges, (@[ change3 ])); + + FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc] + initWithQuery:snap2.query + documents:snap2.documents + oldDocuments:[FSTDocumentSet documentSetWithComparator:snap2.query.comparator] + documentChanges:@[ change1, change4 ] + fromCache:snap2.fromCache + hasPendingWrites:snap2.hasPendingWrites + syncStateChanged:YES]; + XCTAssertEqualObjects(otherAccum, (@[ expectedSnap2 ])); +} + +- (void)testRaisesErrorEvent { + NSMutableArray *accum = [NSMutableArray array]; + FSTQuery *query = FSTTestQuery(@"rooms/Eros"); + + FSTQueryListener *listener = [self listenToQuery:query + handler:^(FSTViewSnapshot *snapshot, NSError *error) { + [accum addObject:error]; + }]; + + NSError *testError = + [NSError errorWithDomain:@"com.google.firestore.test" code:42 userInfo:@{@"some" : @"info"}]; + [listener queryDidError:testError]; + + XCTAssertEqualObjects(accum, @[ testError ]); +} + +- (void)testRaisesEventForEmptyCollectionAfterSync { + NSMutableArray *accum = [NSMutableArray array]; + FSTQuery *query = FSTTestQuery(@"rooms"); + + FSTQueryListener *listener = [self listenToQuery:query accumulatingSnapshots:accum]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); + + FSTTargetChange *ackTarget = + [FSTTargetChange changeWithDocuments:@[] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]; + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget); + + [listener queryDidChangeViewSnapshot:snap1]; + XCTAssertEqualObjects(accum, @[]); + + [listener queryDidChangeViewSnapshot:snap2]; + XCTAssertEqualObjects(accum, @[ snap2 ]); +} + +- (void)testMutingAsyncListenerPreventsAllSubsequentEvents { + NSMutableArray *accum = [NSMutableArray array]; + + FSTQuery *query = FSTTestQuery(@"rooms/Eros"); + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 3, @{@"name" : @"Eros"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Eros", 4, @{@"name" : @"Eros2"}, NO); + + __block FSTAsyncQueryListener *listener = [[FSTAsyncQueryListener alloc] + initWithDispatchQueue:self.asyncQueue + snapshotHandler:^(FSTViewSnapshot *snapshot, NSError *error) { + [accum addObject:snapshot]; + [listener mute]; + }]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *viewSnapshot1 = FSTTestApplyChanges(view, @[ doc1 ], nil); + FSTViewSnapshot *viewSnapshot2 = FSTTestApplyChanges(view, @[ doc2 ], nil); + + FSTViewSnapshotHandler handler = listener.asyncSnapshotHandler; + handler(viewSnapshot1, nil); + handler(viewSnapshot2, nil); + + // Drain queue + XCTestExpectation *expectation = [self expectationWithDescription:@"Queue drained"]; + [self.asyncQueue dispatchAsync:^{ + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:4.0 + handler:^(NSError *_Nullable expectationError) { + if (expectationError) { + XCTFail(@"Error waiting for timeout: %@", expectationError); + } + }]; + + // We should get the first snapshot but not the second. + XCTAssertEqualObjects(accum, @[ viewSnapshot1 ]); +} + +- (void)testDoesNotRaiseEventsForMetadataChangesUnlessSpecified { + NSMutableArray *filteredAccum = [NSMutableArray array]; + NSMutableArray *fullAccum = [NSMutableArray array]; + + FSTQuery *query = FSTTestQuery(@"rooms"); + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + + FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES + includeDocumentMetadataChanges:NO + waitForSyncWhenOnline:NO]; + + FSTQueryListener *filteredListener = + [self listenToQuery:query accumulatingSnapshots:filteredAccum]; + FSTQueryListener *fullListener = + [self listenToQuery:query options:options accumulatingSnapshots:fullAccum]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); + + FSTTargetChange *ackTarget = + [FSTTargetChange changeWithDocuments:@[ doc1 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]; + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget); + FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc2 ], nil); + + [filteredListener queryDidChangeViewSnapshot:snap1]; // local event + [filteredListener queryDidChangeViewSnapshot:snap2]; // no event + [filteredListener queryDidChangeViewSnapshot:snap3]; // doc2 update + + [fullListener queryDidChangeViewSnapshot:snap1]; // local event + [fullListener queryDidChangeViewSnapshot:snap2]; // state change event + [fullListener queryDidChangeViewSnapshot:snap3]; // doc2 update + + XCTAssertEqualObjects(filteredAccum, (@[ snap1, snap3 ])); + XCTAssertEqualObjects(fullAccum, (@[ snap1, snap2, snap3 ])); +} + +- (void)testRaisesDocumentMetadataEventsOnlyWhenSpecified { + NSMutableArray *filteredAccum = [NSMutableArray array]; + NSMutableArray *fullAccum = [NSMutableArray array]; + + FSTQuery *query = FSTTestQuery(@"rooms"); + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/Other", 3, @{@"name" : @"Other"}, NO); + + FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO + includeDocumentMetadataChanges:YES + waitForSyncWhenOnline:NO]; + + FSTQueryListener *filteredListener = + [self listenToQuery:query accumulatingSnapshots:filteredAccum]; + FSTQueryListener *fullListener = + [self listenToQuery:query options:options accumulatingSnapshots:fullAccum]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil); + FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil); + + FSTDocumentViewChange *change1 = + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded]; + FSTDocumentViewChange *change2 = + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]; + FSTDocumentViewChange *change3 = + [FSTDocumentViewChange changeWithDocument:doc1Prime type:FSTDocumentViewChangeTypeMetadata]; + FSTDocumentViewChange *change4 = + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded]; + + [filteredListener queryDidChangeViewSnapshot:snap1]; + [filteredListener queryDidChangeViewSnapshot:snap2]; + [filteredListener queryDidChangeViewSnapshot:snap3]; + [fullListener queryDidChangeViewSnapshot:snap1]; + [fullListener queryDidChangeViewSnapshot:snap2]; + [fullListener queryDidChangeViewSnapshot:snap3]; + + XCTAssertEqualObjects(filteredAccum, (@[ snap1, snap3 ])); + XCTAssertEqualObjects(filteredAccum[0].documentChanges, (@[ change1, change2 ])); + XCTAssertEqualObjects(filteredAccum[1].documentChanges, (@[ change4 ])); + + XCTAssertEqualObjects(fullAccum, (@[ snap1, snap2, snap3 ])); + XCTAssertEqualObjects(fullAccum[0].documentChanges, (@[ change1, change2 ])); + XCTAssertEqualObjects(fullAccum[1].documentChanges, (@[ change3 ])); + XCTAssertEqualObjects(fullAccum[2].documentChanges, (@[ change4 ])); +} + +- (void)testRaisesQueryMetadataEventsOnlyWhenHasPendingWritesOnTheQueryChanges { + NSMutableArray *fullAccum = [NSMutableArray array]; + + FSTQuery *query = FSTTestQuery(@"rooms"); + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, YES); + FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc2Prime = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/Other", 3, @{@"name" : @"Other"}, NO); + + FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES + includeDocumentMetadataChanges:NO + waitForSyncWhenOnline:NO]; + FSTQueryListener *fullListener = + [self listenToQuery:query options:options accumulatingSnapshots:fullAccum]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil); + FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil); + FSTViewSnapshot *snap4 = FSTTestApplyChanges(view, @[ doc2Prime ], nil); + + [fullListener queryDidChangeViewSnapshot:snap1]; + [fullListener queryDidChangeViewSnapshot:snap2]; // Emits no events. + [fullListener queryDidChangeViewSnapshot:snap3]; + [fullListener queryDidChangeViewSnapshot:snap4]; // Metadata change event. + + FSTViewSnapshot *expectedSnap4 = [[FSTViewSnapshot alloc] initWithQuery:snap4.query + documents:snap4.documents + oldDocuments:snap3.documents + documentChanges:@[] + fromCache:snap4.fromCache + hasPendingWrites:NO + syncStateChanged:snap4.syncStateChanged]; + XCTAssertEqualObjects(fullAccum, (@[ snap1, snap3, expectedSnap4 ])); +} + +- (void)testMetadataOnlyDocumentChangesAreFilteredOutWhenIncludeDocumentMetadataChangesIsFalse { + NSMutableArray *filteredAccum = [NSMutableArray array]; + + FSTQuery *query = FSTTestQuery(@"rooms"); + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/Other", 3, @{@"name" : @"Other"}, NO); + + FSTQueryListener *filteredListener = + [self listenToQuery:query accumulatingSnapshots:filteredAccum]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime, doc3 ], nil); + + FSTDocumentViewChange *change3 = + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded]; + + [filteredListener queryDidChangeViewSnapshot:snap1]; + [filteredListener queryDidChangeViewSnapshot:snap2]; + + FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc] initWithQuery:snap2.query + documents:snap2.documents + oldDocuments:snap1.documents + documentChanges:@[ change3 ] + fromCache:snap2.isFromCache + hasPendingWrites:snap2.hasPendingWrites + syncStateChanged:snap2.syncStateChanged]; + XCTAssertEqualObjects(filteredAccum, (@[ snap1, expectedSnap2 ])); +} + +- (void)testWillWaitForSyncIfOnline { + NSMutableArray *events = [NSMutableArray array]; + + FSTQuery *query = FSTTestQuery(@"rooms"); + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + FSTQueryListener *listener = + [self listenToQuery:query + options:[[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO + includeDocumentMetadataChanges:NO + waitForSyncWhenOnline:YES] + accumulatingSnapshots:events]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil); + FSTViewSnapshot *snap3 = + FSTTestApplyChanges(view, @[], + [FSTTargetChange changeWithDocuments:@[ doc1, doc2 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); + + [listener applyChangedOnlineState:FSTOnlineStateHealthy]; // no event + [listener queryDidChangeViewSnapshot:snap1]; + [listener applyChangedOnlineState:FSTOnlineStateUnknown]; + [listener applyChangedOnlineState:FSTOnlineStateHealthy]; + [listener queryDidChangeViewSnapshot:snap2]; + [listener queryDidChangeViewSnapshot:snap3]; + + FSTDocumentViewChange *change1 = + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded]; + FSTDocumentViewChange *change2 = + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]; + FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc] + initWithQuery:snap3.query + documents:snap3.documents + oldDocuments:[FSTDocumentSet documentSetWithComparator:snap3.query.comparator] + documentChanges:@[ change1, change2 ] + fromCache:NO + hasPendingWrites:NO + syncStateChanged:YES]; + XCTAssertEqualObjects(events, (@[ expectedSnap ])); +} + +- (void)testWillRaiseInitialEventWhenGoingOffline { + NSMutableArray *events = [NSMutableArray array]; + + FSTQuery *query = FSTTestQuery(@"rooms"); + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + FSTQueryListener *listener = + [self listenToQuery:query + options:[[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO + includeDocumentMetadataChanges:NO + waitForSyncWhenOnline:YES] + accumulatingSnapshots:events]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil); + + [listener applyChangedOnlineState:FSTOnlineStateHealthy]; // no event + [listener queryDidChangeViewSnapshot:snap1]; // no event + [listener applyChangedOnlineState:FSTOnlineStateFailed]; // event + [listener applyChangedOnlineState:FSTOnlineStateUnknown]; // no event + [listener applyChangedOnlineState:FSTOnlineStateFailed]; // no event + [listener queryDidChangeViewSnapshot:snap2]; // another event + + FSTDocumentViewChange *change1 = + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded]; + FSTDocumentViewChange *change2 = + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]; + FSTViewSnapshot *expectedSnap1 = [[FSTViewSnapshot alloc] + initWithQuery:query + documents:snap1.documents + oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator] + documentChanges:@[ change1 ] + fromCache:YES + hasPendingWrites:NO + syncStateChanged:YES]; + FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc] initWithQuery:query + documents:snap2.documents + oldDocuments:snap1.documents + documentChanges:@[ change2 ] + fromCache:YES + hasPendingWrites:NO + syncStateChanged:NO]; + XCTAssertEqualObjects(events, (@[ expectedSnap1, expectedSnap2 ])); +} + +- (void)testWillRaiseInitialEventWhenGoingOfflineAndThereAreNoDocs { + NSMutableArray *events = [NSMutableArray array]; + + FSTQuery *query = FSTTestQuery(@"rooms"); + FSTQueryListener *listener = [self listenToQuery:query + options:[FSTListenOptions defaultOptions] + accumulatingSnapshots:events]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); + + [listener applyChangedOnlineState:FSTOnlineStateHealthy]; // no event + [listener queryDidChangeViewSnapshot:snap1]; // no event + [listener applyChangedOnlineState:FSTOnlineStateFailed]; // event + + FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc] + initWithQuery:query + documents:snap1.documents + oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator] + documentChanges:@[] + fromCache:YES + hasPendingWrites:NO + syncStateChanged:YES]; + XCTAssertEqualObjects(events, (@[ expectedSnap ])); +} + +- (void)testWillRaiseInitialEventWhenStartingOfflineAndThereAreNoDocs { + NSMutableArray *events = [NSMutableArray array]; + + FSTQuery *query = FSTTestQuery(@"rooms"); + FSTQueryListener *listener = [self listenToQuery:query + options:[FSTListenOptions defaultOptions] + accumulatingSnapshots:events]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); + + [listener applyChangedOnlineState:FSTOnlineStateFailed]; // no event + [listener queryDidChangeViewSnapshot:snap1]; // event + + FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc] + initWithQuery:query + documents:snap1.documents + oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator] + documentChanges:@[] + fromCache:YES + hasPendingWrites:NO + syncStateChanged:YES]; + XCTAssertEqualObjects(events, (@[ expectedSnap ])); +} + +- (FSTQueryListener *)listenToQuery:(FSTQuery *)query handler:(FSTViewSnapshotHandler)handler { + return [[FSTQueryListener alloc] initWithQuery:query + options:[FSTListenOptions defaultOptions] + viewSnapshotHandler:handler]; +} + +- (FSTQueryListener *)listenToQuery:(FSTQuery *)query + options:(FSTListenOptions *)options + accumulatingSnapshots:(NSMutableArray *)values { + return [[FSTQueryListener alloc] initWithQuery:query + options:options + viewSnapshotHandler:^(FSTViewSnapshot *snapshot, NSError *error) { + [values addObject:snapshot]; + }]; +} + +- (FSTQueryListener *)listenToQuery:(FSTQuery *)query + accumulatingSnapshots:(NSMutableArray *)values { + return [self listenToQuery:query + options:[FSTListenOptions defaultOptions] + accumulatingSnapshots:values]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTQueryTests.m b/Firestore/Example/Tests/Core/FSTQueryTests.m deleted file mode 100644 index 3d2bd82..0000000 --- a/Firestore/Example/Tests/Core/FSTQueryTests.m +++ /dev/null @@ -1,566 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Core/FSTQuery.h" - -#import - -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/Model/FSTDatabaseID.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTPath.h" - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -/** Convenience methods for building test queries. */ -@interface FSTQuery (Tests) -- (FSTQuery *)queryByAddingSortBy:(NSString *)key ascending:(BOOL)ascending; -@end - -@implementation FSTQuery (Tests) - -- (FSTQuery *)queryByAddingSortBy:(NSString *)key ascending:(BOOL)ascending { - return [self queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(key) - ascending:ascending]]; -} - -@end - -@interface FSTQueryTests : XCTestCase -@end - -@implementation FSTQueryTests - -- (void)testConstructor { - FSTResourcePath *path = - [FSTResourcePath pathWithSegments:@[ @"rooms", @"Firestore", @"messages", @"0001" ]]; - FSTQuery *query = [FSTQuery queryWithPath:path]; - XCTAssertNotNil(query); - - XCTAssertEqual(query.sortOrders.count, 1); - XCTAssertEqualObjects(query.sortOrders[0].field.canonicalString, kDocumentKeyPath); - XCTAssertEqual(query.sortOrders[0].ascending, YES); - - XCTAssertEqual(query.explicitSortOrders.count, 0); -} - -- (void)testOrderBy { - FSTQuery *query = FSTTestQuery(@"rooms/Firestore/messages"); - query = - [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"length") - ascending:NO]]; - - XCTAssertEqual(query.sortOrders.count, 2); - XCTAssertEqualObjects(query.sortOrders[0].field.canonicalString, @"length"); - XCTAssertEqual(query.sortOrders[0].ascending, NO); - XCTAssertEqualObjects(query.sortOrders[1].field.canonicalString, kDocumentKeyPath); - XCTAssertEqual(query.sortOrders[1].ascending, NO); - - XCTAssertEqual(query.explicitSortOrders.count, 1); - XCTAssertEqualObjects(query.explicitSortOrders[0].field.canonicalString, @"length"); - XCTAssertEqual(query.explicitSortOrders[0].ascending, NO); -} - -- (void)testMatchesBasedOnDocumentKey { - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); - FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO); - - // document query - FSTQuery *query = FSTTestQuery(@"rooms/eros/messages/1"); - XCTAssertTrue([query matchesDocument:doc1]); - XCTAssertFalse([query matchesDocument:doc2]); - XCTAssertFalse([query matchesDocument:doc3]); -} - -- (void)testMatchesCorrectlyForShallowAncestorQuery { - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); - FSTDocument *doc1Meta = FSTTestDoc(@"rooms/eros/messages/1/meta/1", 0, @{@"meta" : @"mv"}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); - FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO); - - // shallow ancestor query - FSTQuery *query = FSTTestQuery(@"rooms/eros/messages"); - XCTAssertTrue([query matchesDocument:doc1]); - XCTAssertFalse([query matchesDocument:doc1Meta]); - XCTAssertTrue([query matchesDocument:doc2]); - XCTAssertFalse([query matchesDocument:doc3]); -} - -- (void)testEmptyFieldsAreAllowedForQueries { - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); - - FSTQuery *query = [FSTTestQuery(@"rooms/eros/messages") - queryByAddingFilter:FSTTestFilter(@"text", @"==", @"msg1")]; - XCTAssertTrue([query matchesDocument:doc1]); - XCTAssertFalse([query matchesDocument:doc2]); -} - -- (void)testMatchesPrimitiveValuesForFilters { - FSTQuery *query1 = - [FSTTestQuery(@"collection") queryByAddingFilter:FSTTestFilter(@"sort", @">=", @(2))]; - FSTQuery *query2 = - [FSTTestQuery(@"collection") queryByAddingFilter:FSTTestFilter(@"sort", @"<=", @(2))]; - - FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @1 }, NO); - FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO); - FSTDocument *doc3 = FSTTestDoc(@"collection/3", 0, @{ @"sort" : @3 }, NO); - FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @NO }, NO); - FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{@"sort" : @"string"}, NO); - FSTDocument *doc6 = FSTTestDoc(@"collection/6", 0, @{}, NO); - - XCTAssertFalse([query1 matchesDocument:doc1]); - XCTAssertTrue([query1 matchesDocument:doc2]); - XCTAssertTrue([query1 matchesDocument:doc3]); - XCTAssertFalse([query1 matchesDocument:doc4]); - XCTAssertFalse([query1 matchesDocument:doc5]); - XCTAssertFalse([query1 matchesDocument:doc6]); - - XCTAssertTrue([query2 matchesDocument:doc1]); - XCTAssertTrue([query2 matchesDocument:doc2]); - XCTAssertFalse([query2 matchesDocument:doc3]); - XCTAssertFalse([query2 matchesDocument:doc4]); - XCTAssertFalse([query2 matchesDocument:doc5]); - XCTAssertFalse([query2 matchesDocument:doc6]); -} - -- (void)testNullFilter { - FSTQuery *query = [FSTTestQuery(@"collection") - queryByAddingFilter:FSTTestFilter(@"sort", @"==", [NSNull null])]; - FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{@"sort" : [NSNull null]}, NO); - FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO); - FSTDocument *doc3 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @3.1 }, NO); - FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @NO }, NO); - FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{@"sort" : @"string"}, NO); - - XCTAssertTrue([query matchesDocument:doc1]); - XCTAssertFalse([query matchesDocument:doc2]); - XCTAssertFalse([query matchesDocument:doc3]); - XCTAssertFalse([query matchesDocument:doc4]); - XCTAssertFalse([query matchesDocument:doc5]); -} - -- (void)testNanFilter { - FSTQuery *query = - [FSTTestQuery(@"collection") queryByAddingFilter:FSTTestFilter(@"sort", @"==", @(NAN))]; - FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @(NAN) }, NO); - FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO); - FSTDocument *doc3 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @3.1 }, NO); - FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @NO }, NO); - FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{@"sort" : @"string"}, NO); - - XCTAssertTrue([query matchesDocument:doc1]); - XCTAssertFalse([query matchesDocument:doc2]); - XCTAssertFalse([query matchesDocument:doc3]); - XCTAssertFalse([query matchesDocument:doc4]); - XCTAssertFalse([query matchesDocument:doc5]); -} - -- (void)testDoesNotMatchComplexObjectsForFilters { - FSTQuery *query1 = - [FSTTestQuery(@"collection") queryByAddingFilter:FSTTestFilter(@"sort", @"<=", @(2))]; - FSTQuery *query2 = - [FSTTestQuery(@"collection") queryByAddingFilter:FSTTestFilter(@"sort", @">=", @(2))]; - - FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @2 }, NO); - FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @[] }, NO); - FSTDocument *doc3 = FSTTestDoc(@"collection/3", 0, @{ @"sort" : @[ @1 ] }, NO); - FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @{@"foo" : @2} }, NO); - FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{ @"sort" : @{@"foo" : @"bar"} }, NO); - FSTDocument *doc6 = FSTTestDoc(@"collection/6", 0, @{ @"sort" : @{} }, NO); // no sort field - FSTDocument *doc7 = FSTTestDoc(@"collection/7", 0, @{ @"sort" : @[ @3, @1 ] }, NO); - - XCTAssertTrue([query1 matchesDocument:doc1]); - XCTAssertFalse([query1 matchesDocument:doc2]); - XCTAssertFalse([query1 matchesDocument:doc3]); - XCTAssertFalse([query1 matchesDocument:doc4]); - XCTAssertFalse([query1 matchesDocument:doc5]); - XCTAssertFalse([query1 matchesDocument:doc6]); - XCTAssertFalse([query1 matchesDocument:doc7]); - - XCTAssertTrue([query2 matchesDocument:doc1]); - XCTAssertFalse([query2 matchesDocument:doc2]); - XCTAssertFalse([query2 matchesDocument:doc3]); - XCTAssertFalse([query2 matchesDocument:doc4]); - XCTAssertFalse([query2 matchesDocument:doc5]); - XCTAssertFalse([query2 matchesDocument:doc6]); - XCTAssertFalse([query2 matchesDocument:doc7]); -} - -- (void)testDoesntRemoveComplexObjectsWithOrderBy { - FSTQuery *query1 = [FSTTestQuery(@"collection") - queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort") - ascending:YES]]; - - FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @2 }, NO); - FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @[] }, NO); - FSTDocument *doc3 = FSTTestDoc(@"collection/3", 0, @{ @"sort" : @[ @1 ] }, NO); - FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @{@"foo" : @2} }, NO); - FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{ @"sort" : @{@"foo" : @"bar"} }, NO); - FSTDocument *doc6 = FSTTestDoc(@"collection/6", 0, @{}, NO); - - XCTAssertTrue([query1 matchesDocument:doc1]); - XCTAssertTrue([query1 matchesDocument:doc2]); - XCTAssertTrue([query1 matchesDocument:doc3]); - XCTAssertTrue([query1 matchesDocument:doc4]); - XCTAssertTrue([query1 matchesDocument:doc5]); - XCTAssertFalse([query1 matchesDocument:doc6]); -} - -- (void)testFiltersBasedOnArrayValue { - FSTQuery *baseQuery = FSTTestQuery(@"collection"); - FSTDocument *doc1 = FSTTestDoc(@"collection/doc", 0, @{ @"tags" : @[ @"foo", @1, @YES ] }, NO); - - NSArray> *matchingFilters = - @[ FSTTestFilter(@"tags", @"==", @[ @"foo", @1, @YES ]) ]; - - NSArray> *nonMatchingFilters = @[ - FSTTestFilter(@"tags", @"==", @"foo"), - FSTTestFilter(@"tags", @"==", @[ @"foo", @1 ]), - FSTTestFilter(@"tags", @"==", @[ @"foo", @YES, @1 ]), - ]; - - for (id filter in matchingFilters) { - XCTAssertTrue([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]); - } - - for (id filter in nonMatchingFilters) { - XCTAssertFalse([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]); - } -} - -- (void)testFiltersBasedOnObjectValue { - FSTQuery *baseQuery = FSTTestQuery(@"collection"); - FSTDocument *doc1 = - FSTTestDoc(@"collection/doc", 0, - @{ @"tags" : @{@"foo" : @"foo", @"a" : @0, @"b" : @YES, @"c" : @(NAN)} }, NO); - - NSArray> *matchingFilters = @[ - FSTTestFilter(@"tags", @"==", - @{ @"foo" : @"foo", - @"a" : @0, - @"b" : @YES, - @"c" : @(NAN) }), - FSTTestFilter(@"tags", @"==", - @{ @"b" : @YES, - @"a" : @0, - @"foo" : @"foo", - @"c" : @(NAN) }), - FSTTestFilter(@"tags.foo", @"==", @"foo") - ]; - - NSArray> *nonMatchingFilters = @[ - FSTTestFilter(@"tags", @"==", @"foo"), FSTTestFilter(@"tags", @"==", @{ - @"foo" : @"foo", - @"a" : @0, - @"b" : @YES, - }) - ]; - - for (id filter in matchingFilters) { - XCTAssertTrue([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]); - } - - for (id filter in nonMatchingFilters) { - XCTAssertFalse([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]); - } -} - -/** - * Checks that an ordered array of elements yields the correct pair-wise comparison result for the - * supplied comparator. - */ -- (void)assertCorrectComparisonsWithArray:(NSArray *)array comparator:(NSComparator)comp { - [array enumerateObjectsUsingBlock:^(id iObj, NSUInteger i, BOOL *outerStop) { - [array enumerateObjectsUsingBlock:^(id _Nonnull jObj, NSUInteger j, BOOL *innerStop) { - NSComparisonResult expected = [@(i) compare:@(j)]; - NSComparisonResult actual = comp(iObj, jObj); - XCTAssertEqual(actual, expected, @"Compared %@ to %@ at (%lu, %lu).", iObj, jObj, - (unsigned long)i, (unsigned long)j); - }]; - }]; -} - -- (void)testSortsDocumentsInTheCorrectOrder { - FSTQuery *query = FSTTestQuery(@"collection"); - query = - [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort") - ascending:YES]]; - - // clang-format off - NSArray *docs = @[ - FSTTestDoc(@"collection/1", 0, @{@"sort": [NSNull null]}, NO), - FSTTestDoc(@"collection/1", 0, @{@"sort": @NO}, NO), - FSTTestDoc(@"collection/1", 0, @{@"sort": @YES}, NO), - FSTTestDoc(@"collection/1", 0, @{@"sort": @1}, NO), - FSTTestDoc(@"collection/2", 0, @{@"sort": @1}, NO), // by key - FSTTestDoc(@"collection/3", 0, @{@"sort": @1}, NO), // by key - FSTTestDoc(@"collection/1", 0, @{@"sort": @1.9}, NO), - FSTTestDoc(@"collection/1", 0, @{@"sort": @2}, NO), - FSTTestDoc(@"collection/1", 0, @{@"sort": @2.1}, NO), - FSTTestDoc(@"collection/1", 0, @{@"sort": @""}, NO), - FSTTestDoc(@"collection/1", 0, @{@"sort": @"a"}, NO), - FSTTestDoc(@"collection/1", 0, @{@"sort": @"ab"}, NO), - FSTTestDoc(@"collection/1", 0, @{@"sort": @"b"}, NO), - FSTTestDoc(@"collection/1", 0, @{@"sort": - FSTTestRef(@"project", kDefaultDatabaseID, @"collection/id1")}, NO), - ]; - // clang-format on - - [self assertCorrectComparisonsWithArray:docs comparator:query.comparator]; -} - -- (void)testSortsDocumentsUsingMultipleFields { - FSTQuery *query = FSTTestQuery(@"collection"); - query = - [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort1") - ascending:YES]]; - query = - [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort2") - ascending:YES]]; - - // clang-format off - NSArray *docs = - @[FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @1}, NO), - FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @2}, NO), - FSTTestDoc(@"collection/2", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key - FSTTestDoc(@"collection/3", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key - FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @3}, NO), - FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @1}, NO), - FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @2}, NO), - FSTTestDoc(@"collection/2", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key - FSTTestDoc(@"collection/3", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key - FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @3}, NO), - ]; - // clang-format on - - [self assertCorrectComparisonsWithArray:docs comparator:query.comparator]; -} - -- (void)testSortsDocumentsWithDescendingToo { - FSTQuery *query = FSTTestQuery(@"collection"); - query = - [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort1") - ascending:NO]]; - query = - [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort2") - ascending:NO]]; - - // clang-format off - NSArray *docs = - @[FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @3}, NO), - FSTTestDoc(@"collection/3", 0, @{@"sort1": @2, @"sort2": @2}, NO), - FSTTestDoc(@"collection/2", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key - FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key - FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @1}, NO), - FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @3}, NO), - FSTTestDoc(@"collection/3", 0, @{@"sort1": @1, @"sort2": @2}, NO), - FSTTestDoc(@"collection/2", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key - FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key - FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @1}, NO), - ]; - // clang-format on - - [self assertCorrectComparisonsWithArray:docs comparator:query.comparator]; -} - -- (void)testEquality { - FSTQuery *q11 = FSTTestQuery(@"foo"); - q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; - q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; - FSTQuery *q12 = FSTTestQuery(@"foo"); - q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; - q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; - - FSTQuery *q21 = FSTTestQuery(@"foo"); - FSTQuery *q22 = FSTTestQuery(@"foo"); - - FSTQuery *q31 = FSTTestQuery(@"foo/bar"); - FSTQuery *q32 = FSTTestQuery(@"foo/bar"); - - FSTQuery *q41 = FSTTestQuery(@"foo"); - q41 = [q41 queryByAddingSortBy:@"foo" ascending:YES]; - q41 = [q41 queryByAddingSortBy:@"bar" ascending:YES]; - FSTQuery *q42 = FSTTestQuery(@"foo"); - q42 = [q42 queryByAddingSortBy:@"foo" ascending:YES]; - q42 = [q42 queryByAddingSortBy:@"bar" ascending:YES]; - FSTQuery *q43Diff = FSTTestQuery(@"foo"); - q43Diff = [q43Diff queryByAddingSortBy:@"bar" ascending:YES]; - q43Diff = [q43Diff queryByAddingSortBy:@"foo" ascending:YES]; - - FSTQuery *q51 = FSTTestQuery(@"foo"); - q51 = [q51 queryByAddingSortBy:@"foo" ascending:YES]; - q51 = [q51 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; - FSTQuery *q52 = FSTTestQuery(@"foo"); - q52 = [q52 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; - q52 = [q52 queryByAddingSortBy:@"foo" ascending:YES]; - FSTQuery *q53Diff = FSTTestQuery(@"foo"); - q53Diff = [q53Diff queryByAddingFilter:FSTTestFilter(@"bar", @">", @(2))]; - q53Diff = [q53Diff queryByAddingSortBy:@"bar" ascending:YES]; - - FSTQuery *q61 = FSTTestQuery(@"foo"); - q61 = [q61 queryBySettingLimit:10]; - - // XCTAssertEqualObjects(q11, q12); // TODO(klimt): not canonical yet - XCTAssertNotEqualObjects(q11, q21); - XCTAssertNotEqualObjects(q11, q31); - XCTAssertNotEqualObjects(q11, q41); - XCTAssertNotEqualObjects(q11, q51); - XCTAssertNotEqualObjects(q11, q61); - - XCTAssertEqualObjects(q21, q22); - XCTAssertNotEqualObjects(q21, q31); - XCTAssertNotEqualObjects(q21, q41); - XCTAssertNotEqualObjects(q21, q51); - XCTAssertNotEqualObjects(q21, q61); - - XCTAssertEqualObjects(q31, q32); - XCTAssertNotEqualObjects(q31, q41); - XCTAssertNotEqualObjects(q31, q51); - XCTAssertNotEqualObjects(q31, q61); - - XCTAssertEqualObjects(q41, q42); - XCTAssertNotEqualObjects(q41, q43Diff); - XCTAssertNotEqualObjects(q41, q51); - XCTAssertNotEqualObjects(q41, q61); - - XCTAssertEqualObjects(q51, q52); - XCTAssertNotEqualObjects(q51, q53Diff); - XCTAssertNotEqualObjects(q51, q61); -} - -- (void)testUniqueIds { - FSTQuery *q11 = FSTTestQuery(@"foo"); - q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; - q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; - FSTQuery *q12 = FSTTestQuery(@"foo"); - q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; - q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; - - FSTQuery *q21 = FSTTestQuery(@"foo"); - FSTQuery *q22 = FSTTestQuery(@"foo"); - - FSTQuery *q31 = FSTTestQuery(@"foo/bar"); - FSTQuery *q32 = FSTTestQuery(@"foo/bar"); - - FSTQuery *q41 = FSTTestQuery(@"foo"); - q41 = [q41 queryByAddingSortBy:@"foo" ascending:YES]; - q41 = [q41 queryByAddingSortBy:@"bar" ascending:YES]; - FSTQuery *q42 = FSTTestQuery(@"foo"); - q42 = [q42 queryByAddingSortBy:@"foo" ascending:YES]; - q42 = [q42 queryByAddingSortBy:@"bar" ascending:YES]; - FSTQuery *q43Diff = FSTTestQuery(@"foo"); - q43Diff = [q43Diff queryByAddingSortBy:@"bar" ascending:YES]; - q43Diff = [q43Diff queryByAddingSortBy:@"foo" ascending:YES]; - - FSTQuery *q51 = FSTTestQuery(@"foo"); - q51 = [q51 queryByAddingSortBy:@"foo" ascending:YES]; - q51 = [q51 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; - FSTQuery *q52 = FSTTestQuery(@"foo"); - q52 = [q52 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; - q52 = [q52 queryByAddingSortBy:@"foo" ascending:YES]; - FSTQuery *q53Diff = FSTTestQuery(@"foo"); - q53Diff = [q53Diff queryByAddingFilter:FSTTestFilter(@"bar", @">", @(2))]; - q53Diff = [q53Diff queryByAddingSortBy:@"bar" ascending:YES]; - - FSTQuery *q61 = FSTTestQuery(@"foo"); - q61 = [q61 queryBySettingLimit:10]; - - // XCTAssertEqual(q11.hash, q12.hash); // TODO(klimt): not canonical yet - XCTAssertNotEqual(q11.hash, q21.hash); - XCTAssertNotEqual(q11.hash, q31.hash); - XCTAssertNotEqual(q11.hash, q41.hash); - XCTAssertNotEqual(q11.hash, q51.hash); - XCTAssertNotEqual(q11.hash, q61.hash); - - XCTAssertEqual(q21.hash, q22.hash); - XCTAssertNotEqual(q21.hash, q31.hash); - XCTAssertNotEqual(q21.hash, q41.hash); - XCTAssertNotEqual(q21.hash, q51.hash); - XCTAssertNotEqual(q21.hash, q61.hash); - - XCTAssertEqual(q31.hash, q32.hash); - XCTAssertNotEqual(q31.hash, q41.hash); - XCTAssertNotEqual(q31.hash, q51.hash); - XCTAssertNotEqual(q31.hash, q61.hash); - - XCTAssertEqual(q41.hash, q42.hash); - XCTAssertNotEqual(q41.hash, q43Diff.hash); - XCTAssertNotEqual(q41.hash, q51.hash); - XCTAssertNotEqual(q41.hash, q61.hash); - - XCTAssertEqual(q51.hash, q52.hash); - XCTAssertNotEqual(q51.hash, q53Diff.hash); - XCTAssertNotEqual(q51.hash, q61.hash); -} - -- (void)testImplicitOrderBy { - FSTQuery *baseQuery = FSTTestQuery(@"foo"); - // Default is ascending - XCTAssertEqualObjects(baseQuery.sortOrders, @[ FSTTestOrderBy(kDocumentKeyPath, @"asc") ]); - - // Explicit key ordering is respected - XCTAssertEqualObjects( - [baseQuery queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"asc")].sortOrders, - @[ FSTTestOrderBy(kDocumentKeyPath, @"asc") ]); - XCTAssertEqualObjects( - [baseQuery queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"desc")].sortOrders, - @[ FSTTestOrderBy(kDocumentKeyPath, @"desc") ]); - - XCTAssertEqualObjects( - [[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"asc")] - queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"asc")] - .sortOrders, - (@[ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(kDocumentKeyPath, @"asc") ])); - - XCTAssertEqualObjects( - [[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"asc")] - queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"desc")] - .sortOrders, - (@[ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(kDocumentKeyPath, @"desc") ])); - - // Inequality filters add order bys - XCTAssertEqualObjects( - [baseQuery queryByAddingFilter:FSTTestFilter(@"foo", @"<", @5)].sortOrders, - (@[ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(kDocumentKeyPath, @"asc") ])); - - // Descending order by applies to implicit key ordering - XCTAssertEqualObjects( - [baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"desc")].sortOrders, - (@[ FSTTestOrderBy(@"foo", @"desc"), FSTTestOrderBy(kDocumentKeyPath, @"desc") ])); - XCTAssertEqualObjects([[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"asc")] - queryByAddingSortOrder:FSTTestOrderBy(@"bar", @"desc")] - .sortOrders, - (@[ - FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(@"bar", @"desc"), - FSTTestOrderBy(kDocumentKeyPath, @"desc") - ])); - XCTAssertEqualObjects([[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"desc")] - queryByAddingSortOrder:FSTTestOrderBy(@"bar", @"asc")] - .sortOrders, - (@[ - FSTTestOrderBy(@"foo", @"desc"), FSTTestOrderBy(@"bar", @"asc"), - FSTTestOrderBy(kDocumentKeyPath, @"asc") - ])); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTQueryTests.mm b/Firestore/Example/Tests/Core/FSTQueryTests.mm new file mode 100644 index 0000000..3d2bd82 --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTQueryTests.mm @@ -0,0 +1,566 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Core/FSTQuery.h" + +#import + +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/Model/FSTDatabaseID.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTPath.h" + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +/** Convenience methods for building test queries. */ +@interface FSTQuery (Tests) +- (FSTQuery *)queryByAddingSortBy:(NSString *)key ascending:(BOOL)ascending; +@end + +@implementation FSTQuery (Tests) + +- (FSTQuery *)queryByAddingSortBy:(NSString *)key ascending:(BOOL)ascending { + return [self queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(key) + ascending:ascending]]; +} + +@end + +@interface FSTQueryTests : XCTestCase +@end + +@implementation FSTQueryTests + +- (void)testConstructor { + FSTResourcePath *path = + [FSTResourcePath pathWithSegments:@[ @"rooms", @"Firestore", @"messages", @"0001" ]]; + FSTQuery *query = [FSTQuery queryWithPath:path]; + XCTAssertNotNil(query); + + XCTAssertEqual(query.sortOrders.count, 1); + XCTAssertEqualObjects(query.sortOrders[0].field.canonicalString, kDocumentKeyPath); + XCTAssertEqual(query.sortOrders[0].ascending, YES); + + XCTAssertEqual(query.explicitSortOrders.count, 0); +} + +- (void)testOrderBy { + FSTQuery *query = FSTTestQuery(@"rooms/Firestore/messages"); + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"length") + ascending:NO]]; + + XCTAssertEqual(query.sortOrders.count, 2); + XCTAssertEqualObjects(query.sortOrders[0].field.canonicalString, @"length"); + XCTAssertEqual(query.sortOrders[0].ascending, NO); + XCTAssertEqualObjects(query.sortOrders[1].field.canonicalString, kDocumentKeyPath); + XCTAssertEqual(query.sortOrders[1].ascending, NO); + + XCTAssertEqual(query.explicitSortOrders.count, 1); + XCTAssertEqualObjects(query.explicitSortOrders[0].field.canonicalString, @"length"); + XCTAssertEqual(query.explicitSortOrders[0].ascending, NO); +} + +- (void)testMatchesBasedOnDocumentKey { + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO); + + // document query + FSTQuery *query = FSTTestQuery(@"rooms/eros/messages/1"); + XCTAssertTrue([query matchesDocument:doc1]); + XCTAssertFalse([query matchesDocument:doc2]); + XCTAssertFalse([query matchesDocument:doc3]); +} + +- (void)testMatchesCorrectlyForShallowAncestorQuery { + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc1Meta = FSTTestDoc(@"rooms/eros/messages/1/meta/1", 0, @{@"meta" : @"mv"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO); + + // shallow ancestor query + FSTQuery *query = FSTTestQuery(@"rooms/eros/messages"); + XCTAssertTrue([query matchesDocument:doc1]); + XCTAssertFalse([query matchesDocument:doc1Meta]); + XCTAssertTrue([query matchesDocument:doc2]); + XCTAssertFalse([query matchesDocument:doc3]); +} + +- (void)testEmptyFieldsAreAllowedForQueries { + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); + + FSTQuery *query = [FSTTestQuery(@"rooms/eros/messages") + queryByAddingFilter:FSTTestFilter(@"text", @"==", @"msg1")]; + XCTAssertTrue([query matchesDocument:doc1]); + XCTAssertFalse([query matchesDocument:doc2]); +} + +- (void)testMatchesPrimitiveValuesForFilters { + FSTQuery *query1 = + [FSTTestQuery(@"collection") queryByAddingFilter:FSTTestFilter(@"sort", @">=", @(2))]; + FSTQuery *query2 = + [FSTTestQuery(@"collection") queryByAddingFilter:FSTTestFilter(@"sort", @"<=", @(2))]; + + FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"collection/3", 0, @{ @"sort" : @3 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @NO }, NO); + FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{@"sort" : @"string"}, NO); + FSTDocument *doc6 = FSTTestDoc(@"collection/6", 0, @{}, NO); + + XCTAssertFalse([query1 matchesDocument:doc1]); + XCTAssertTrue([query1 matchesDocument:doc2]); + XCTAssertTrue([query1 matchesDocument:doc3]); + XCTAssertFalse([query1 matchesDocument:doc4]); + XCTAssertFalse([query1 matchesDocument:doc5]); + XCTAssertFalse([query1 matchesDocument:doc6]); + + XCTAssertTrue([query2 matchesDocument:doc1]); + XCTAssertTrue([query2 matchesDocument:doc2]); + XCTAssertFalse([query2 matchesDocument:doc3]); + XCTAssertFalse([query2 matchesDocument:doc4]); + XCTAssertFalse([query2 matchesDocument:doc5]); + XCTAssertFalse([query2 matchesDocument:doc6]); +} + +- (void)testNullFilter { + FSTQuery *query = [FSTTestQuery(@"collection") + queryByAddingFilter:FSTTestFilter(@"sort", @"==", [NSNull null])]; + FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{@"sort" : [NSNull null]}, NO); + FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @3.1 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @NO }, NO); + FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{@"sort" : @"string"}, NO); + + XCTAssertTrue([query matchesDocument:doc1]); + XCTAssertFalse([query matchesDocument:doc2]); + XCTAssertFalse([query matchesDocument:doc3]); + XCTAssertFalse([query matchesDocument:doc4]); + XCTAssertFalse([query matchesDocument:doc5]); +} + +- (void)testNanFilter { + FSTQuery *query = + [FSTTestQuery(@"collection") queryByAddingFilter:FSTTestFilter(@"sort", @"==", @(NAN))]; + FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @(NAN) }, NO); + FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @3.1 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @NO }, NO); + FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{@"sort" : @"string"}, NO); + + XCTAssertTrue([query matchesDocument:doc1]); + XCTAssertFalse([query matchesDocument:doc2]); + XCTAssertFalse([query matchesDocument:doc3]); + XCTAssertFalse([query matchesDocument:doc4]); + XCTAssertFalse([query matchesDocument:doc5]); +} + +- (void)testDoesNotMatchComplexObjectsForFilters { + FSTQuery *query1 = + [FSTTestQuery(@"collection") queryByAddingFilter:FSTTestFilter(@"sort", @"<=", @(2))]; + FSTQuery *query2 = + [FSTTestQuery(@"collection") queryByAddingFilter:FSTTestFilter(@"sort", @">=", @(2))]; + + FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @[] }, NO); + FSTDocument *doc3 = FSTTestDoc(@"collection/3", 0, @{ @"sort" : @[ @1 ] }, NO); + FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @{@"foo" : @2} }, NO); + FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{ @"sort" : @{@"foo" : @"bar"} }, NO); + FSTDocument *doc6 = FSTTestDoc(@"collection/6", 0, @{ @"sort" : @{} }, NO); // no sort field + FSTDocument *doc7 = FSTTestDoc(@"collection/7", 0, @{ @"sort" : @[ @3, @1 ] }, NO); + + XCTAssertTrue([query1 matchesDocument:doc1]); + XCTAssertFalse([query1 matchesDocument:doc2]); + XCTAssertFalse([query1 matchesDocument:doc3]); + XCTAssertFalse([query1 matchesDocument:doc4]); + XCTAssertFalse([query1 matchesDocument:doc5]); + XCTAssertFalse([query1 matchesDocument:doc6]); + XCTAssertFalse([query1 matchesDocument:doc7]); + + XCTAssertTrue([query2 matchesDocument:doc1]); + XCTAssertFalse([query2 matchesDocument:doc2]); + XCTAssertFalse([query2 matchesDocument:doc3]); + XCTAssertFalse([query2 matchesDocument:doc4]); + XCTAssertFalse([query2 matchesDocument:doc5]); + XCTAssertFalse([query2 matchesDocument:doc6]); + XCTAssertFalse([query2 matchesDocument:doc7]); +} + +- (void)testDoesntRemoveComplexObjectsWithOrderBy { + FSTQuery *query1 = [FSTTestQuery(@"collection") + queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort") + ascending:YES]]; + + FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @[] }, NO); + FSTDocument *doc3 = FSTTestDoc(@"collection/3", 0, @{ @"sort" : @[ @1 ] }, NO); + FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @{@"foo" : @2} }, NO); + FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{ @"sort" : @{@"foo" : @"bar"} }, NO); + FSTDocument *doc6 = FSTTestDoc(@"collection/6", 0, @{}, NO); + + XCTAssertTrue([query1 matchesDocument:doc1]); + XCTAssertTrue([query1 matchesDocument:doc2]); + XCTAssertTrue([query1 matchesDocument:doc3]); + XCTAssertTrue([query1 matchesDocument:doc4]); + XCTAssertTrue([query1 matchesDocument:doc5]); + XCTAssertFalse([query1 matchesDocument:doc6]); +} + +- (void)testFiltersBasedOnArrayValue { + FSTQuery *baseQuery = FSTTestQuery(@"collection"); + FSTDocument *doc1 = FSTTestDoc(@"collection/doc", 0, @{ @"tags" : @[ @"foo", @1, @YES ] }, NO); + + NSArray> *matchingFilters = + @[ FSTTestFilter(@"tags", @"==", @[ @"foo", @1, @YES ]) ]; + + NSArray> *nonMatchingFilters = @[ + FSTTestFilter(@"tags", @"==", @"foo"), + FSTTestFilter(@"tags", @"==", @[ @"foo", @1 ]), + FSTTestFilter(@"tags", @"==", @[ @"foo", @YES, @1 ]), + ]; + + for (id filter in matchingFilters) { + XCTAssertTrue([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]); + } + + for (id filter in nonMatchingFilters) { + XCTAssertFalse([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]); + } +} + +- (void)testFiltersBasedOnObjectValue { + FSTQuery *baseQuery = FSTTestQuery(@"collection"); + FSTDocument *doc1 = + FSTTestDoc(@"collection/doc", 0, + @{ @"tags" : @{@"foo" : @"foo", @"a" : @0, @"b" : @YES, @"c" : @(NAN)} }, NO); + + NSArray> *matchingFilters = @[ + FSTTestFilter(@"tags", @"==", + @{ @"foo" : @"foo", + @"a" : @0, + @"b" : @YES, + @"c" : @(NAN) }), + FSTTestFilter(@"tags", @"==", + @{ @"b" : @YES, + @"a" : @0, + @"foo" : @"foo", + @"c" : @(NAN) }), + FSTTestFilter(@"tags.foo", @"==", @"foo") + ]; + + NSArray> *nonMatchingFilters = @[ + FSTTestFilter(@"tags", @"==", @"foo"), FSTTestFilter(@"tags", @"==", @{ + @"foo" : @"foo", + @"a" : @0, + @"b" : @YES, + }) + ]; + + for (id filter in matchingFilters) { + XCTAssertTrue([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]); + } + + for (id filter in nonMatchingFilters) { + XCTAssertFalse([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]); + } +} + +/** + * Checks that an ordered array of elements yields the correct pair-wise comparison result for the + * supplied comparator. + */ +- (void)assertCorrectComparisonsWithArray:(NSArray *)array comparator:(NSComparator)comp { + [array enumerateObjectsUsingBlock:^(id iObj, NSUInteger i, BOOL *outerStop) { + [array enumerateObjectsUsingBlock:^(id _Nonnull jObj, NSUInteger j, BOOL *innerStop) { + NSComparisonResult expected = [@(i) compare:@(j)]; + NSComparisonResult actual = comp(iObj, jObj); + XCTAssertEqual(actual, expected, @"Compared %@ to %@ at (%lu, %lu).", iObj, jObj, + (unsigned long)i, (unsigned long)j); + }]; + }]; +} + +- (void)testSortsDocumentsInTheCorrectOrder { + FSTQuery *query = FSTTestQuery(@"collection"); + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort") + ascending:YES]]; + + // clang-format off + NSArray *docs = @[ + FSTTestDoc(@"collection/1", 0, @{@"sort": [NSNull null]}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @NO}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @YES}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @1}, NO), + FSTTestDoc(@"collection/2", 0, @{@"sort": @1}, NO), // by key + FSTTestDoc(@"collection/3", 0, @{@"sort": @1}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort": @1.9}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @2}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @2.1}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @""}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @"a"}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @"ab"}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @"b"}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": + FSTTestRef(@"project", kDefaultDatabaseID, @"collection/id1")}, NO), + ]; + // clang-format on + + [self assertCorrectComparisonsWithArray:docs comparator:query.comparator]; +} + +- (void)testSortsDocumentsUsingMultipleFields { + FSTQuery *query = FSTTestQuery(@"collection"); + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort1") + ascending:YES]]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort2") + ascending:YES]]; + + // clang-format off + NSArray *docs = + @[FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @1}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @2}, NO), + FSTTestDoc(@"collection/2", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/3", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @3}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @1}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @2}, NO), + FSTTestDoc(@"collection/2", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/3", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @3}, NO), + ]; + // clang-format on + + [self assertCorrectComparisonsWithArray:docs comparator:query.comparator]; +} + +- (void)testSortsDocumentsWithDescendingToo { + FSTQuery *query = FSTTestQuery(@"collection"); + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort1") + ascending:NO]]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort2") + ascending:NO]]; + + // clang-format off + NSArray *docs = + @[FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @3}, NO), + FSTTestDoc(@"collection/3", 0, @{@"sort1": @2, @"sort2": @2}, NO), + FSTTestDoc(@"collection/2", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @1}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @3}, NO), + FSTTestDoc(@"collection/3", 0, @{@"sort1": @1, @"sort2": @2}, NO), + FSTTestDoc(@"collection/2", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @1}, NO), + ]; + // clang-format on + + [self assertCorrectComparisonsWithArray:docs comparator:query.comparator]; +} + +- (void)testEquality { + FSTQuery *q11 = FSTTestQuery(@"foo"); + q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; + q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; + FSTQuery *q12 = FSTTestQuery(@"foo"); + q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; + q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; + + FSTQuery *q21 = FSTTestQuery(@"foo"); + FSTQuery *q22 = FSTTestQuery(@"foo"); + + FSTQuery *q31 = FSTTestQuery(@"foo/bar"); + FSTQuery *q32 = FSTTestQuery(@"foo/bar"); + + FSTQuery *q41 = FSTTestQuery(@"foo"); + q41 = [q41 queryByAddingSortBy:@"foo" ascending:YES]; + q41 = [q41 queryByAddingSortBy:@"bar" ascending:YES]; + FSTQuery *q42 = FSTTestQuery(@"foo"); + q42 = [q42 queryByAddingSortBy:@"foo" ascending:YES]; + q42 = [q42 queryByAddingSortBy:@"bar" ascending:YES]; + FSTQuery *q43Diff = FSTTestQuery(@"foo"); + q43Diff = [q43Diff queryByAddingSortBy:@"bar" ascending:YES]; + q43Diff = [q43Diff queryByAddingSortBy:@"foo" ascending:YES]; + + FSTQuery *q51 = FSTTestQuery(@"foo"); + q51 = [q51 queryByAddingSortBy:@"foo" ascending:YES]; + q51 = [q51 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; + FSTQuery *q52 = FSTTestQuery(@"foo"); + q52 = [q52 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; + q52 = [q52 queryByAddingSortBy:@"foo" ascending:YES]; + FSTQuery *q53Diff = FSTTestQuery(@"foo"); + q53Diff = [q53Diff queryByAddingFilter:FSTTestFilter(@"bar", @">", @(2))]; + q53Diff = [q53Diff queryByAddingSortBy:@"bar" ascending:YES]; + + FSTQuery *q61 = FSTTestQuery(@"foo"); + q61 = [q61 queryBySettingLimit:10]; + + // XCTAssertEqualObjects(q11, q12); // TODO(klimt): not canonical yet + XCTAssertNotEqualObjects(q11, q21); + XCTAssertNotEqualObjects(q11, q31); + XCTAssertNotEqualObjects(q11, q41); + XCTAssertNotEqualObjects(q11, q51); + XCTAssertNotEqualObjects(q11, q61); + + XCTAssertEqualObjects(q21, q22); + XCTAssertNotEqualObjects(q21, q31); + XCTAssertNotEqualObjects(q21, q41); + XCTAssertNotEqualObjects(q21, q51); + XCTAssertNotEqualObjects(q21, q61); + + XCTAssertEqualObjects(q31, q32); + XCTAssertNotEqualObjects(q31, q41); + XCTAssertNotEqualObjects(q31, q51); + XCTAssertNotEqualObjects(q31, q61); + + XCTAssertEqualObjects(q41, q42); + XCTAssertNotEqualObjects(q41, q43Diff); + XCTAssertNotEqualObjects(q41, q51); + XCTAssertNotEqualObjects(q41, q61); + + XCTAssertEqualObjects(q51, q52); + XCTAssertNotEqualObjects(q51, q53Diff); + XCTAssertNotEqualObjects(q51, q61); +} + +- (void)testUniqueIds { + FSTQuery *q11 = FSTTestQuery(@"foo"); + q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; + q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; + FSTQuery *q12 = FSTTestQuery(@"foo"); + q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; + q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; + + FSTQuery *q21 = FSTTestQuery(@"foo"); + FSTQuery *q22 = FSTTestQuery(@"foo"); + + FSTQuery *q31 = FSTTestQuery(@"foo/bar"); + FSTQuery *q32 = FSTTestQuery(@"foo/bar"); + + FSTQuery *q41 = FSTTestQuery(@"foo"); + q41 = [q41 queryByAddingSortBy:@"foo" ascending:YES]; + q41 = [q41 queryByAddingSortBy:@"bar" ascending:YES]; + FSTQuery *q42 = FSTTestQuery(@"foo"); + q42 = [q42 queryByAddingSortBy:@"foo" ascending:YES]; + q42 = [q42 queryByAddingSortBy:@"bar" ascending:YES]; + FSTQuery *q43Diff = FSTTestQuery(@"foo"); + q43Diff = [q43Diff queryByAddingSortBy:@"bar" ascending:YES]; + q43Diff = [q43Diff queryByAddingSortBy:@"foo" ascending:YES]; + + FSTQuery *q51 = FSTTestQuery(@"foo"); + q51 = [q51 queryByAddingSortBy:@"foo" ascending:YES]; + q51 = [q51 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; + FSTQuery *q52 = FSTTestQuery(@"foo"); + q52 = [q52 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; + q52 = [q52 queryByAddingSortBy:@"foo" ascending:YES]; + FSTQuery *q53Diff = FSTTestQuery(@"foo"); + q53Diff = [q53Diff queryByAddingFilter:FSTTestFilter(@"bar", @">", @(2))]; + q53Diff = [q53Diff queryByAddingSortBy:@"bar" ascending:YES]; + + FSTQuery *q61 = FSTTestQuery(@"foo"); + q61 = [q61 queryBySettingLimit:10]; + + // XCTAssertEqual(q11.hash, q12.hash); // TODO(klimt): not canonical yet + XCTAssertNotEqual(q11.hash, q21.hash); + XCTAssertNotEqual(q11.hash, q31.hash); + XCTAssertNotEqual(q11.hash, q41.hash); + XCTAssertNotEqual(q11.hash, q51.hash); + XCTAssertNotEqual(q11.hash, q61.hash); + + XCTAssertEqual(q21.hash, q22.hash); + XCTAssertNotEqual(q21.hash, q31.hash); + XCTAssertNotEqual(q21.hash, q41.hash); + XCTAssertNotEqual(q21.hash, q51.hash); + XCTAssertNotEqual(q21.hash, q61.hash); + + XCTAssertEqual(q31.hash, q32.hash); + XCTAssertNotEqual(q31.hash, q41.hash); + XCTAssertNotEqual(q31.hash, q51.hash); + XCTAssertNotEqual(q31.hash, q61.hash); + + XCTAssertEqual(q41.hash, q42.hash); + XCTAssertNotEqual(q41.hash, q43Diff.hash); + XCTAssertNotEqual(q41.hash, q51.hash); + XCTAssertNotEqual(q41.hash, q61.hash); + + XCTAssertEqual(q51.hash, q52.hash); + XCTAssertNotEqual(q51.hash, q53Diff.hash); + XCTAssertNotEqual(q51.hash, q61.hash); +} + +- (void)testImplicitOrderBy { + FSTQuery *baseQuery = FSTTestQuery(@"foo"); + // Default is ascending + XCTAssertEqualObjects(baseQuery.sortOrders, @[ FSTTestOrderBy(kDocumentKeyPath, @"asc") ]); + + // Explicit key ordering is respected + XCTAssertEqualObjects( + [baseQuery queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"asc")].sortOrders, + @[ FSTTestOrderBy(kDocumentKeyPath, @"asc") ]); + XCTAssertEqualObjects( + [baseQuery queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"desc")].sortOrders, + @[ FSTTestOrderBy(kDocumentKeyPath, @"desc") ]); + + XCTAssertEqualObjects( + [[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"asc")] + queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"asc")] + .sortOrders, + (@[ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(kDocumentKeyPath, @"asc") ])); + + XCTAssertEqualObjects( + [[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"asc")] + queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"desc")] + .sortOrders, + (@[ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(kDocumentKeyPath, @"desc") ])); + + // Inequality filters add order bys + XCTAssertEqualObjects( + [baseQuery queryByAddingFilter:FSTTestFilter(@"foo", @"<", @5)].sortOrders, + (@[ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(kDocumentKeyPath, @"asc") ])); + + // Descending order by applies to implicit key ordering + XCTAssertEqualObjects( + [baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"desc")].sortOrders, + (@[ FSTTestOrderBy(@"foo", @"desc"), FSTTestOrderBy(kDocumentKeyPath, @"desc") ])); + XCTAssertEqualObjects([[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"asc")] + queryByAddingSortOrder:FSTTestOrderBy(@"bar", @"desc")] + .sortOrders, + (@[ + FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(@"bar", @"desc"), + FSTTestOrderBy(kDocumentKeyPath, @"desc") + ])); + XCTAssertEqualObjects([[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"desc")] + queryByAddingSortOrder:FSTTestOrderBy(@"bar", @"asc")] + .sortOrders, + (@[ + FSTTestOrderBy(@"foo", @"desc"), FSTTestOrderBy(@"bar", @"asc"), + FSTTestOrderBy(kDocumentKeyPath, @"asc") + ])); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTTimestampTests.m b/Firestore/Example/Tests/Core/FSTTimestampTests.m deleted file mode 100644 index a3765fe..0000000 --- a/Firestore/Example/Tests/Core/FSTTimestampTests.m +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Core/FSTTimestamp.h" - -#import - -#import "Firestore/Source/Util/FSTAssert.h" - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTTimestampTests : XCTestCase -@end - -@implementation FSTTimestampTests - -- (void)testFromDate { - // Very carefully construct an NSDate that won't lose precision with its milliseconds. - NSDate *input = [NSDate dateWithTimeIntervalSinceReferenceDate:1.5]; - - FSTTimestamp *actual = [FSTTimestamp timestampWithDate:input]; - static const int64_t kSecondsFromEpochToReferenceDate = 978307200; - XCTAssertEqual(kSecondsFromEpochToReferenceDate + 1, actual.seconds); - XCTAssertEqual(500000000, actual.nanos); - - FSTTimestamp *expected = - [[FSTTimestamp alloc] initWithSeconds:(kSecondsFromEpochToReferenceDate + 1) nanos:500000000]; - XCTAssertEqualObjects(expected, actual); -} - -- (void)testSO8601String { - NSDate *date = FSTTestDate(1912, 4, 14, 23, 40, 0); - FSTTimestamp *timestamp = - [[FSTTimestamp alloc] initWithSeconds:(int64_t)date.timeIntervalSince1970 nanos:543000000]; - XCTAssertEqualObjects(timestamp.ISO8601String, @"1912-04-14T23:40:00.543000000Z"); -} - -- (void)testISO8601String_withLowMilliseconds { - NSDate *date = FSTTestDate(1912, 4, 14, 23, 40, 0); - FSTTimestamp *timestamp = - [[FSTTimestamp alloc] initWithSeconds:(int64_t)date.timeIntervalSince1970 nanos:7000000]; - XCTAssertEqualObjects(timestamp.ISO8601String, @"1912-04-14T23:40:00.007000000Z"); -} - -- (void)testISO8601String_withLowNanos { - FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:0 nanos:1]; - XCTAssertEqualObjects(timestamp.ISO8601String, @"1970-01-01T00:00:00.000000001Z"); -} - -- (void)testISO8601String_withNegativeSeconds { - FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:-1 nanos:999999999]; - XCTAssertEqualObjects(timestamp.ISO8601String, @"1969-12-31T23:59:59.999999999Z"); -} - -- (void)testCompare { - NSArray *timestamps = @[ - [[FSTTimestamp alloc] initWithSeconds:12344 nanos:999999999], - [[FSTTimestamp alloc] initWithSeconds:12345 nanos:0], - [[FSTTimestamp alloc] initWithSeconds:12345 nanos:000000001], - [[FSTTimestamp alloc] initWithSeconds:12345 nanos:99999999], - [[FSTTimestamp alloc] initWithSeconds:12345 nanos:100000000], - [[FSTTimestamp alloc] initWithSeconds:12345 nanos:100000001], - [[FSTTimestamp alloc] initWithSeconds:12346 nanos:0], - ]; - for (int i = 0; i < timestamps.count - 1; ++i) { - XCTAssertEqual(NSOrderedAscending, [timestamps[i] compare:timestamps[i + 1]]); - XCTAssertEqual(NSOrderedDescending, [timestamps[i + 1] compare:timestamps[i]]); - } -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTTimestampTests.mm b/Firestore/Example/Tests/Core/FSTTimestampTests.mm new file mode 100644 index 0000000..a3765fe --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTTimestampTests.mm @@ -0,0 +1,88 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Core/FSTTimestamp.h" + +#import + +#import "Firestore/Source/Util/FSTAssert.h" + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTTimestampTests : XCTestCase +@end + +@implementation FSTTimestampTests + +- (void)testFromDate { + // Very carefully construct an NSDate that won't lose precision with its milliseconds. + NSDate *input = [NSDate dateWithTimeIntervalSinceReferenceDate:1.5]; + + FSTTimestamp *actual = [FSTTimestamp timestampWithDate:input]; + static const int64_t kSecondsFromEpochToReferenceDate = 978307200; + XCTAssertEqual(kSecondsFromEpochToReferenceDate + 1, actual.seconds); + XCTAssertEqual(500000000, actual.nanos); + + FSTTimestamp *expected = + [[FSTTimestamp alloc] initWithSeconds:(kSecondsFromEpochToReferenceDate + 1) nanos:500000000]; + XCTAssertEqualObjects(expected, actual); +} + +- (void)testSO8601String { + NSDate *date = FSTTestDate(1912, 4, 14, 23, 40, 0); + FSTTimestamp *timestamp = + [[FSTTimestamp alloc] initWithSeconds:(int64_t)date.timeIntervalSince1970 nanos:543000000]; + XCTAssertEqualObjects(timestamp.ISO8601String, @"1912-04-14T23:40:00.543000000Z"); +} + +- (void)testISO8601String_withLowMilliseconds { + NSDate *date = FSTTestDate(1912, 4, 14, 23, 40, 0); + FSTTimestamp *timestamp = + [[FSTTimestamp alloc] initWithSeconds:(int64_t)date.timeIntervalSince1970 nanos:7000000]; + XCTAssertEqualObjects(timestamp.ISO8601String, @"1912-04-14T23:40:00.007000000Z"); +} + +- (void)testISO8601String_withLowNanos { + FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:0 nanos:1]; + XCTAssertEqualObjects(timestamp.ISO8601String, @"1970-01-01T00:00:00.000000001Z"); +} + +- (void)testISO8601String_withNegativeSeconds { + FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:-1 nanos:999999999]; + XCTAssertEqualObjects(timestamp.ISO8601String, @"1969-12-31T23:59:59.999999999Z"); +} + +- (void)testCompare { + NSArray *timestamps = @[ + [[FSTTimestamp alloc] initWithSeconds:12344 nanos:999999999], + [[FSTTimestamp alloc] initWithSeconds:12345 nanos:0], + [[FSTTimestamp alloc] initWithSeconds:12345 nanos:000000001], + [[FSTTimestamp alloc] initWithSeconds:12345 nanos:99999999], + [[FSTTimestamp alloc] initWithSeconds:12345 nanos:100000000], + [[FSTTimestamp alloc] initWithSeconds:12345 nanos:100000001], + [[FSTTimestamp alloc] initWithSeconds:12346 nanos:0], + ]; + for (int i = 0; i < timestamps.count - 1; ++i) { + XCTAssertEqual(NSOrderedAscending, [timestamps[i] compare:timestamps[i + 1]]); + XCTAssertEqual(NSOrderedDescending, [timestamps[i + 1] compare:timestamps[i]]); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTViewSnapshotTest.m b/Firestore/Example/Tests/Core/FSTViewSnapshotTest.m deleted file mode 100644 index fe3e42d..0000000 --- a/Firestore/Example/Tests/Core/FSTViewSnapshotTest.m +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Core/FSTViewSnapshot.h" - -#import - -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" -#import "Firestore/Source/Model/FSTPath.h" - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTViewSnapshotTests : XCTestCase -@end - -@implementation FSTViewSnapshotTests - -- (void)testDocumentChangeConstructor { - FSTDocument *doc = FSTTestDoc(@"a/b", 0, @{}, NO); - FSTDocumentViewChangeType type = FSTDocumentViewChangeTypeModified; - FSTDocumentViewChange *change = [FSTDocumentViewChange changeWithDocument:doc type:type]; - XCTAssertEqual(change.document, doc); - XCTAssertEqual(change.type, type); -} - -- (void)testTrack { - FSTDocumentViewChangeSet *set = [FSTDocumentViewChangeSet changeSet]; - - FSTDocument *docAdded = FSTTestDoc(@"a/1", 0, @{}, NO); - FSTDocument *docRemoved = FSTTestDoc(@"a/2", 0, @{}, NO); - FSTDocument *docModified = FSTTestDoc(@"a/3", 0, @{}, NO); - - FSTDocument *docAddedThenModified = FSTTestDoc(@"b/1", 0, @{}, NO); - FSTDocument *docAddedThenRemoved = FSTTestDoc(@"b/2", 0, @{}, NO); - FSTDocument *docRemovedThenAdded = FSTTestDoc(@"b/3", 0, @{}, NO); - FSTDocument *docModifiedThenRemoved = FSTTestDoc(@"b/4", 0, @{}, NO); - FSTDocument *docModifiedThenModified = FSTTestDoc(@"b/5", 0, @{}, NO); - - [set addChange:[FSTDocumentViewChange changeWithDocument:docAdded - type:FSTDocumentViewChangeTypeAdded]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docRemoved - type:FSTDocumentViewChangeTypeRemoved]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docModified - type:FSTDocumentViewChangeTypeModified]]; - - [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenModified - type:FSTDocumentViewChangeTypeAdded]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenModified - type:FSTDocumentViewChangeTypeModified]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenRemoved - type:FSTDocumentViewChangeTypeAdded]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenRemoved - type:FSTDocumentViewChangeTypeRemoved]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docRemovedThenAdded - type:FSTDocumentViewChangeTypeRemoved]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docRemovedThenAdded - type:FSTDocumentViewChangeTypeAdded]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenRemoved - type:FSTDocumentViewChangeTypeModified]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenRemoved - type:FSTDocumentViewChangeTypeRemoved]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenModified - type:FSTDocumentViewChangeTypeModified]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenModified - type:FSTDocumentViewChangeTypeModified]]; - - NSArray *changes = [set changes]; - XCTAssertEqual(changes.count, 7); - - XCTAssertEqual(changes[0].document, docAdded); - XCTAssertEqual(changes[0].type, FSTDocumentViewChangeTypeAdded); - - XCTAssertEqual(changes[1].document, docRemoved); - XCTAssertEqual(changes[1].type, FSTDocumentViewChangeTypeRemoved); - - XCTAssertEqual(changes[2].document, docModified); - XCTAssertEqual(changes[2].type, FSTDocumentViewChangeTypeModified); - - XCTAssertEqual(changes[3].document, docAddedThenModified); - XCTAssertEqual(changes[3].type, FSTDocumentViewChangeTypeAdded); - - XCTAssertEqual(changes[4].document, docRemovedThenAdded); - XCTAssertEqual(changes[4].type, FSTDocumentViewChangeTypeModified); - - XCTAssertEqual(changes[5].document, docModifiedThenRemoved); - XCTAssertEqual(changes[5].type, FSTDocumentViewChangeTypeRemoved); - - XCTAssertEqual(changes[6].document, docModifiedThenModified); - XCTAssertEqual(changes[6].type, FSTDocumentViewChangeTypeModified); -} - -- (void)testViewSnapshotConstructor { - FSTQuery *query = FSTTestQuery(@"a"); - FSTDocumentSet *documents = [FSTDocumentSet documentSetWithComparator:FSTDocumentComparatorByKey]; - FSTDocumentSet *oldDocuments = documents; - documents = [documents documentSetByAddingDocument:FSTTestDoc(@"c/a", 1, @{}, NO)]; - NSArray *documentChanges = - @[ [FSTDocumentViewChange changeWithDocument:FSTTestDoc(@"c/a", 1, @{}, NO) - type:FSTDocumentViewChangeTypeAdded] ]; - - BOOL fromCache = YES; - BOOL hasPendingWrites = NO; - BOOL syncStateChanged = YES; - - FSTViewSnapshot *snapshot = [[FSTViewSnapshot alloc] initWithQuery:query - documents:documents - oldDocuments:oldDocuments - documentChanges:documentChanges - fromCache:fromCache - hasPendingWrites:hasPendingWrites - syncStateChanged:syncStateChanged]; - - XCTAssertEqual(snapshot.query, query); - XCTAssertEqual(snapshot.documents, documents); - XCTAssertEqual(snapshot.oldDocuments, oldDocuments); - XCTAssertEqual(snapshot.documentChanges, documentChanges); - XCTAssertEqual(snapshot.fromCache, fromCache); - XCTAssertEqual(snapshot.hasPendingWrites, hasPendingWrites); - XCTAssertEqual(snapshot.syncStateChanged, syncStateChanged); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTViewSnapshotTest.mm b/Firestore/Example/Tests/Core/FSTViewSnapshotTest.mm new file mode 100644 index 0000000..fe3e42d --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTViewSnapshotTest.mm @@ -0,0 +1,141 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Core/FSTViewSnapshot.h" + +#import + +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" +#import "Firestore/Source/Model/FSTPath.h" + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTViewSnapshotTests : XCTestCase +@end + +@implementation FSTViewSnapshotTests + +- (void)testDocumentChangeConstructor { + FSTDocument *doc = FSTTestDoc(@"a/b", 0, @{}, NO); + FSTDocumentViewChangeType type = FSTDocumentViewChangeTypeModified; + FSTDocumentViewChange *change = [FSTDocumentViewChange changeWithDocument:doc type:type]; + XCTAssertEqual(change.document, doc); + XCTAssertEqual(change.type, type); +} + +- (void)testTrack { + FSTDocumentViewChangeSet *set = [FSTDocumentViewChangeSet changeSet]; + + FSTDocument *docAdded = FSTTestDoc(@"a/1", 0, @{}, NO); + FSTDocument *docRemoved = FSTTestDoc(@"a/2", 0, @{}, NO); + FSTDocument *docModified = FSTTestDoc(@"a/3", 0, @{}, NO); + + FSTDocument *docAddedThenModified = FSTTestDoc(@"b/1", 0, @{}, NO); + FSTDocument *docAddedThenRemoved = FSTTestDoc(@"b/2", 0, @{}, NO); + FSTDocument *docRemovedThenAdded = FSTTestDoc(@"b/3", 0, @{}, NO); + FSTDocument *docModifiedThenRemoved = FSTTestDoc(@"b/4", 0, @{}, NO); + FSTDocument *docModifiedThenModified = FSTTestDoc(@"b/5", 0, @{}, NO); + + [set addChange:[FSTDocumentViewChange changeWithDocument:docAdded + type:FSTDocumentViewChangeTypeAdded]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docRemoved + type:FSTDocumentViewChangeTypeRemoved]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docModified + type:FSTDocumentViewChangeTypeModified]]; + + [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenModified + type:FSTDocumentViewChangeTypeAdded]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenModified + type:FSTDocumentViewChangeTypeModified]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenRemoved + type:FSTDocumentViewChangeTypeAdded]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenRemoved + type:FSTDocumentViewChangeTypeRemoved]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docRemovedThenAdded + type:FSTDocumentViewChangeTypeRemoved]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docRemovedThenAdded + type:FSTDocumentViewChangeTypeAdded]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenRemoved + type:FSTDocumentViewChangeTypeModified]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenRemoved + type:FSTDocumentViewChangeTypeRemoved]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenModified + type:FSTDocumentViewChangeTypeModified]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenModified + type:FSTDocumentViewChangeTypeModified]]; + + NSArray *changes = [set changes]; + XCTAssertEqual(changes.count, 7); + + XCTAssertEqual(changes[0].document, docAdded); + XCTAssertEqual(changes[0].type, FSTDocumentViewChangeTypeAdded); + + XCTAssertEqual(changes[1].document, docRemoved); + XCTAssertEqual(changes[1].type, FSTDocumentViewChangeTypeRemoved); + + XCTAssertEqual(changes[2].document, docModified); + XCTAssertEqual(changes[2].type, FSTDocumentViewChangeTypeModified); + + XCTAssertEqual(changes[3].document, docAddedThenModified); + XCTAssertEqual(changes[3].type, FSTDocumentViewChangeTypeAdded); + + XCTAssertEqual(changes[4].document, docRemovedThenAdded); + XCTAssertEqual(changes[4].type, FSTDocumentViewChangeTypeModified); + + XCTAssertEqual(changes[5].document, docModifiedThenRemoved); + XCTAssertEqual(changes[5].type, FSTDocumentViewChangeTypeRemoved); + + XCTAssertEqual(changes[6].document, docModifiedThenModified); + XCTAssertEqual(changes[6].type, FSTDocumentViewChangeTypeModified); +} + +- (void)testViewSnapshotConstructor { + FSTQuery *query = FSTTestQuery(@"a"); + FSTDocumentSet *documents = [FSTDocumentSet documentSetWithComparator:FSTDocumentComparatorByKey]; + FSTDocumentSet *oldDocuments = documents; + documents = [documents documentSetByAddingDocument:FSTTestDoc(@"c/a", 1, @{}, NO)]; + NSArray *documentChanges = + @[ [FSTDocumentViewChange changeWithDocument:FSTTestDoc(@"c/a", 1, @{}, NO) + type:FSTDocumentViewChangeTypeAdded] ]; + + BOOL fromCache = YES; + BOOL hasPendingWrites = NO; + BOOL syncStateChanged = YES; + + FSTViewSnapshot *snapshot = [[FSTViewSnapshot alloc] initWithQuery:query + documents:documents + oldDocuments:oldDocuments + documentChanges:documentChanges + fromCache:fromCache + hasPendingWrites:hasPendingWrites + syncStateChanged:syncStateChanged]; + + XCTAssertEqual(snapshot.query, query); + XCTAssertEqual(snapshot.documents, documents); + XCTAssertEqual(snapshot.oldDocuments, oldDocuments); + XCTAssertEqual(snapshot.documentChanges, documentChanges); + XCTAssertEqual(snapshot.fromCache, fromCache); + XCTAssertEqual(snapshot.hasPendingWrites, hasPendingWrites); + XCTAssertEqual(snapshot.syncStateChanged, syncStateChanged); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTViewTests.m b/Firestore/Example/Tests/Core/FSTViewTests.m deleted file mode 100644 index e6c4510..0000000 --- a/Firestore/Example/Tests/Core/FSTViewTests.m +++ /dev/null @@ -1,618 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Core/FSTView.h" - -#import - -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTViewTests : XCTestCase -@end - -@implementation FSTViewTests - -/** Returns a new empty query to use for testing. */ -- (FSTQuery *)queryForMessages { - return [FSTQuery - queryWithPath:[FSTResourcePath pathWithSegments:@[ @"rooms", @"eros", @"messages" ]]]; -} - -- (void)testAddsDocumentsBasedOnQuery { - FSTQuery *query = [self queryForMessages]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); - FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO); - - FSTViewSnapshot *_Nullable snapshot = - FSTTestApplyChanges(view, @[ doc1, doc2, doc3 ], - [FSTTargetChange changeWithDocuments:@[ doc1, doc2, doc3 ] - currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); - - XCTAssertEqual(snapshot.query, query); - - XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc2 ])); - - XCTAssertEqualObjects( - snapshot.documentChanges, (@[ - [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded], - [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded] - ])); - - XCTAssertFalse(snapshot.isFromCache); - XCTAssertFalse(snapshot.hasPendingWrites); - XCTAssertTrue(snapshot.syncStateChanged); -} - -- (void)testRemovesDocuments { - FSTQuery *query = [self queryForMessages]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); - FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{@"text" : @"msg3"}, NO); - - // initial state - FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); - - // delete doc2, add doc3 - FSTViewSnapshot *snapshot = - FSTTestApplyChanges(view, @[ FSTTestDeletedDoc(@"rooms/eros/messages/2", 0), doc3 ], - [FSTTargetChange changeWithDocuments:@[ doc1, doc3 ] - currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); - - XCTAssertEqual(snapshot.query, query); - - XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ])); - - XCTAssertEqualObjects( - snapshot.documentChanges, (@[ - [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeRemoved], - [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded] - ])); - - XCTAssertFalse(snapshot.isFromCache); - XCTAssertTrue(snapshot.syncStateChanged); -} - -- (void)testReturnsNilIfThereAreNoChanges { - FSTQuery *query = [self queryForMessages]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); - - // initial state - FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); - - // reapply same docs, no changes - FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); - XCTAssertNil(snapshot); -} - -- (void)testDoesNotReturnNilForFirstChanges { - FSTQuery *query = [self queryForMessages]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - - FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[], nil); - XCTAssertNotNil(snapshot); -} - -- (void)testFiltersDocumentsBasedOnQueryWithFilter { - FSTQuery *query = [self queryForMessages]; - FSTRelationFilter *filter = - [FSTRelationFilter filterWithField:FSTTestFieldPath(@"sort") - filterOperator:FSTRelationFilterOperatorLessThanOrEqual - value:[FSTDoubleValue doubleValue:2]]; - query = [query queryByAddingFilter:filter]; - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"sort" : @1 }, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"sort" : @2 }, NO); - FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"sort" : @3 }, NO); - FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{}, NO); // no sort, no match - FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/5", 0, @{ @"sort" : @1 }, NO); - - FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4, doc5 ], nil); - - XCTAssertEqual(snapshot.query, query); - - XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc5, doc2 ])); - - XCTAssertEqualObjects( - snapshot.documentChanges, (@[ - [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded], - [FSTDocumentViewChange changeWithDocument:doc5 type:FSTDocumentViewChangeTypeAdded], - [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded] - ])); - - XCTAssertTrue(snapshot.isFromCache); - XCTAssertTrue(snapshot.syncStateChanged); -} - -- (void)testUpdatesDocumentsBasedOnQueryWithFilter { - FSTQuery *query = [self queryForMessages]; - FSTRelationFilter *filter = - [FSTRelationFilter filterWithField:FSTTestFieldPath(@"sort") - filterOperator:FSTRelationFilterOperatorLessThanOrEqual - value:[FSTDoubleValue doubleValue:2]]; - query = [query queryByAddingFilter:filter]; - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"sort" : @1 }, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"sort" : @3 }, NO); - FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"sort" : @2 }, NO); - FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{}, NO); - - FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4 ], nil); - - XCTAssertEqual(snapshot.query, query); - - XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ])); - - FSTDocument *newDoc2 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{ @"sort" : @2 }, NO); - FSTDocument *newDoc3 = FSTTestDoc(@"rooms/eros/messages/3", 1, @{ @"sort" : @3 }, NO); - FSTDocument *newDoc4 = FSTTestDoc(@"rooms/eros/messages/4", 1, @{ @"sort" : @0 }, NO); - - snapshot = FSTTestApplyChanges(view, @[ newDoc2, newDoc3, newDoc4 ], nil); - - XCTAssertEqual(snapshot.query, query); - - XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ newDoc4, doc1, newDoc2 ])); - - XCTAssertEqualObjects( - snapshot.documentChanges, (@[ - [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeRemoved], - [FSTDocumentViewChange changeWithDocument:newDoc4 type:FSTDocumentViewChangeTypeAdded], - [FSTDocumentViewChange changeWithDocument:newDoc2 type:FSTDocumentViewChangeTypeAdded] - ])); - - XCTAssertTrue(snapshot.isFromCache); - XCTAssertFalse(snapshot.syncStateChanged); -} - -- (void)testRemovesDocumentsForQueryWithLimit { - FSTQuery *query = [self queryForMessages]; - query = [query queryBySettingLimit:2]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); - FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{@"text" : @"msg3"}, NO); - - // initial state - FSTTestApplyChanges(view, @[ doc1, doc3 ], nil); - - // add doc2, which should push out doc3 - FSTViewSnapshot *snapshot = - FSTTestApplyChanges(view, @[ doc2 ], - [FSTTargetChange changeWithDocuments:@[ doc1, doc2, doc3 ] - currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); - - XCTAssertEqual(snapshot.query, query); - - XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc2 ])); - - XCTAssertEqualObjects( - snapshot.documentChanges, (@[ - [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeRemoved], - [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded] - ])); - - XCTAssertFalse(snapshot.isFromCache); - XCTAssertTrue(snapshot.syncStateChanged); -} - -- (void)testDoesntReportChangesForDocumentBeyondLimitOfQuery { - FSTQuery *query = [self queryForMessages]; - query = - [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"num") - ascending:YES]]; - query = [query queryBySettingLimit:2]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"num" : @1 }, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"num" : @2 }, NO); - FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"num" : @3 }, NO); - FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"num" : @4 }, NO); - - // initial state - FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); - - // change doc2 to 5, and add doc3 and doc4. - // doc2 will be modified + removed = removed - // doc3 will be added - // doc4 will be added + removed = nothing - doc2 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{ @"num" : @5 }, NO); - FSTViewDocumentChanges *viewDocChanges = - [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2, doc3, doc4 ])]; - XCTAssertTrue(viewDocChanges.needsRefill); - // Verify that all the docs still match. - viewDocChanges = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4 ]) - previousChanges:viewDocChanges]; - FSTViewSnapshot *snapshot = - [view applyChangesToDocuments:viewDocChanges - targetChange:[FSTTargetChange - changeWithDocuments:@[ doc1, doc2, doc3, doc4 ] - currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]] - .snapshot; - - XCTAssertEqual(snapshot.query, query); - - XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ])); - - XCTAssertEqualObjects( - snapshot.documentChanges, (@[ - [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeRemoved], - [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded] - ])); - - XCTAssertFalse(snapshot.isFromCache); - XCTAssertTrue(snapshot.syncStateChanged); -} - -- (void)testKeepsTrackOfLimboDocuments { - FSTQuery *query = [self queryForMessages]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); - FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); - - FSTViewChange *change = [view - applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])]]; - XCTAssertEqualObjects(change.limboChanges, @[]); - - change = - [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])] - targetChange:[FSTTargetChange - changeWithDocuments:@[] - currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]]; - XCTAssertEqualObjects( - change.limboChanges, - @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded key:doc1.key] ]); - - change = [view - applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])] - targetChange:[FSTTargetChange changeWithDocuments:@[ doc1 ] - currentStatusUpdate:FSTCurrentStatusUpdateNone]]; - XCTAssertEqualObjects( - change.limboChanges, - @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved key:doc1.key] ]); - - change = [view - applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])] - targetChange:[FSTTargetChange changeWithDocuments:@[ doc2 ] - currentStatusUpdate:FSTCurrentStatusUpdateNone]]; - XCTAssertEqualObjects(change.limboChanges, @[]); - - change = [view - applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]]; - XCTAssertEqualObjects( - change.limboChanges, - @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded key:doc3.key] ]); - - change = [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ - FSTTestDeletedDoc(@"rooms/eros/messages/2", - 1) - ])]]; // remove - XCTAssertEqualObjects( - change.limboChanges, - @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved key:doc3.key] ]); -} - -- (void)testResumingQueryCreatesNoLimbos { - FSTQuery *query = [self queryForMessages]; - - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); - - // Unlike other cases, here the view is initialized with a set of previously synced documents - // which happens when listening to a previously listened-to query. - FSTView *view = [[FSTView alloc] initWithQuery:query - remoteDocuments:FSTTestDocKeySet(@[ doc1.key, doc2.key ])]; - - FSTTargetChange *markCurrent = - [FSTTargetChange changeWithDocuments:@[] - currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]; - FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[])]; - FSTViewChange *change = [view applyChangesToDocuments:changes targetChange:markCurrent]; - XCTAssertEqualObjects(change.limboChanges, @[]); -} - -- (void)assertDocSet:(FSTDocumentSet *)docSet containsDocs:(NSArray *)docs { - XCTAssertEqual(docs.count, docSet.count); - for (FSTDocument *doc in docs) { - XCTAssertTrue([docSet containsKey:doc.key]); - } -} - -- (void)testReturnsNeedsRefillOnDeleteInLimitQuery { - FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2]; - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(2, [changes.changeSet changes].count); - [view applyChangesToDocuments:changes]; - - // Remove one of the docs. - changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc( - @"rooms/eros/messages/0", 0) ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc2 ]]; - XCTAssertTrue(changes.needsRefill); - XCTAssertEqual(1, [changes.changeSet changes].count); - // Refill it with just the one doc remaining. - changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ]) previousChanges:changes]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc2 ]]; - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(1, [changes.changeSet changes].count); - [view applyChangesToDocuments:changes]; -} - -- (void)testReturnsNeedsRefillOnReorderInLimitQuery { - FSTQuery *query = [self queryForMessages]; - query = - [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order") - ascending:YES]]; - query = [query queryBySettingLimit:2]; - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO); - FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(2, [changes.changeSet changes].count); - [view applyChangesToDocuments:changes]; - - // Move one of the docs. - doc2 = FSTTestDoc(@"rooms/eros/messages/1", 1, @{ @"order" : @2000 }, NO); - changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; - XCTAssertTrue(changes.needsRefill); - XCTAssertEqual(1, [changes.changeSet changes].count); - // Refill it with all three current docs. - changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3 ]) - previousChanges:changes]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc3 ]]; - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(2, [changes.changeSet changes].count); - [view applyChangesToDocuments:changes]; -} - -- (void)testDoesntNeedRefillOnReorderWithinLimit { - FSTQuery *query = [self queryForMessages]; - query = - [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order") - ascending:YES]]; - query = [query queryBySettingLimit:3]; - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO); - FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO); - FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"order" : @4 }, NO); - FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"order" : @5 }, NO); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4, doc5 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]]; - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(3, [changes.changeSet changes].count); - [view applyChangesToDocuments:changes]; - - // Move one of the docs. - doc1 = FSTTestDoc(@"rooms/eros/messages/0", 1, @{ @"order" : @3 }, NO); - changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc2, doc3, doc1 ]]; - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(1, [changes.changeSet changes].count); - [view applyChangesToDocuments:changes]; -} - -- (void)testDoesntNeedRefillOnReorderAfterLimitQuery { - FSTQuery *query = [self queryForMessages]; - query = - [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order") - ascending:YES]]; - query = [query queryBySettingLimit:3]; - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO); - FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO); - FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"order" : @4 }, NO); - FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"order" : @5 }, NO); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4, doc5 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]]; - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(3, [changes.changeSet changes].count); - [view applyChangesToDocuments:changes]; - - // Move one of the docs. - doc4 = FSTTestDoc(@"rooms/eros/messages/3", 1, @{ @"order" : @6 }, NO); - changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc4 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]]; - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(0, [changes.changeSet changes].count); - [view applyChangesToDocuments:changes]; -} - -- (void)testDoesntNeedRefillForAdditionAfterTheLimit { - FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2]; - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(2, [changes.changeSet changes].count); - [view applyChangesToDocuments:changes]; - - // Add a doc that is past the limit. - FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{}, NO); - changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(0, [changes.changeSet changes].count); - [view applyChangesToDocuments:changes]; -} - -- (void)testDoesntNeedRefillForDeletionsWhenNotNearTheLimit { - FSTQuery *query = [[self queryForMessages] queryBySettingLimit:20]; - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(2, [changes.changeSet changes].count); - [view applyChangesToDocuments:changes]; - - // Remove one of the docs. - changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc( - @"rooms/eros/messages/1", 0) ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1 ]]; - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(1, [changes.changeSet changes].count); - [view applyChangesToDocuments:changes]; -} - -- (void)testHandlesApplyingIrrelevantDocs { - FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2]; - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(2, [changes.changeSet changes].count); - [view applyChangesToDocuments:changes]; - - // Remove a doc that isn't even in the results. - changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc( - @"rooms/eros/messages/2", 0) ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(0, [changes.changeSet changes].count); - [view applyChangesToDocuments:changes]; -} - -- (void)testComputesMutatedKeys { - FSTQuery *query = [self queryForMessages]; - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; - [view applyChangesToDocuments:changes]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[])); - - FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, YES); - changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc3.key ])); -} - -- (void)testRemovesKeysFromMutatedKeysWhenNewDocHasNoLocalChanges { - FSTQuery *query = [self queryForMessages]; - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; - [view applyChangesToDocuments:changes]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); - - FSTDocument *doc2Prime = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); - changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2Prime ])]; - [view applyChangesToDocuments:changes]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[])); -} - -- (void)testRemembersLocalMutationsFromPreviousSnapshot { - FSTQuery *query = [self queryForMessages]; - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; - [view applyChangesToDocuments:changes]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); - - FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); - changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]; - [view applyChangesToDocuments:changes]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); -} - -- (void)testRemembersLocalMutationsFromPreviousCallToComputeChangesWithDocuments { - FSTQuery *query = [self queryForMessages]; - FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); - FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); - - FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); - changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ]) previousChanges:changes]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTViewTests.mm b/Firestore/Example/Tests/Core/FSTViewTests.mm new file mode 100644 index 0000000..e6c4510 --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTViewTests.mm @@ -0,0 +1,618 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Core/FSTView.h" + +#import + +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTViewSnapshot.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Remote/FSTRemoteEvent.h" + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTViewTests : XCTestCase +@end + +@implementation FSTViewTests + +/** Returns a new empty query to use for testing. */ +- (FSTQuery *)queryForMessages { + return [FSTQuery + queryWithPath:[FSTResourcePath pathWithSegments:@[ @"rooms", @"eros", @"messages" ]]]; +} + +- (void)testAddsDocumentsBasedOnQuery { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO); + + FSTViewSnapshot *_Nullable snapshot = + FSTTestApplyChanges(view, @[ doc1, doc2, doc3 ], + [FSTTargetChange changeWithDocuments:@[ doc1, doc2, doc3 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc2 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded], + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertFalse(snapshot.isFromCache); + XCTAssertFalse(snapshot.hasPendingWrites); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testRemovesDocuments { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{@"text" : @"msg3"}, NO); + + // initial state + FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + + // delete doc2, add doc3 + FSTViewSnapshot *snapshot = + FSTTestApplyChanges(view, @[ FSTTestDeletedDoc(@"rooms/eros/messages/2", 0), doc3 ], + [FSTTargetChange changeWithDocuments:@[ doc1, doc3 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeRemoved], + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertFalse(snapshot.isFromCache); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testReturnsNilIfThereAreNoChanges { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + + // initial state + FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + + // reapply same docs, no changes + FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + XCTAssertNil(snapshot); +} + +- (void)testDoesNotReturnNilForFirstChanges { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[], nil); + XCTAssertNotNil(snapshot); +} + +- (void)testFiltersDocumentsBasedOnQueryWithFilter { + FSTQuery *query = [self queryForMessages]; + FSTRelationFilter *filter = + [FSTRelationFilter filterWithField:FSTTestFieldPath(@"sort") + filterOperator:FSTRelationFilterOperatorLessThanOrEqual + value:[FSTDoubleValue doubleValue:2]]; + query = [query queryByAddingFilter:filter]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"sort" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"sort" : @3 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{}, NO); // no sort, no match + FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/5", 0, @{ @"sort" : @1 }, NO); + + FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4, doc5 ], nil); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc5, doc2 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded], + [FSTDocumentViewChange changeWithDocument:doc5 type:FSTDocumentViewChangeTypeAdded], + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertTrue(snapshot.isFromCache); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testUpdatesDocumentsBasedOnQueryWithFilter { + FSTQuery *query = [self queryForMessages]; + FSTRelationFilter *filter = + [FSTRelationFilter filterWithField:FSTTestFieldPath(@"sort") + filterOperator:FSTRelationFilterOperatorLessThanOrEqual + value:[FSTDoubleValue doubleValue:2]]; + query = [query queryByAddingFilter:filter]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"sort" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"sort" : @3 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{}, NO); + + FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4 ], nil); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ])); + + FSTDocument *newDoc2 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{ @"sort" : @2 }, NO); + FSTDocument *newDoc3 = FSTTestDoc(@"rooms/eros/messages/3", 1, @{ @"sort" : @3 }, NO); + FSTDocument *newDoc4 = FSTTestDoc(@"rooms/eros/messages/4", 1, @{ @"sort" : @0 }, NO); + + snapshot = FSTTestApplyChanges(view, @[ newDoc2, newDoc3, newDoc4 ], nil); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ newDoc4, doc1, newDoc2 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeRemoved], + [FSTDocumentViewChange changeWithDocument:newDoc4 type:FSTDocumentViewChangeTypeAdded], + [FSTDocumentViewChange changeWithDocument:newDoc2 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertTrue(snapshot.isFromCache); + XCTAssertFalse(snapshot.syncStateChanged); +} + +- (void)testRemovesDocumentsForQueryWithLimit { + FSTQuery *query = [self queryForMessages]; + query = [query queryBySettingLimit:2]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{@"text" : @"msg3"}, NO); + + // initial state + FSTTestApplyChanges(view, @[ doc1, doc3 ], nil); + + // add doc2, which should push out doc3 + FSTViewSnapshot *snapshot = + FSTTestApplyChanges(view, @[ doc2 ], + [FSTTargetChange changeWithDocuments:@[ doc1, doc2, doc3 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc2 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeRemoved], + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertFalse(snapshot.isFromCache); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testDoesntReportChangesForDocumentBeyondLimitOfQuery { + FSTQuery *query = [self queryForMessages]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"num") + ascending:YES]]; + query = [query queryBySettingLimit:2]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"num" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"num" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"num" : @3 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"num" : @4 }, NO); + + // initial state + FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + + // change doc2 to 5, and add doc3 and doc4. + // doc2 will be modified + removed = removed + // doc3 will be added + // doc4 will be added + removed = nothing + doc2 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{ @"num" : @5 }, NO); + FSTViewDocumentChanges *viewDocChanges = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2, doc3, doc4 ])]; + XCTAssertTrue(viewDocChanges.needsRefill); + // Verify that all the docs still match. + viewDocChanges = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4 ]) + previousChanges:viewDocChanges]; + FSTViewSnapshot *snapshot = + [view applyChangesToDocuments:viewDocChanges + targetChange:[FSTTargetChange + changeWithDocuments:@[ doc1, doc2, doc3, doc4 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]] + .snapshot; + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeRemoved], + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertFalse(snapshot.isFromCache); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testKeepsTrackOfLimboDocuments { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); + + FSTViewChange *change = [view + applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])]]; + XCTAssertEqualObjects(change.limboChanges, @[]); + + change = + [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])] + targetChange:[FSTTargetChange + changeWithDocuments:@[] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]]; + XCTAssertEqualObjects( + change.limboChanges, + @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded key:doc1.key] ]); + + change = [view + applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])] + targetChange:[FSTTargetChange changeWithDocuments:@[ doc1 ] + currentStatusUpdate:FSTCurrentStatusUpdateNone]]; + XCTAssertEqualObjects( + change.limboChanges, + @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved key:doc1.key] ]); + + change = [view + applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])] + targetChange:[FSTTargetChange changeWithDocuments:@[ doc2 ] + currentStatusUpdate:FSTCurrentStatusUpdateNone]]; + XCTAssertEqualObjects(change.limboChanges, @[]); + + change = [view + applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]]; + XCTAssertEqualObjects( + change.limboChanges, + @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded key:doc3.key] ]); + + change = [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ + FSTTestDeletedDoc(@"rooms/eros/messages/2", + 1) + ])]]; // remove + XCTAssertEqualObjects( + change.limboChanges, + @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved key:doc3.key] ]); +} + +- (void)testResumingQueryCreatesNoLimbos { + FSTQuery *query = [self queryForMessages]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + + // Unlike other cases, here the view is initialized with a set of previously synced documents + // which happens when listening to a previously listened-to query. + FSTView *view = [[FSTView alloc] initWithQuery:query + remoteDocuments:FSTTestDocKeySet(@[ doc1.key, doc2.key ])]; + + FSTTargetChange *markCurrent = + [FSTTargetChange changeWithDocuments:@[] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]; + FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[])]; + FSTViewChange *change = [view applyChangesToDocuments:changes targetChange:markCurrent]; + XCTAssertEqualObjects(change.limboChanges, @[]); +} + +- (void)assertDocSet:(FSTDocumentSet *)docSet containsDocs:(NSArray *)docs { + XCTAssertEqual(docs.count, docSet.count); + for (FSTDocument *doc in docs) { + XCTAssertTrue([docSet containsKey:doc.key]); + } +} + +- (void)testReturnsNeedsRefillOnDeleteInLimitQuery { + FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Remove one of the docs. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc( + @"rooms/eros/messages/0", 0) ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc2 ]]; + XCTAssertTrue(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + // Refill it with just the one doc remaining. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ]) previousChanges:changes]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testReturnsNeedsRefillOnReorderInLimitQuery { + FSTQuery *query = [self queryForMessages]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order") + ascending:YES]]; + query = [query queryBySettingLimit:2]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Move one of the docs. + doc2 = FSTTestDoc(@"rooms/eros/messages/1", 1, @{ @"order" : @2000 }, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertTrue(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + // Refill it with all three current docs. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3 ]) + previousChanges:changes]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc3 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testDoesntNeedRefillOnReorderWithinLimit { + FSTQuery *query = [self queryForMessages]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order") + ascending:YES]]; + query = [query queryBySettingLimit:3]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"order" : @4 }, NO); + FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"order" : @5 }, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4, doc5 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(3, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Move one of the docs. + doc1 = FSTTestDoc(@"rooms/eros/messages/0", 1, @{ @"order" : @3 }, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc2, doc3, doc1 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testDoesntNeedRefillOnReorderAfterLimitQuery { + FSTQuery *query = [self queryForMessages]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order") + ascending:YES]]; + query = [query queryBySettingLimit:3]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"order" : @4 }, NO); + FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"order" : @5 }, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4, doc5 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(3, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Move one of the docs. + doc4 = FSTTestDoc(@"rooms/eros/messages/3", 1, @{ @"order" : @6 }, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc4 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(0, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testDoesntNeedRefillForAdditionAfterTheLimit { + FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Add a doc that is past the limit. + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{}, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(0, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testDoesntNeedRefillForDeletionsWhenNotNearTheLimit { + FSTQuery *query = [[self queryForMessages] queryBySettingLimit:20]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Remove one of the docs. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc( + @"rooms/eros/messages/1", 0) ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testHandlesApplyingIrrelevantDocs { + FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Remove a doc that isn't even in the results. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc( + @"rooms/eros/messages/2", 0) ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(0, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testComputesMutatedKeys { + FSTQuery *query = [self queryForMessages]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[])); + + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, YES); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc3.key ])); +} + +- (void)testRemovesKeysFromMutatedKeysWhenNewDocHasNoLocalChanges { + FSTQuery *query = [self queryForMessages]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + + FSTDocument *doc2Prime = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2Prime ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[])); +} + +- (void)testRemembersLocalMutationsFromPreviousSnapshot { + FSTQuery *query = [self queryForMessages]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); +} + +- (void)testRemembersLocalMutationsFromPreviousCallToComputeChangesWithDocuments { + FSTQuery *query = [self queryForMessages]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ]) previousChanges:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Integration/API/FIRCursorTests.m b/Firestore/Example/Tests/Integration/API/FIRCursorTests.m deleted file mode 100644 index dc9da83..0000000 --- a/Firestore/Example/Tests/Integration/API/FIRCursorTests.m +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 FirebaseFirestore; - -#import - -#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" - -@interface FIRCursorTests : FSTIntegrationTestCase -@end - -@implementation FIRCursorTests - -- (void)testCanPageThroughItems { - FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ - @"a" : @{@"v" : @"a"}, - @"b" : @{@"v" : @"b"}, - @"c" : @{@"v" : @"c"}, - @"d" : @{@"v" : @"d"}, - @"e" : @{@"v" : @"e"}, - @"f" : @{@"v" : @"f"} - }]; - - FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[testCollection queryLimitedTo:2]]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{@"v" : @"a"}, @{@"v" : @"b"} ])); - - FIRDocumentSnapshot *lastDoc = snapshot.documents.lastObject; - snapshot = [self - readDocumentSetForRef:[[testCollection queryLimitedTo:3] queryStartingAfterDocument:lastDoc]]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), - (@[ @{@"v" : @"c"}, @{@"v" : @"d"}, @{@"v" : @"e"} ])); - - lastDoc = snapshot.documents.lastObject; - snapshot = [self - readDocumentSetForRef:[[testCollection queryLimitedTo:1] queryStartingAfterDocument:lastDoc]]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), @[ @{@"v" : @"f"} ]); - - lastDoc = snapshot.documents.lastObject; - snapshot = [self - readDocumentSetForRef:[[testCollection queryLimitedTo:3] queryStartingAfterDocument:lastDoc]]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), @[]); -} - -- (void)testCanBeCreatedFromDocuments { - FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ - @"a" : @{@"v" : @"a", @"sort" : @1.0}, - @"b" : @{@"v" : @"b", @"sort" : @2.0}, - @"c" : @{@"v" : @"c", @"sort" : @2.0}, - @"d" : @{@"v" : @"d", @"sort" : @2.0}, - @"e" : @{@"v" : @"e", @"sort" : @0.0}, - @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up - }]; - - FIRQuery *query = [testCollection queryOrderedByField:@"sort"]; - FIRDocumentSnapshot *snapshot = [self readDocumentForRef:[testCollection documentWithPath:@"c"]]; - - XCTAssertTrue(snapshot.exists); - FIRQuerySnapshot *querySnapshot = - [self readDocumentSetForRef:[query queryStartingAtDocument:snapshot]]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[ - @{ @"v" : @"c", - @"sort" : @2.0 }, - @{ @"v" : @"d", - @"sort" : @2.0 } - ])); - - querySnapshot = [self readDocumentSetForRef:[query queryEndingBeforeDocument:snapshot]]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[ - @{ @"v" : @"e", - @"sort" : @0.0 }, - @{ @"v" : @"a", - @"sort" : @1.0 }, - @{ @"v" : @"b", - @"sort" : @2.0 } - ])); -} - -- (void)testCanBeCreatedFromValues { - FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ - @"a" : @{@"v" : @"a", @"sort" : @1.0}, - @"b" : @{@"v" : @"b", @"sort" : @2.0}, - @"c" : @{@"v" : @"c", @"sort" : @2.0}, - @"d" : @{@"v" : @"d", @"sort" : @2.0}, - @"e" : @{@"v" : @"e", @"sort" : @0.0}, - @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up - }]; - - FIRQuery *query = [testCollection queryOrderedByField:@"sort"]; - FIRQuerySnapshot *querySnapshot = - [self readDocumentSetForRef:[query queryStartingAtValues:@[ @2.0 ]]]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[ - @{ @"v" : @"b", - @"sort" : @2.0 }, - @{ @"v" : @"c", - @"sort" : @2.0 }, - @{ @"v" : @"d", - @"sort" : @2.0 } - ])); - - querySnapshot = [self readDocumentSetForRef:[query queryEndingBeforeValues:@[ @2.0 ]]]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[ - @{ @"v" : @"e", - @"sort" : @0.0 }, - @{ @"v" : @"a", - @"sort" : @1.0 } - ])); -} - -- (void)testCanBeCreatedUsingDocumentId { - NSDictionary *testDocs = @{ - @"a" : @{@"k" : @"a"}, - @"b" : @{@"k" : @"b"}, - @"c" : @{@"k" : @"c"}, - @"d" : @{@"k" : @"d"}, - @"e" : @{@"k" : @"e"} - }; - FIRCollectionReference *writer = [[[[self firestore] collectionWithPath:@"parent-collection"] - documentWithAutoID] collectionWithPath:@"sub-collection"]; - [self writeAllDocuments:testDocs toCollection:writer]; - - FIRCollectionReference *reader = [[self firestore] collectionWithPath:writer.path]; - FIRQuerySnapshot *querySnapshot = - [self readDocumentSetForRef:[[[reader queryOrderedByFieldPath:[FIRFieldPath documentID]] - queryStartingAtValues:@[ @"b" ]] - queryEndingBeforeValues:@[ @"d" ]]]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), - (@[ @{@"k" : @"b"}, @{@"k" : @"c"} ])); -} - -- (void)testCanBeUsedWithReferenceValues { - FIRFirestore *db = [self firestore]; - - FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ - @"a" : @{@"k" : @"1a", @"ref" : [db documentWithPath:@"1/a"]}, - @"b" : @{@"k" : @"1b", @"ref" : [db documentWithPath:@"1/b"]}, - @"c" : @{@"k" : @"2a", @"ref" : [db documentWithPath:@"2/a"]}, - @"d" : @{@"k" : @"2b", @"ref" : [db documentWithPath:@"2/b"]}, - @"e" : @{@"k" : @"3a", @"ref" : [db documentWithPath:@"3/a"]}, - }]; - FIRQuery *query = [testCollection queryOrderedByField:@"ref"]; - FIRQuerySnapshot *querySnapshot = [self - readDocumentSetForRef:[[query queryStartingAfterValues:@[ [db documentWithPath:@"1/a"] ]] - queryEndingAtValues:@[ [db documentWithPath:@"2/b"] ]]]; - NSMutableArray *actual = [NSMutableArray array]; - [querySnapshot.documents enumerateObjectsUsingBlock:^(FIRDocumentSnapshot *_Nonnull doc, - NSUInteger idx, BOOL *_Nonnull stop) { - [actual addObject:doc.data[@"k"]]; - }]; - XCTAssertEqualObjects(actual, (@[ @"1b", @"2a", @"2b" ])); -} - -- (void)testCanBeUsedInDescendingQueries { - FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ - @"a" : @{@"v" : @"a", @"sort" : @1.0}, - @"b" : @{@"v" : @"b", @"sort" : @2.0}, - @"c" : @{@"v" : @"c", @"sort" : @2.0}, - @"d" : @{@"v" : @"d", @"sort" : @3.0}, - @"e" : @{@"v" : @"e", @"sort" : @0.0}, - @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up - }]; - FIRQuery *query = [[testCollection queryOrderedByField:@"sort" descending:YES] - queryOrderedByFieldPath:[FIRFieldPath documentID] - descending:YES]; - - FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[query queryStartingAtValues:@[ @2.0 ]]]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ - @{ @"v" : @"c", - @"sort" : @2.0 }, - @{ @"v" : @"b", - @"sort" : @2.0 }, - @{ @"v" : @"a", - @"sort" : @1.0 }, - @{ @"v" : @"e", - @"sort" : @0.0 } - ])); - - snapshot = [self readDocumentSetForRef:[query queryEndingBeforeValues:@[ @2.0 ]]]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{ @"v" : @"d", @"sort" : @3.0 } ])); -} - -@end diff --git a/Firestore/Example/Tests/Integration/API/FIRCursorTests.mm b/Firestore/Example/Tests/Integration/API/FIRCursorTests.mm new file mode 100644 index 0000000..3b5c38f --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRCursorTests.mm @@ -0,0 +1,195 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import + +#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" + +@interface FIRCursorTests : FSTIntegrationTestCase +@end + +@implementation FIRCursorTests + +- (void)testCanPageThroughItems { + FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ + @"a" : @{@"v" : @"a"}, + @"b" : @{@"v" : @"b"}, + @"c" : @{@"v" : @"c"}, + @"d" : @{@"v" : @"d"}, + @"e" : @{@"v" : @"e"}, + @"f" : @{@"v" : @"f"} + }]; + + FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[testCollection queryLimitedTo:2]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{@"v" : @"a"}, @{@"v" : @"b"} ])); + + FIRDocumentSnapshot *lastDoc = snapshot.documents.lastObject; + snapshot = [self + readDocumentSetForRef:[[testCollection queryLimitedTo:3] queryStartingAfterDocument:lastDoc]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), + (@[ @{@"v" : @"c"}, @{@"v" : @"d"}, @{@"v" : @"e"} ])); + + lastDoc = snapshot.documents.lastObject; + snapshot = [self + readDocumentSetForRef:[[testCollection queryLimitedTo:1] queryStartingAfterDocument:lastDoc]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), @[ @{@"v" : @"f"} ]); + + lastDoc = snapshot.documents.lastObject; + snapshot = [self + readDocumentSetForRef:[[testCollection queryLimitedTo:3] queryStartingAfterDocument:lastDoc]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), @[]); +} + +- (void)testCanBeCreatedFromDocuments { + FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ + @"a" : @{@"v" : @"a", @"sort" : @1.0}, + @"b" : @{@"v" : @"b", @"sort" : @2.0}, + @"c" : @{@"v" : @"c", @"sort" : @2.0}, + @"d" : @{@"v" : @"d", @"sort" : @2.0}, + @"e" : @{@"v" : @"e", @"sort" : @0.0}, + @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up + }]; + + FIRQuery *query = [testCollection queryOrderedByField:@"sort"]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:[testCollection documentWithPath:@"c"]]; + + XCTAssertTrue(snapshot.exists); + FIRQuerySnapshot *querySnapshot = + [self readDocumentSetForRef:[query queryStartingAtDocument:snapshot]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[ + @{ @"v" : @"c", + @"sort" : @2.0 }, + @{ @"v" : @"d", + @"sort" : @2.0 } + ])); + + querySnapshot = [self readDocumentSetForRef:[query queryEndingBeforeDocument:snapshot]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[ + @{ @"v" : @"e", + @"sort" : @0.0 }, + @{ @"v" : @"a", + @"sort" : @1.0 }, + @{ @"v" : @"b", + @"sort" : @2.0 } + ])); +} + +- (void)testCanBeCreatedFromValues { + FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ + @"a" : @{@"v" : @"a", @"sort" : @1.0}, + @"b" : @{@"v" : @"b", @"sort" : @2.0}, + @"c" : @{@"v" : @"c", @"sort" : @2.0}, + @"d" : @{@"v" : @"d", @"sort" : @2.0}, + @"e" : @{@"v" : @"e", @"sort" : @0.0}, + @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up + }]; + + FIRQuery *query = [testCollection queryOrderedByField:@"sort"]; + FIRQuerySnapshot *querySnapshot = + [self readDocumentSetForRef:[query queryStartingAtValues:@[ @2.0 ]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[ + @{ @"v" : @"b", + @"sort" : @2.0 }, + @{ @"v" : @"c", + @"sort" : @2.0 }, + @{ @"v" : @"d", + @"sort" : @2.0 } + ])); + + querySnapshot = [self readDocumentSetForRef:[query queryEndingBeforeValues:@[ @2.0 ]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[ + @{ @"v" : @"e", + @"sort" : @0.0 }, + @{ @"v" : @"a", + @"sort" : @1.0 } + ])); +} + +- (void)testCanBeCreatedUsingDocumentId { + NSDictionary *testDocs = @{ + @"a" : @{@"k" : @"a"}, + @"b" : @{@"k" : @"b"}, + @"c" : @{@"k" : @"c"}, + @"d" : @{@"k" : @"d"}, + @"e" : @{@"k" : @"e"} + }; + FIRCollectionReference *writer = [[[[self firestore] collectionWithPath:@"parent-collection"] + documentWithAutoID] collectionWithPath:@"sub-collection"]; + [self writeAllDocuments:testDocs toCollection:writer]; + + FIRCollectionReference *reader = [[self firestore] collectionWithPath:writer.path]; + FIRQuerySnapshot *querySnapshot = + [self readDocumentSetForRef:[[[reader queryOrderedByFieldPath:[FIRFieldPath documentID]] + queryStartingAtValues:@[ @"b" ]] + queryEndingBeforeValues:@[ @"d" ]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), + (@[ @{@"k" : @"b"}, @{@"k" : @"c"} ])); +} + +- (void)testCanBeUsedWithReferenceValues { + FIRFirestore *db = [self firestore]; + + FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ + @"a" : @{@"k" : @"1a", @"ref" : [db documentWithPath:@"1/a"]}, + @"b" : @{@"k" : @"1b", @"ref" : [db documentWithPath:@"1/b"]}, + @"c" : @{@"k" : @"2a", @"ref" : [db documentWithPath:@"2/a"]}, + @"d" : @{@"k" : @"2b", @"ref" : [db documentWithPath:@"2/b"]}, + @"e" : @{@"k" : @"3a", @"ref" : [db documentWithPath:@"3/a"]}, + }]; + FIRQuery *query = [testCollection queryOrderedByField:@"ref"]; + FIRQuerySnapshot *querySnapshot = [self + readDocumentSetForRef:[[query queryStartingAfterValues:@[ [db documentWithPath:@"1/a"] ]] + queryEndingAtValues:@[ [db documentWithPath:@"2/b"] ]]]; + NSMutableArray *actual = [NSMutableArray array]; + [querySnapshot.documents enumerateObjectsUsingBlock:^(FIRDocumentSnapshot *_Nonnull doc, + NSUInteger idx, BOOL *_Nonnull stop) { + [actual addObject:doc.data[@"k"]]; + }]; + XCTAssertEqualObjects(actual, (@[ @"1b", @"2a", @"2b" ])); +} + +- (void)testCanBeUsedInDescendingQueries { + FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ + @"a" : @{@"v" : @"a", @"sort" : @1.0}, + @"b" : @{@"v" : @"b", @"sort" : @2.0}, + @"c" : @{@"v" : @"c", @"sort" : @2.0}, + @"d" : @{@"v" : @"d", @"sort" : @3.0}, + @"e" : @{@"v" : @"e", @"sort" : @0.0}, + @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up + }]; + FIRQuery *query = [[testCollection queryOrderedByField:@"sort" descending:YES] + queryOrderedByFieldPath:[FIRFieldPath documentID] + descending:YES]; + + FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[query queryStartingAtValues:@[ @2.0 ]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ + @{ @"v" : @"c", + @"sort" : @2.0 }, + @{ @"v" : @"b", + @"sort" : @2.0 }, + @{ @"v" : @"a", + @"sort" : @1.0 }, + @{ @"v" : @"e", + @"sort" : @0.0 } + ])); + + snapshot = [self readDocumentSetForRef:[query queryEndingBeforeValues:@[ @2.0 ]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{ @"v" : @"d", @"sort" : @3.0 } ])); +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m deleted file mode 100644 index 9a53e29..0000000 --- a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m +++ /dev/null @@ -1,964 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 FirebaseFirestore; - -#import -#import - -#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/Core/FSTFirestoreClient.h" - -@interface FIRDatabaseTests : FSTIntegrationTestCase -@end - -@implementation FIRDatabaseTests - -- (void)testCanUpdateAnExistingDocument { - FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"]; - NSDictionary *initialData = - @{ @"desc" : @"Description", - @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; - NSDictionary *updateData = - @{@"desc" : @"NewDescription", @"owner.email" : @"new@xyz.com"}; - NSDictionary *finalData = - @{ @"desc" : @"NewDescription", - @"owner" : @{@"name" : @"Jonny", @"email" : @"new@xyz.com"} }; - - [self writeDocumentRef:doc data:initialData]; - - XCTestExpectation *updateCompletion = [self expectationWithDescription:@"updateData"]; - [doc updateData:updateData - completion:^(NSError *_Nullable error) { - XCTAssertNil(error); - [updateCompletion fulfill]; - }]; - [self awaitExpectations]; - - FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; - XCTAssertTrue(result.exists); - XCTAssertEqualObjects(result.data, finalData); -} - -- (void)testCanDeleteAFieldWithAnUpdate { - FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"]; - NSDictionary *initialData = - @{ @"desc" : @"Description", - @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; - NSDictionary *updateData = - @{@"owner.email" : [FIRFieldValue fieldValueForDelete]}; - NSDictionary *finalData = - @{ @"desc" : @"Description", - @"owner" : @{@"name" : @"Jonny"} }; - - [self writeDocumentRef:doc data:initialData]; - [self updateDocumentRef:doc data:updateData]; - - FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; - XCTAssertTrue(result.exists); - XCTAssertEqualObjects(result.data, finalData); -} - -- (void)testDeleteDocument { - FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"]; - NSDictionary *data = @{@"value" : @"foo"}; - [self writeDocumentRef:doc data:data]; - FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; - XCTAssertEqualObjects(result.data, data); - [self deleteDocumentRef:doc]; - result = [self readDocumentForRef:doc]; - XCTAssertFalse(result.exists); -} - -- (void)testCanRetrieveDocumentThatDoesNotExist { - FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; - FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; - XCTAssertNil(result.data); - XCTAssertNil(result[@"foo"]); -} - -- (void)testCannotUpdateNonexistentDocument { - FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; - - XCTestExpectation *setCompletion = [self expectationWithDescription:@"setData"]; - [doc updateData:@{@"owner" : @"abc"} - completion:^(NSError *_Nullable error) { - XCTAssertNotNil(error); - XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); - XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound); - [setCompletion fulfill]; - }]; - [self awaitExpectations]; - FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; - XCTAssertFalse(result.exists); -} - -- (void)testCanOverwriteDataAnExistingDocumentUsingSet { - FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; - - NSDictionary *initialData = - @{ @"desc" : @"Description", - @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; - NSDictionary *udpateData = @{@"desc" : @"NewDescription"}; - - [self writeDocumentRef:doc data:initialData]; - [self writeDocumentRef:doc data:udpateData]; - - FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; - XCTAssertEqualObjects(document.data, udpateData); -} - -- (void)testCanMergeDataWithAnExistingDocumentUsingSet { - FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; - - NSDictionary *initialData = @{ - @"desc" : @"Description", - @"owner.data" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} - }; - NSDictionary *mergeData = - @{ @"updated" : @YES, - @"owner.data" : @{@"name" : @"Sebastian"} }; - NSDictionary *finalData = @{ - @"desc" : @"Description", - @"updated" : @YES, - @"owner.data" : @{@"name" : @"Sebastian", @"email" : @"abc@xyz.com"} - }; - - [self writeDocumentRef:doc data:initialData]; - - XCTestExpectation *completed = - [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"]; - - [doc setData:mergeData - options:[FIRSetOptions merge] - completion:^(NSError *error) { - XCTAssertNil(error); - [completed fulfill]; - }]; - - [self awaitExpectations]; - - FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; - XCTAssertEqualObjects(document.data, finalData); -} - -- (void)testCanMergeServerTimestamps { - FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; - - NSDictionary *initialData = @{ - @"updated" : @NO, - }; - NSDictionary *mergeData = - @{@"time" : [FIRFieldValue fieldValueForServerTimestamp]}; - - [self writeDocumentRef:doc data:initialData]; - - XCTestExpectation *completed = - [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"]; - - [doc setData:mergeData - options:[FIRSetOptions merge] - completion:^(NSError *error) { - XCTAssertNil(error); - [completed fulfill]; - }]; - - [self awaitExpectations]; - - FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; - XCTAssertEqual(document[@"updated"], @NO); - XCTAssertTrue([document[@"time"] isKindOfClass:[NSDate class]]); -} - -- (void)testCanDeleteFieldUsingMerge { - FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; - - NSDictionary *initialData = @{ - @"untouched" : @YES, - @"foo" : @"bar", - @"nested" : @{@"untouched" : @YES, @"foo" : @"bar"} - }; - NSDictionary *mergeData = @{ - @"foo" : [FIRFieldValue fieldValueForDelete], - @"nested" : @{@"foo" : [FIRFieldValue fieldValueForDelete]} - }; - - [self writeDocumentRef:doc data:initialData]; - - XCTestExpectation *completed = - [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"]; - - [doc setData:mergeData - options:[FIRSetOptions merge] - completion:^(NSError *error) { - XCTAssertNil(error); - [completed fulfill]; - }]; - - [self awaitExpectations]; - - FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; - XCTAssertEqual(document[@"untouched"], @YES); - XCTAssertNil(document[@"foo"]); - XCTAssertEqual(document[@"nested.untouched"], @YES); - XCTAssertNil(document[@"nested.foo"]); -} - -- (void)testMergeReplacesArrays { - FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; - - NSDictionary *initialData = @{ - @"untouched" : @YES, - @"data" : @"old", - @"topLevel" : @[ @"old", @"old" ], - @"mapInArray" : @[ @{@"data" : @"old"} ] - }; - NSDictionary *mergeData = - @{ @"data" : @"new", - @"topLevel" : @[ @"new" ], - @"mapInArray" : @[ @{@"data" : @"new"} ] }; - NSDictionary *finalData = @{ - @"untouched" : @YES, - @"data" : @"new", - @"topLevel" : @[ @"new" ], - @"mapInArray" : @[ @{@"data" : @"new"} ] - }; - - [self writeDocumentRef:doc data:initialData]; - - XCTestExpectation *completed = - [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"]; - - [doc setData:mergeData - options:[FIRSetOptions merge] - completion:^(NSError *error) { - XCTAssertNil(error); - [completed fulfill]; - }]; - - [self awaitExpectations]; - - FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; - XCTAssertEqualObjects(document.data, finalData); -} - -- (void)testAddingToACollectionYieldsTheCorrectDocumentReference { - FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"]; - FIRDocumentReference *ref = [coll addDocumentWithData:@{ @"foo" : @1 }]; - - XCTestExpectation *getCompletion = [self expectationWithDescription:@"getData"]; - [ref getDocumentWithCompletion:^(FIRDocumentSnapshot *_Nullable document, - NSError *_Nullable error) { - XCTAssertNil(error); - XCTAssertEqualObjects(document.data, (@{ @"foo" : @1 })); - - [getCompletion fulfill]; - }]; - [self awaitExpectations]; -} - -- (void)testListenCanBeCalledMultipleTimes { - FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"]; - FIRDocumentReference *doc = [coll documentWithAutoID]; - - XCTestExpectation *completed = [self expectationWithDescription:@"multiple addSnapshotListeners"]; - - __block NSDictionary *resultingData; - - // Shut the compiler up about strong references to doc. - FIRDocumentReference *__weak weakDoc = doc; - - [doc setData:@{@"foo" : @"bar"} - completion:^(NSError *error1) { - XCTAssertNil(error1); - FIRDocumentReference *strongDoc = weakDoc; - - [strongDoc addSnapshotListener:^(FIRDocumentSnapshot *snapshot2, NSError *error2) { - XCTAssertNil(error2); - - FIRDocumentReference *strongDoc2 = weakDoc; - [strongDoc2 addSnapshotListener:^(FIRDocumentSnapshot *snapshot3, NSError *error3) { - XCTAssertNil(error3); - resultingData = snapshot3.data; - [completed fulfill]; - }]; - }]; - }]; - - [self awaitExpectations]; - XCTAssertEqualObjects(resultingData, @{@"foo" : @"bar"}); -} - -- (void)testDocumentSnapshotEvents_nonExistent { - FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; - - XCTestExpectation *snapshotCompletion = [self expectationWithDescription:@"snapshot"]; - __block int callbacks = 0; - - id listenerRegistration = - [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { - callbacks++; - - if (callbacks == 1) { - XCTAssertNotNil(doc); - XCTAssertFalse(doc.exists); - [snapshotCompletion fulfill]; - - } else if (callbacks == 2) { - XCTFail("Should not have received this callback"); - } - }]; - - [self awaitExpectations]; - - [listenerRegistration remove]; -} - -- (void)testDocumentSnapshotEvents_forAdd { - FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; - - XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"]; - __block XCTestExpectation *dataCompletion; - __block int callbacks = 0; - - id listenerRegistration = - [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { - callbacks++; - - if (callbacks == 1) { - XCTAssertNotNil(doc); - XCTAssertFalse(doc.exists); - [emptyCompletion fulfill]; - - } else if (callbacks == 2) { - XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 })); - XCTAssertEqual(doc.metadata.hasPendingWrites, YES); - [dataCompletion fulfill]; - - } else if (callbacks == 3) { - XCTFail("Should not have received this callback"); - } - }]; - - [self awaitExpectations]; - dataCompletion = [self expectationWithDescription:@"data snapshot"]; - - [docRef setData:@{ @"a" : @1 }]; - [self awaitExpectations]; - - [listenerRegistration remove]; -} - -- (void)testDocumentSnapshotEvents_forAddIncludingMetadata { - FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; - - XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"]; - __block XCTestExpectation *dataCompletion; - __block int callbacks = 0; - - FIRDocumentListenOptions *options = - [[FIRDocumentListenOptions options] includeMetadataChanges:YES]; - - id listenerRegistration = - [docRef addSnapshotListenerWithOptions:options - listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { - callbacks++; - - if (callbacks == 1) { - XCTAssertNotNil(doc); - XCTAssertFalse(doc.exists); - [emptyCompletion fulfill]; - - } else if (callbacks == 2) { - XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 })); - XCTAssertEqual(doc.metadata.hasPendingWrites, YES); - - } else if (callbacks == 3) { - XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 })); - XCTAssertEqual(doc.metadata.hasPendingWrites, NO); - [dataCompletion fulfill]; - - } else if (callbacks == 4) { - XCTFail("Should not have received this callback"); - } - }]; - - [self awaitExpectations]; - dataCompletion = [self expectationWithDescription:@"data snapshot"]; - - [docRef setData:@{ @"a" : @1 }]; - [self awaitExpectations]; - - [listenerRegistration remove]; -} - -- (void)testDocumentSnapshotEvents_forChange { - FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; - - NSDictionary *initialData = @{ @"a" : @1 }; - NSDictionary *changedData = @{ @"b" : @2 }; - - [self writeDocumentRef:docRef data:initialData]; - - XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; - __block XCTestExpectation *changeCompletion; - __block int callbacks = 0; - - id listenerRegistration = - [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { - callbacks++; - - if (callbacks == 1) { - XCTAssertEqualObjects(doc.data, initialData); - XCTAssertEqual(doc.metadata.hasPendingWrites, NO); - [initialCompletion fulfill]; - - } else if (callbacks == 2) { - XCTAssertEqualObjects(doc.data, changedData); - XCTAssertEqual(doc.metadata.hasPendingWrites, YES); - [changeCompletion fulfill]; - - } else if (callbacks == 3) { - XCTFail("Should not have received this callback"); - } - }]; - - [self awaitExpectations]; - changeCompletion = [self expectationWithDescription:@"listen for changed data"]; - - [docRef setData:changedData]; - [self awaitExpectations]; - - [listenerRegistration remove]; -} - -- (void)testDocumentSnapshotEvents_forChangeIncludingMetadata { - FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; - - NSDictionary *initialData = @{ @"a" : @1 }; - NSDictionary *changedData = @{ @"b" : @2 }; - - [self writeDocumentRef:docRef data:initialData]; - - XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; - __block XCTestExpectation *changeCompletion; - __block int callbacks = 0; - - FIRDocumentListenOptions *options = - [[FIRDocumentListenOptions options] includeMetadataChanges:YES]; - - id listenerRegistration = - [docRef addSnapshotListenerWithOptions:options - listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { - callbacks++; - - if (callbacks == 1) { - XCTAssertEqualObjects(doc.data, initialData); - XCTAssertEqual(doc.metadata.hasPendingWrites, NO); - XCTAssertEqual(doc.metadata.isFromCache, YES); - - } else if (callbacks == 2) { - XCTAssertEqualObjects(doc.data, initialData); - XCTAssertEqual(doc.metadata.hasPendingWrites, NO); - XCTAssertEqual(doc.metadata.isFromCache, NO); - [initialCompletion fulfill]; - - } else if (callbacks == 3) { - XCTAssertEqualObjects(doc.data, changedData); - XCTAssertEqual(doc.metadata.hasPendingWrites, YES); - XCTAssertEqual(doc.metadata.isFromCache, NO); - - } else if (callbacks == 4) { - XCTAssertEqualObjects(doc.data, changedData); - XCTAssertEqual(doc.metadata.hasPendingWrites, NO); - XCTAssertEqual(doc.metadata.isFromCache, NO); - [changeCompletion fulfill]; - - } else if (callbacks == 5) { - XCTFail("Should not have received this callback"); - } - }]; - - [self awaitExpectations]; - changeCompletion = [self expectationWithDescription:@"listen for changed data"]; - - [docRef setData:changedData]; - [self awaitExpectations]; - - [listenerRegistration remove]; -} - -- (void)testDocumentSnapshotEvents_forDelete { - FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; - - NSDictionary *initialData = @{ @"a" : @1 }; - - [self writeDocumentRef:docRef data:initialData]; - - XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; - __block XCTestExpectation *changeCompletion; - __block int callbacks = 0; - - id listenerRegistration = - [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { - callbacks++; - - if (callbacks == 1) { - XCTAssertEqualObjects(doc.data, initialData); - XCTAssertEqual(doc.metadata.hasPendingWrites, NO); - XCTAssertEqual(doc.metadata.isFromCache, YES); - [initialCompletion fulfill]; - - } else if (callbacks == 2) { - XCTAssertFalse(doc.exists); - [changeCompletion fulfill]; - - } else if (callbacks == 3) { - XCTFail("Should not have received this callback"); - } - }]; - - [self awaitExpectations]; - changeCompletion = [self expectationWithDescription:@"listen for changed data"]; - - [docRef deleteDocument]; - [self awaitExpectations]; - - [listenerRegistration remove]; -} - -- (void)testDocumentSnapshotEvents_forDeleteIncludingMetadata { - FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; - - NSDictionary *initialData = @{ @"a" : @1 }; - - [self writeDocumentRef:docRef data:initialData]; - - FIRDocumentListenOptions *options = - [[FIRDocumentListenOptions options] includeMetadataChanges:YES]; - - XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; - __block XCTestExpectation *changeCompletion; - __block int callbacks = 0; - - id listenerRegistration = - [docRef addSnapshotListenerWithOptions:options - listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { - callbacks++; - - if (callbacks == 1) { - XCTAssertEqualObjects(doc.data, initialData); - XCTAssertEqual(doc.metadata.hasPendingWrites, NO); - XCTAssertEqual(doc.metadata.isFromCache, YES); - - } else if (callbacks == 2) { - XCTAssertEqualObjects(doc.data, initialData); - XCTAssertEqual(doc.metadata.hasPendingWrites, NO); - XCTAssertEqual(doc.metadata.isFromCache, NO); - [initialCompletion fulfill]; - - } else if (callbacks == 3) { - XCTAssertFalse(doc.exists); - XCTAssertEqual(doc.metadata.hasPendingWrites, NO); - XCTAssertEqual(doc.metadata.isFromCache, NO); - [changeCompletion fulfill]; - - } else if (callbacks == 4) { - XCTFail("Should not have received this callback"); - } - }]; - - [self awaitExpectations]; - changeCompletion = [self expectationWithDescription:@"listen for changed data"]; - - [docRef deleteDocument]; - [self awaitExpectations]; - - [listenerRegistration remove]; -} - -- (void)testQuerySnapshotEvents_forAdd { - FIRCollectionReference *roomsRef = [self collectionRef]; - FIRDocumentReference *docRef = [roomsRef documentWithAutoID]; - - NSDictionary *newData = @{ @"a" : @1 }; - - XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"]; - __block XCTestExpectation *changeCompletion; - __block int callbacks = 0; - - id listenerRegistration = - [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) { - callbacks++; - - if (callbacks == 1) { - XCTAssertEqual(docSet.count, 0); - [emptyCompletion fulfill]; - - } else if (callbacks == 2) { - XCTAssertEqual(docSet.count, 1); - XCTAssertTrue([docSet.documents[0] isKindOfClass:[FIRQueryDocumentSnapshot class]]); - XCTAssertEqualObjects(docSet.documents[0].data, newData); - XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, YES); - [changeCompletion fulfill]; - - } else if (callbacks == 3) { - XCTFail("Should not have received a third callback"); - } - }]; - - [self awaitExpectations]; - changeCompletion = [self expectationWithDescription:@"changed snapshot"]; - - [docRef setData:newData]; - [self awaitExpectations]; - - [listenerRegistration remove]; -} - -- (void)testQuerySnapshotEvents_forChange { - FIRCollectionReference *roomsRef = [self collectionRef]; - FIRDocumentReference *docRef = [roomsRef documentWithAutoID]; - - NSDictionary *initialData = @{ @"a" : @1 }; - NSDictionary *changedData = @{ @"b" : @2 }; - - [self writeDocumentRef:docRef data:initialData]; - - XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; - __block XCTestExpectation *changeCompletion; - __block int callbacks = 0; - - id listenerRegistration = - [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) { - callbacks++; - - if (callbacks == 1) { - XCTAssertEqual(docSet.count, 1); - XCTAssertEqualObjects(docSet.documents[0].data, initialData); - XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, NO); - [initialCompletion fulfill]; - - } else if (callbacks == 2) { - XCTAssertEqual(docSet.count, 1); - XCTAssertEqualObjects(docSet.documents[0].data, changedData); - XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, YES); - [changeCompletion fulfill]; - - } else if (callbacks == 3) { - XCTFail("Should not have received a third callback"); - } - }]; - - [self awaitExpectations]; - changeCompletion = [self expectationWithDescription:@"listen for changed data"]; - - [docRef setData:changedData]; - [self awaitExpectations]; - - [listenerRegistration remove]; -} - -- (void)testQuerySnapshotEvents_forDelete { - FIRCollectionReference *roomsRef = [self collectionRef]; - FIRDocumentReference *docRef = [roomsRef documentWithAutoID]; - - NSDictionary *initialData = @{ @"a" : @1 }; - - [self writeDocumentRef:docRef data:initialData]; - - XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; - __block XCTestExpectation *changeCompletion; - __block int callbacks = 0; - - id listenerRegistration = - [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) { - callbacks++; - - if (callbacks == 1) { - XCTAssertEqual(docSet.count, 1); - XCTAssertEqualObjects(docSet.documents[0].data, initialData); - XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, NO); - [initialCompletion fulfill]; - - } else if (callbacks == 2) { - XCTAssertEqual(docSet.count, 0); - [changeCompletion fulfill]; - - } else if (callbacks == 4) { - XCTFail("Should not have received a third callback"); - } - }]; - - [self awaitExpectations]; - changeCompletion = [self expectationWithDescription:@"listen for changed data"]; - - [docRef deleteDocument]; - [self awaitExpectations]; - - [listenerRegistration remove]; -} - -- (void)testExposesFirestoreOnDocumentReferences { - FIRDocumentReference *doc = [self.db documentWithPath:@"foo/bar"]; - XCTAssertEqual(doc.firestore, self.db); -} - -- (void)testExposesFirestoreOnQueries { - FIRQuery *q = [[self.db collectionWithPath:@"foo"] queryLimitedTo:5]; - XCTAssertEqual(q.firestore, self.db); -} - -- (void)testDocumentReferenceEquality { - FIRFirestore *firestore = self.db; - FIRDocumentReference *docRef = [firestore documentWithPath:@"foo/bar"]; - XCTAssertEqualObjects([firestore documentWithPath:@"foo/bar"], docRef); - XCTAssertEqualObjects([docRef collectionWithPath:@"blah"].parent, docRef); - - XCTAssertNotEqualObjects([firestore documentWithPath:@"foo/BAR"], docRef); - - FIRFirestore *otherFirestore = [self firestore]; - XCTAssertNotEqualObjects([otherFirestore documentWithPath:@"foo/bar"], docRef); -} - -- (void)testQueryReferenceEquality { - FIRFirestore *firestore = self.db; - FIRQuery *query = - [[[firestore collectionWithPath:@"foo"] queryOrderedByField:@"bar"] queryWhereField:@"baz" - isEqualTo:@42]; - FIRQuery *query2 = - [[[firestore collectionWithPath:@"foo"] queryOrderedByField:@"bar"] queryWhereField:@"baz" - isEqualTo:@42]; - XCTAssertEqualObjects(query, query2); - - FIRQuery *query3 = - [[[firestore collectionWithPath:@"foo"] queryOrderedByField:@"BAR"] queryWhereField:@"baz" - isEqualTo:@42]; - XCTAssertNotEqualObjects(query, query3); - - FIRFirestore *otherFirestore = [self firestore]; - FIRQuery *query4 = [[[otherFirestore collectionWithPath:@"foo"] queryOrderedByField:@"bar"] - queryWhereField:@"baz" - isEqualTo:@42]; - XCTAssertNotEqualObjects(query, query4); -} - -- (void)testCanTraverseCollectionsAndDocuments { - NSString *expected = @"a/b/c/d"; - // doc path from root Firestore. - XCTAssertEqualObjects([self.db documentWithPath:@"a/b/c/d"].path, expected); - // collection path from root Firestore. - XCTAssertEqualObjects([[self.db collectionWithPath:@"a/b/c"] documentWithPath:@"d"].path, - expected); - // doc path from CollectionReference. - XCTAssertEqualObjects([[self.db collectionWithPath:@"a"] documentWithPath:@"b/c/d"].path, - expected); - // collection path from DocumentReference. - XCTAssertEqualObjects([[self.db documentWithPath:@"a/b"] collectionWithPath:@"c/d/e"].path, - @"a/b/c/d/e"); -} - -- (void)testCanTraverseCollectionAndDocumentParents { - FIRCollectionReference *collection = [self.db collectionWithPath:@"a/b/c"]; - XCTAssertEqualObjects(collection.path, @"a/b/c"); - - FIRDocumentReference *doc = collection.parent; - XCTAssertEqualObjects(doc.path, @"a/b"); - - collection = doc.parent; - XCTAssertEqualObjects(collection.path, @"a"); - - FIRDocumentReference *nilDoc = collection.parent; - XCTAssertNil(nilDoc); -} - -- (void)testUpdateFieldsWithDots { - FIRDocumentReference *doc = [self documentRef]; - - [self writeDocumentRef:doc data:@{@"a.b" : @"old", @"c.d" : @"old"}]; - - [self updateDocumentRef:doc data:@{ [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new" }]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateFieldsWithDots"]; - - [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { - XCTAssertNil(error); - XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"})); - [expectation fulfill]; - }]; - - [self awaitExpectations]; -} - -- (void)testUpdateNestedFields { - FIRDocumentReference *doc = [self documentRef]; - - [self writeDocumentRef:doc - data:@{ - @"a" : @{@"b" : @"old"}, - @"c" : @{@"d" : @"old"}, - @"e" : @{@"f" : @"old"} - }]; - - [self updateDocumentRef:doc - data:@{ - @"a.b" : @"new", - [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new" - }]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateNestedFields"]; - - [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { - XCTAssertNil(error); - XCTAssertEqualObjects(snapshot.data, (@{ - @"a" : @{@"b" : @"new"}, - @"c" : @{@"d" : @"new"}, - @"e" : @{@"f" : @"old"} - })); - [expectation fulfill]; - }]; - - [self awaitExpectations]; -} - -- (void)testCollectionID { - XCTAssertEqualObjects([self.db collectionWithPath:@"foo"].collectionID, @"foo"); - XCTAssertEqualObjects([self.db collectionWithPath:@"foo/bar/baz"].collectionID, @"baz"); -} - -- (void)testDocumentID { - XCTAssertEqualObjects([self.db documentWithPath:@"foo/bar"].documentID, @"bar"); - XCTAssertEqualObjects([self.db documentWithPath:@"foo/bar/baz/qux"].documentID, @"qux"); -} - -- (void)testCanQueueWritesWhileOffline { - XCTestExpectation *writeEpectation = [self expectationWithDescription:@"successfull write"]; - XCTestExpectation *networkExpectation = [self expectationWithDescription:@"enable network"]; - - FIRDocumentReference *doc = [self documentRef]; - FIRFirestore *firestore = doc.firestore; - NSDictionary *data = @{@"a" : @"b"}; - - [firestore disableNetworkWithCompletion:^(NSError *error) { - XCTAssertNil(error); - - [doc setData:data - completion:^(NSError *error) { - XCTAssertNil(error); - [writeEpectation fulfill]; - }]; - - [firestore enableNetworkWithCompletion:^(NSError *error) { - XCTAssertNil(error); - [networkExpectation fulfill]; - }]; - }]; - - [self awaitExpectations]; - - XCTestExpectation *getExpectation = [self expectationWithDescription:@"successfull get"]; - [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { - XCTAssertNil(error); - XCTAssertEqualObjects(snapshot.data, data); - XCTAssertFalse(snapshot.metadata.isFromCache); - - [getExpectation fulfill]; - }]; - - [self awaitExpectations]; -} - -- (void)testCantGetDocumentsWhileOffline { - FIRDocumentReference *doc = [self documentRef]; - FIRFirestore *firestore = doc.firestore; - NSDictionary *data = @{@"a" : @"b"}; - - XCTestExpectation *onlineExpectation = [self expectationWithDescription:@"online read"]; - XCTestExpectation *networkExpectation = [self expectationWithDescription:@"network online"]; - - __weak FIRDocumentReference *weakDoc = doc; - - [firestore disableNetworkWithCompletion:^(NSError *error) { - XCTAssertNil(error); - [doc setData:data - completion:^(NSError *_Nullable error) { - XCTAssertNil(error); - - [weakDoc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { - XCTAssertNil(error); - - // Verify that we are not reading from cache. - XCTAssertFalse(snapshot.metadata.isFromCache); - [onlineExpectation fulfill]; - }]; - }]; - - [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { - XCTAssertNil(error); - - // Verify that we are reading from cache. - XCTAssertTrue(snapshot.metadata.fromCache); - XCTAssertEqualObjects(snapshot.data, data); - [firestore enableNetworkWithCompletion:^(NSError *error) { - [networkExpectation fulfill]; - }]; - }]; - }]; - - [self awaitExpectations]; -} - -- (void)testWriteStreamReconnectsAfterIdle { - FIRDocumentReference *doc = [self documentRef]; - FIRFirestore *firestore = doc.firestore; - - [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; - [self waitForIdleFirestore:firestore]; - [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; -} - -- (void)testWatchStreamReconnectsAfterIdle { - FIRDocumentReference *doc = [self documentRef]; - FIRFirestore *firestore = doc.firestore; - - [self readSnapshotForRef:[self documentRef] requireOnline:YES]; - [self waitForIdleFirestore:firestore]; - [self readSnapshotForRef:[self documentRef] requireOnline:YES]; -} - -- (void)testCanDisableNetwork { - FIRDocumentReference *doc = [self documentRef]; - FIRFirestore *firestore = doc.firestore; - - [firestore enableNetworkWithCompletion:[self completionForExpectationWithName:@"Enable network"]]; - [self awaitExpectations]; - [firestore - enableNetworkWithCompletion:[self completionForExpectationWithName:@"Enable network again"]]; - [self awaitExpectations]; - [firestore - disableNetworkWithCompletion:[self completionForExpectationWithName:@"Disable network"]]; - [self awaitExpectations]; - [firestore - disableNetworkWithCompletion:[self - completionForExpectationWithName:@"Disable network again"]]; - [self awaitExpectations]; - [firestore - enableNetworkWithCompletion:[self completionForExpectationWithName:@"Final enable network"]]; - [self awaitExpectations]; -} - -@end diff --git a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm new file mode 100644 index 0000000..3b6a67e --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm @@ -0,0 +1,963 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import + +#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/Core/FSTFirestoreClient.h" + +@interface FIRDatabaseTests : FSTIntegrationTestCase +@end + +@implementation FIRDatabaseTests + +- (void)testCanUpdateAnExistingDocument { + FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"]; + NSDictionary *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + NSDictionary *updateData = + @{@"desc" : @"NewDescription", @"owner.email" : @"new@xyz.com"}; + NSDictionary *finalData = + @{ @"desc" : @"NewDescription", + @"owner" : @{@"name" : @"Jonny", @"email" : @"new@xyz.com"} }; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *updateCompletion = [self expectationWithDescription:@"updateData"]; + [doc updateData:updateData + completion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [updateCompletion fulfill]; + }]; + [self awaitExpectations]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertTrue(result.exists); + XCTAssertEqualObjects(result.data, finalData); +} + +- (void)testCanDeleteAFieldWithAnUpdate { + FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"]; + NSDictionary *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + NSDictionary *updateData = + @{@"owner.email" : [FIRFieldValue fieldValueForDelete]}; + NSDictionary *finalData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny"} }; + + [self writeDocumentRef:doc data:initialData]; + [self updateDocumentRef:doc data:updateData]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertTrue(result.exists); + XCTAssertEqualObjects(result.data, finalData); +} + +- (void)testDeleteDocument { + FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"]; + NSDictionary *data = @{@"value" : @"foo"}; + [self writeDocumentRef:doc data:data]; + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(result.data, data); + [self deleteDocumentRef:doc]; + result = [self readDocumentForRef:doc]; + XCTAssertFalse(result.exists); +} + +- (void)testCanRetrieveDocumentThatDoesNotExist { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertNil(result.data); + XCTAssertNil(result[@"foo"]); +} + +- (void)testCannotUpdateNonexistentDocument { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + XCTestExpectation *setCompletion = [self expectationWithDescription:@"setData"]; + [doc updateData:@{@"owner" : @"abc"} + completion:^(NSError *_Nullable error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound); + [setCompletion fulfill]; + }]; + [self awaitExpectations]; + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertFalse(result.exists); +} + +- (void)testCanOverwriteDataAnExistingDocumentUsingSet { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + NSDictionary *udpateData = @{@"desc" : @"NewDescription"}; + + [self writeDocumentRef:doc data:initialData]; + [self writeDocumentRef:doc data:udpateData]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, udpateData); +} + +- (void)testCanMergeDataWithAnExistingDocumentUsingSet { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary *initialData = @{ + @"desc" : @"Description", + @"owner.data" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} + }; + NSDictionary *mergeData = + @{ @"updated" : @YES, + @"owner.data" : @{@"name" : @"Sebastian"} }; + NSDictionary *finalData = @{ + @"desc" : @"Description", + @"updated" : @YES, + @"owner.data" : @{@"name" : @"Sebastian", @"email" : @"abc@xyz.com"} + }; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"]; + + [doc setData:mergeData + options:[FIRSetOptions merge] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + +- (void)testCanMergeServerTimestamps { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary *initialData = @{ + @"updated" : @NO, + }; + NSDictionary *mergeData = + @{@"time" : [FIRFieldValue fieldValueForServerTimestamp]}; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"]; + + [doc setData:mergeData + options:[FIRSetOptions merge] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqual(document[@"updated"], @NO); + XCTAssertTrue([document[@"time"] isKindOfClass:[NSDate class]]); +} + +- (void)testCanDeleteFieldUsingMerge { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary *initialData = @{ + @"untouched" : @YES, + @"foo" : @"bar", + @"nested" : @{@"untouched" : @YES, @"foo" : @"bar"} + }; + NSDictionary *mergeData = @{ + @"foo" : [FIRFieldValue fieldValueForDelete], + @"nested" : @{@"foo" : [FIRFieldValue fieldValueForDelete]} + }; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"]; + + [doc setData:mergeData + options:[FIRSetOptions merge] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqual(document[@"untouched"], @YES); + XCTAssertNil(document[@"foo"]); + XCTAssertEqual(document[@"nested.untouched"], @YES); + XCTAssertNil(document[@"nested.foo"]); +} + +- (void)testMergeReplacesArrays { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary *initialData = @{ + @"untouched" : @YES, + @"data" : @"old", + @"topLevel" : @[ @"old", @"old" ], + @"mapInArray" : @[ @{@"data" : @"old"} ] + }; + NSDictionary *mergeData = + @{ @"data" : @"new", + @"topLevel" : @[ @"new" ], + @"mapInArray" : @[ @{@"data" : @"new"} ] }; + NSDictionary *finalData = @{ + @"untouched" : @YES, + @"data" : @"new", + @"topLevel" : @[ @"new" ], + @"mapInArray" : @[ @{@"data" : @"new"} ] + }; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"]; + + [doc setData:mergeData + options:[FIRSetOptions merge] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + +- (void)testAddingToACollectionYieldsTheCorrectDocumentReference { + FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"]; + FIRDocumentReference *ref = [coll addDocumentWithData:@{ @"foo" : @1 }]; + + XCTestExpectation *getCompletion = [self expectationWithDescription:@"getData"]; + [ref getDocumentWithCompletion:^(FIRDocumentSnapshot *_Nullable document, + NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects(document.data, (@{ @"foo" : @1 })); + + [getCompletion fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testListenCanBeCalledMultipleTimes { + FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"]; + FIRDocumentReference *doc = [coll documentWithAutoID]; + + XCTestExpectation *completed = [self expectationWithDescription:@"multiple addSnapshotListeners"]; + + __block NSDictionary *resultingData; + + // Shut the compiler up about strong references to doc. + FIRDocumentReference *__weak weakDoc = doc; + + [doc setData:@{@"foo" : @"bar"} + completion:^(NSError *error1) { + XCTAssertNil(error1); + FIRDocumentReference *strongDoc = weakDoc; + + [strongDoc addSnapshotListener:^(FIRDocumentSnapshot *snapshot2, NSError *error2) { + XCTAssertNil(error2); + + FIRDocumentReference *strongDoc2 = weakDoc; + [strongDoc2 addSnapshotListener:^(FIRDocumentSnapshot *snapshot3, NSError *error3) { + XCTAssertNil(error3); + resultingData = snapshot3.data; + [completed fulfill]; + }]; + }]; + }]; + + [self awaitExpectations]; + XCTAssertEqualObjects(resultingData, @{@"foo" : @"bar"}); +} + +- (void)testDocumentSnapshotEvents_nonExistent { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + XCTestExpectation *snapshotCompletion = [self expectationWithDescription:@"snapshot"]; + __block int callbacks = 0; + + id listenerRegistration = + [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertNotNil(doc); + XCTAssertFalse(doc.exists); + [snapshotCompletion fulfill]; + + } else if (callbacks == 2) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testDocumentSnapshotEvents_forAdd { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"]; + __block XCTestExpectation *dataCompletion; + __block int callbacks = 0; + + id listenerRegistration = + [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertNotNil(doc); + XCTAssertFalse(doc.exists); + [emptyCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 })); + XCTAssertEqual(doc.metadata.hasPendingWrites, YES); + [dataCompletion fulfill]; + + } else if (callbacks == 3) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + dataCompletion = [self expectationWithDescription:@"data snapshot"]; + + [docRef setData:@{ @"a" : @1 }]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testDocumentSnapshotEvents_forAddIncludingMetadata { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"]; + __block XCTestExpectation *dataCompletion; + __block int callbacks = 0; + + FIRDocumentListenOptions *options = + [[FIRDocumentListenOptions options] includeMetadataChanges:YES]; + + id listenerRegistration = + [docRef addSnapshotListenerWithOptions:options + listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertNotNil(doc); + XCTAssertFalse(doc.exists); + [emptyCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 })); + XCTAssertEqual(doc.metadata.hasPendingWrites, YES); + + } else if (callbacks == 3) { + XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 })); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + [dataCompletion fulfill]; + + } else if (callbacks == 4) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + dataCompletion = [self expectationWithDescription:@"data snapshot"]; + + [docRef setData:@{ @"a" : @1 }]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testDocumentSnapshotEvents_forChange { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary *initialData = @{ @"a" : @1 }; + NSDictionary *changedData = @{ @"b" : @2 }; + + [self writeDocumentRef:docRef data:initialData]; + + XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + id listenerRegistration = + [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqualObjects(doc.data, initialData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + [initialCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertEqualObjects(doc.data, changedData); + XCTAssertEqual(doc.metadata.hasPendingWrites, YES); + [changeCompletion fulfill]; + + } else if (callbacks == 3) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"listen for changed data"]; + + [docRef setData:changedData]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testDocumentSnapshotEvents_forChangeIncludingMetadata { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary *initialData = @{ @"a" : @1 }; + NSDictionary *changedData = @{ @"b" : @2 }; + + [self writeDocumentRef:docRef data:initialData]; + + XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + FIRDocumentListenOptions *options = + [[FIRDocumentListenOptions options] includeMetadataChanges:YES]; + + id listenerRegistration = + [docRef addSnapshotListenerWithOptions:options + listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqualObjects(doc.data, initialData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, YES); + + } else if (callbacks == 2) { + XCTAssertEqualObjects(doc.data, initialData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, NO); + [initialCompletion fulfill]; + + } else if (callbacks == 3) { + XCTAssertEqualObjects(doc.data, changedData); + XCTAssertEqual(doc.metadata.hasPendingWrites, YES); + XCTAssertEqual(doc.metadata.isFromCache, NO); + + } else if (callbacks == 4) { + XCTAssertEqualObjects(doc.data, changedData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, NO); + [changeCompletion fulfill]; + + } else if (callbacks == 5) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"listen for changed data"]; + + [docRef setData:changedData]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testDocumentSnapshotEvents_forDelete { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary *initialData = @{ @"a" : @1 }; + + [self writeDocumentRef:docRef data:initialData]; + + XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + id listenerRegistration = + [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqualObjects(doc.data, initialData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, YES); + [initialCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertFalse(doc.exists); + [changeCompletion fulfill]; + + } else if (callbacks == 3) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"listen for changed data"]; + + [docRef deleteDocument]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testDocumentSnapshotEvents_forDeleteIncludingMetadata { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary *initialData = @{ @"a" : @1 }; + + [self writeDocumentRef:docRef data:initialData]; + + FIRDocumentListenOptions *options = + [[FIRDocumentListenOptions options] includeMetadataChanges:YES]; + + XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + id listenerRegistration = + [docRef addSnapshotListenerWithOptions:options + listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqualObjects(doc.data, initialData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, YES); + + } else if (callbacks == 2) { + XCTAssertEqualObjects(doc.data, initialData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, NO); + [initialCompletion fulfill]; + + } else if (callbacks == 3) { + XCTAssertFalse(doc.exists); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, NO); + [changeCompletion fulfill]; + + } else if (callbacks == 4) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"listen for changed data"]; + + [docRef deleteDocument]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testQuerySnapshotEvents_forAdd { + FIRCollectionReference *roomsRef = [self collectionRef]; + FIRDocumentReference *docRef = [roomsRef documentWithAutoID]; + + NSDictionary *newData = @{ @"a" : @1 }; + + XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + id listenerRegistration = + [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqual(docSet.count, 0); + [emptyCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertEqual(docSet.count, 1); + XCTAssertTrue([docSet.documents[0] isKindOfClass:[FIRQueryDocumentSnapshot class]]); + XCTAssertEqualObjects(docSet.documents[0].data, newData); + XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, YES); + [changeCompletion fulfill]; + + } else if (callbacks == 3) { + XCTFail("Should not have received a third callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"changed snapshot"]; + + [docRef setData:newData]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testQuerySnapshotEvents_forChange { + FIRCollectionReference *roomsRef = [self collectionRef]; + FIRDocumentReference *docRef = [roomsRef documentWithAutoID]; + + NSDictionary *initialData = @{ @"a" : @1 }; + NSDictionary *changedData = @{ @"b" : @2 }; + + [self writeDocumentRef:docRef data:initialData]; + + XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + id listenerRegistration = + [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqual(docSet.count, 1); + XCTAssertEqualObjects(docSet.documents[0].data, initialData); + XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, NO); + [initialCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertEqual(docSet.count, 1); + XCTAssertEqualObjects(docSet.documents[0].data, changedData); + XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, YES); + [changeCompletion fulfill]; + + } else if (callbacks == 3) { + XCTFail("Should not have received a third callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"listen for changed data"]; + + [docRef setData:changedData]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testQuerySnapshotEvents_forDelete { + FIRCollectionReference *roomsRef = [self collectionRef]; + FIRDocumentReference *docRef = [roomsRef documentWithAutoID]; + + NSDictionary *initialData = @{ @"a" : @1 }; + + [self writeDocumentRef:docRef data:initialData]; + + XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + id listenerRegistration = + [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqual(docSet.count, 1); + XCTAssertEqualObjects(docSet.documents[0].data, initialData); + XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, NO); + [initialCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertEqual(docSet.count, 0); + [changeCompletion fulfill]; + + } else if (callbacks == 4) { + XCTFail("Should not have received a third callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"listen for changed data"]; + + [docRef deleteDocument]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testExposesFirestoreOnDocumentReferences { + FIRDocumentReference *doc = [self.db documentWithPath:@"foo/bar"]; + XCTAssertEqual(doc.firestore, self.db); +} + +- (void)testExposesFirestoreOnQueries { + FIRQuery *q = [[self.db collectionWithPath:@"foo"] queryLimitedTo:5]; + XCTAssertEqual(q.firestore, self.db); +} + +- (void)testDocumentReferenceEquality { + FIRFirestore *firestore = self.db; + FIRDocumentReference *docRef = [firestore documentWithPath:@"foo/bar"]; + XCTAssertEqualObjects([firestore documentWithPath:@"foo/bar"], docRef); + XCTAssertEqualObjects([docRef collectionWithPath:@"blah"].parent, docRef); + + XCTAssertNotEqualObjects([firestore documentWithPath:@"foo/BAR"], docRef); + + FIRFirestore *otherFirestore = [self firestore]; + XCTAssertNotEqualObjects([otherFirestore documentWithPath:@"foo/bar"], docRef); +} + +- (void)testQueryReferenceEquality { + FIRFirestore *firestore = self.db; + FIRQuery *query = + [[[firestore collectionWithPath:@"foo"] queryOrderedByField:@"bar"] queryWhereField:@"baz" + isEqualTo:@42]; + FIRQuery *query2 = + [[[firestore collectionWithPath:@"foo"] queryOrderedByField:@"bar"] queryWhereField:@"baz" + isEqualTo:@42]; + XCTAssertEqualObjects(query, query2); + + FIRQuery *query3 = + [[[firestore collectionWithPath:@"foo"] queryOrderedByField:@"BAR"] queryWhereField:@"baz" + isEqualTo:@42]; + XCTAssertNotEqualObjects(query, query3); + + FIRFirestore *otherFirestore = [self firestore]; + FIRQuery *query4 = [[[otherFirestore collectionWithPath:@"foo"] queryOrderedByField:@"bar"] + queryWhereField:@"baz" + isEqualTo:@42]; + XCTAssertNotEqualObjects(query, query4); +} + +- (void)testCanTraverseCollectionsAndDocuments { + NSString *expected = @"a/b/c/d"; + // doc path from root Firestore. + XCTAssertEqualObjects([self.db documentWithPath:@"a/b/c/d"].path, expected); + // collection path from root Firestore. + XCTAssertEqualObjects([[self.db collectionWithPath:@"a/b/c"] documentWithPath:@"d"].path, + expected); + // doc path from CollectionReference. + XCTAssertEqualObjects([[self.db collectionWithPath:@"a"] documentWithPath:@"b/c/d"].path, + expected); + // collection path from DocumentReference. + XCTAssertEqualObjects([[self.db documentWithPath:@"a/b"] collectionWithPath:@"c/d/e"].path, + @"a/b/c/d/e"); +} + +- (void)testCanTraverseCollectionAndDocumentParents { + FIRCollectionReference *collection = [self.db collectionWithPath:@"a/b/c"]; + XCTAssertEqualObjects(collection.path, @"a/b/c"); + + FIRDocumentReference *doc = collection.parent; + XCTAssertEqualObjects(doc.path, @"a/b"); + + collection = doc.parent; + XCTAssertEqualObjects(collection.path, @"a"); + + FIRDocumentReference *nilDoc = collection.parent; + XCTAssertNil(nilDoc); +} + +- (void)testUpdateFieldsWithDots { + FIRDocumentReference *doc = [self documentRef]; + + [self writeDocumentRef:doc data:@{@"a.b" : @"old", @"c.d" : @"old"}]; + + [self updateDocumentRef:doc data:@{ [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new" }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateFieldsWithDots"]; + + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"})); + [expectation fulfill]; + }]; + + [self awaitExpectations]; +} + +- (void)testUpdateNestedFields { + FIRDocumentReference *doc = [self documentRef]; + + [self writeDocumentRef:doc + data:@{ + @"a" : @{@"b" : @"old"}, + @"c" : @{@"d" : @"old"}, + @"e" : @{@"f" : @"old"} + }]; + + [self updateDocumentRef:doc + data:@{ + @"a.b" : @"new", + [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new" + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateNestedFields"]; + + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{ + @"a" : @{@"b" : @"new"}, + @"c" : @{@"d" : @"new"}, + @"e" : @{@"f" : @"old"} + })); + [expectation fulfill]; + }]; + + [self awaitExpectations]; +} + +- (void)testCollectionID { + XCTAssertEqualObjects([self.db collectionWithPath:@"foo"].collectionID, @"foo"); + XCTAssertEqualObjects([self.db collectionWithPath:@"foo/bar/baz"].collectionID, @"baz"); +} + +- (void)testDocumentID { + XCTAssertEqualObjects([self.db documentWithPath:@"foo/bar"].documentID, @"bar"); + XCTAssertEqualObjects([self.db documentWithPath:@"foo/bar/baz/qux"].documentID, @"qux"); +} + +- (void)testCanQueueWritesWhileOffline { + XCTestExpectation *writeEpectation = [self expectationWithDescription:@"successfull write"]; + XCTestExpectation *networkExpectation = [self expectationWithDescription:@"enable network"]; + + FIRDocumentReference *doc = [self documentRef]; + FIRFirestore *firestore = doc.firestore; + NSDictionary *data = @{@"a" : @"b"}; + + [firestore disableNetworkWithCompletion:^(NSError *error) { + XCTAssertNil(error); + + [doc setData:data + completion:^(NSError *error) { + XCTAssertNil(error); + [writeEpectation fulfill]; + }]; + + [firestore enableNetworkWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [networkExpectation fulfill]; + }]; + }]; + + [self awaitExpectations]; + + XCTestExpectation *getExpectation = [self expectationWithDescription:@"successfull get"]; + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, data); + XCTAssertFalse(snapshot.metadata.isFromCache); + + [getExpectation fulfill]; + }]; + + [self awaitExpectations]; +} + +- (void)testCantGetDocumentsWhileOffline { + FIRDocumentReference *doc = [self documentRef]; + FIRFirestore *firestore = doc.firestore; + NSDictionary *data = @{@"a" : @"b"}; + + XCTestExpectation *onlineExpectation = [self expectationWithDescription:@"online read"]; + XCTestExpectation *networkExpectation = [self expectationWithDescription:@"network online"]; + + __weak FIRDocumentReference *weakDoc = doc; + + [firestore disableNetworkWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [doc setData:data + completion:^(NSError *_Nullable error) { + XCTAssertNil(error); + + [weakDoc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + + // Verify that we are not reading from cache. + XCTAssertFalse(snapshot.metadata.isFromCache); + [onlineExpectation fulfill]; + }]; + }]; + + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + + // Verify that we are reading from cache. + XCTAssertTrue(snapshot.metadata.fromCache); + XCTAssertEqualObjects(snapshot.data, data); + [firestore enableNetworkWithCompletion:^(NSError *error) { + [networkExpectation fulfill]; + }]; + }]; + }]; + + [self awaitExpectations]; +} + +- (void)testWriteStreamReconnectsAfterIdle { + FIRDocumentReference *doc = [self documentRef]; + FIRFirestore *firestore = doc.firestore; + + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; + [self waitForIdleFirestore:firestore]; + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; +} + +- (void)testWatchStreamReconnectsAfterIdle { + FIRDocumentReference *doc = [self documentRef]; + FIRFirestore *firestore = doc.firestore; + + [self readSnapshotForRef:[self documentRef] requireOnline:YES]; + [self waitForIdleFirestore:firestore]; + [self readSnapshotForRef:[self documentRef] requireOnline:YES]; +} + +- (void)testCanDisableNetwork { + FIRDocumentReference *doc = [self documentRef]; + FIRFirestore *firestore = doc.firestore; + + [firestore enableNetworkWithCompletion:[self completionForExpectationWithName:@"Enable network"]]; + [self awaitExpectations]; + [firestore + enableNetworkWithCompletion:[self completionForExpectationWithName:@"Enable network again"]]; + [self awaitExpectations]; + [firestore + disableNetworkWithCompletion:[self completionForExpectationWithName:@"Disable network"]]; + [self awaitExpectations]; + [firestore + disableNetworkWithCompletion:[self + completionForExpectationWithName:@"Disable network again"]]; + [self awaitExpectations]; + [firestore + enableNetworkWithCompletion:[self completionForExpectationWithName:@"Final enable network"]]; + [self awaitExpectations]; +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRFieldsTests.m b/Firestore/Example/Tests/Integration/API/FIRFieldsTests.m deleted file mode 100644 index b647f52..0000000 --- a/Firestore/Example/Tests/Integration/API/FIRFieldsTests.m +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 FirebaseFirestore; - -#import - -#import "Firestore/Source/Core/FSTFirestoreClient.h" - -#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" - -@interface FIRFieldsTests : FSTIntegrationTestCase -@end - -@implementation FIRFieldsTests - -- (NSDictionary *)testNestedDataNumbered:(int)number { - return @{ - @"name" : [NSString stringWithFormat:@"room %d", number], - @"metadata" : @{ - @"createdAt" : @(number), - @"deep" : @{@"field" : [NSString stringWithFormat:@"deep-field-%d", number]} - } - }; -} - -- (void)testNestedFieldsCanBeWrittenWithSet { - NSDictionary *testData = [self testNestedDataNumbered:1]; - - FIRDocumentReference *doc = [self documentRef]; - [self writeDocumentRef:doc data:testData]; - - FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; - XCTAssertEqualObjects(result.data, testData); -} - -- (void)testNestedFieldsCanBeReadDirectly { - NSDictionary *testData = [self testNestedDataNumbered:1]; - - FIRDocumentReference *doc = [self documentRef]; - [self writeDocumentRef:doc data:testData]; - - FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; - XCTAssertEqualObjects(result[@"name"], testData[@"name"]); - XCTAssertEqualObjects(result[@"metadata"], testData[@"metadata"]); - XCTAssertEqualObjects(result[@"metadata.deep.field"], testData[@"metadata"][@"deep"][@"field"]); - XCTAssertNil(result[@"metadata.nofield"]); - XCTAssertNil(result[@"nometadata.nofield"]); -} - -- (void)testNestedFieldsCanBeUpdated { - NSDictionary *testData = [self testNestedDataNumbered:1]; - - FIRDocumentReference *doc = [self documentRef]; - [self writeDocumentRef:doc data:testData]; - [self updateDocumentRef:doc data:@{ @"metadata.deep.field" : @100, @"metadata.added" : @200 }]; - - FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; - XCTAssertEqualObjects( - result.data, (@{ - @"name" : @"room 1", - @"metadata" : @{@"createdAt" : @1, @"deep" : @{@"field" : @100}, @"added" : @200} - })); -} - -- (void)testNestedFieldsCanBeUsedInQueryFilters { - NSDictionary *> *testDocs = @{ - @"1" : [self testNestedDataNumbered:300], - @"2" : [self testNestedDataNumbered:100], - @"3" : [self testNestedDataNumbered:200] - }; - - // inequality adds implicit sort on field - NSArray *> *expected = - @[ [self testNestedDataNumbered:200], [self testNestedDataNumbered:300] ]; - FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; - - FIRQuery *q = [coll queryWhereField:@"metadata.createdAt" isGreaterThanOrEqualTo:@200]; - FIRQuerySnapshot *results = [self readDocumentSetForRef:q]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (expected)); -} - -- (void)testNestedFieldsCanBeUsedInOrderBy { - NSDictionary *> *testDocs = @{ - @"1" : [self testNestedDataNumbered:300], - @"2" : [self testNestedDataNumbered:100], - @"3" : [self testNestedDataNumbered:200] - }; - FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; - - XCTestExpectation *queryCompletion = [self expectationWithDescription:@"query"]; - FIRQuery *q = [coll queryOrderedByField:@"metadata.createdAt"]; - [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) { - XCTAssertNil(error); - XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[ - [self testNestedDataNumbered:100], [self testNestedDataNumbered:200], - [self testNestedDataNumbered:300] - ])); - [queryCompletion fulfill]; - }]; - [self awaitExpectations]; -} - -/** - * Creates test data with special characters in field names. Datastore currently prohibits mixing - * nested data with special characters so tests that use this data must be separate. - */ -- (NSDictionary *)testDottedDataNumbered:(int)number { - return @{ - @"a" : [NSString stringWithFormat:@"field %d", number], - @"b.dot" : @(number), - @"c\\slash" : @(number) - }; -} - -- (void)testFieldsWithSpecialCharsCanBeWrittenWithSet { - NSDictionary *testData = [self testDottedDataNumbered:1]; - - FIRDocumentReference *doc = [self documentRef]; - [self writeDocumentRef:doc data:testData]; - - FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; - XCTAssertEqualObjects(result.data, testData); -} - -- (void)testFieldsWithSpecialCharsCanBeReadDirectly { - NSDictionary *testData = [self testDottedDataNumbered:1]; - - FIRDocumentReference *doc = [self documentRef]; - [self writeDocumentRef:doc data:testData]; - - FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; - XCTAssertEqualObjects(result[@"a"], testData[@"a"]); - XCTAssertEqualObjects(result[[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]]], - testData[@"b.dot"]); - XCTAssertEqualObjects(result[@"c\\slash"], testData[@"c\\slash"]); -} - -- (void)testFieldsWithSpecialCharsCanBeUpdated { - NSDictionary *testData = [self testDottedDataNumbered:1]; - - FIRDocumentReference *doc = [self documentRef]; - [self writeDocumentRef:doc data:testData]; - [self updateDocumentRef:doc - data:@{ - [[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]] : @100, - @"c\\slash" : @200 - }]; - - FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; - XCTAssertEqualObjects(result.data, (@{ @"a" : @"field 1", @"b.dot" : @100, @"c\\slash" : @200 })); -} - -- (void)testFieldsWithSpecialCharsCanBeUsedInQueryFilters { - NSDictionary *> *testDocs = @{ - @"1" : [self testDottedDataNumbered:300], - @"2" : [self testDottedDataNumbered:100], - @"3" : [self testDottedDataNumbered:200] - }; - - // inequality adds implicit sort on field - NSArray *> *expected = - @[ [self testDottedDataNumbered:200], [self testDottedDataNumbered:300] ]; - FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; - - XCTestExpectation *queryCompletion = [self expectationWithDescription:@"query"]; - FIRQuery *q = [coll queryWhereFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]] - isGreaterThanOrEqualTo:@200]; - [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) { - XCTAssertNil(error); - XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected); - [queryCompletion fulfill]; - }]; - - [self awaitExpectations]; -} - -- (void)testFieldsWithSpecialCharsCanBeUsedInOrderBy { - NSDictionary *> *testDocs = @{ - @"1" : [self testDottedDataNumbered:300], - @"2" : [self testDottedDataNumbered:100], - @"3" : [self testDottedDataNumbered:200] - }; - - NSArray *> *expected = @[ - [self testDottedDataNumbered:100], [self testDottedDataNumbered:200], - [self testDottedDataNumbered:300] - ]; - FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; - - FIRQuery *q = [coll queryOrderedByFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]]]; - XCTestExpectation *queryDot = [self expectationWithDescription:@"query dot"]; - [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) { - XCTAssertNil(error); - XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected); - [queryDot fulfill]; - }]; - [self awaitExpectations]; - - XCTestExpectation *querySlash = [self expectationWithDescription:@"query slash"]; - q = [coll queryOrderedByField:@"c\\slash"]; - [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) { - XCTAssertNil(error); - XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected); - [querySlash fulfill]; - }]; - [self awaitExpectations]; -} - -@end diff --git a/Firestore/Example/Tests/Integration/API/FIRFieldsTests.mm b/Firestore/Example/Tests/Integration/API/FIRFieldsTests.mm new file mode 100644 index 0000000..34bd87e --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRFieldsTests.mm @@ -0,0 +1,223 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import + +#import "Firestore/Source/Core/FSTFirestoreClient.h" + +#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" + +@interface FIRFieldsTests : FSTIntegrationTestCase +@end + +@implementation FIRFieldsTests + +- (NSDictionary *)testNestedDataNumbered:(int)number { + return @{ + @"name" : [NSString stringWithFormat:@"room %d", number], + @"metadata" : @{ + @"createdAt" : @(number), + @"deep" : @{@"field" : [NSString stringWithFormat:@"deep-field-%d", number]} + } + }; +} + +- (void)testNestedFieldsCanBeWrittenWithSet { + NSDictionary *testData = [self testNestedDataNumbered:1]; + + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:testData]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(result.data, testData); +} + +- (void)testNestedFieldsCanBeReadDirectly { + NSDictionary *testData = [self testNestedDataNumbered:1]; + + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:testData]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(result[@"name"], testData[@"name"]); + XCTAssertEqualObjects(result[@"metadata"], testData[@"metadata"]); + XCTAssertEqualObjects(result[@"metadata.deep.field"], testData[@"metadata"][@"deep"][@"field"]); + XCTAssertNil(result[@"metadata.nofield"]); + XCTAssertNil(result[@"nometadata.nofield"]); +} + +- (void)testNestedFieldsCanBeUpdated { + NSDictionary *testData = [self testNestedDataNumbered:1]; + + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:testData]; + [self updateDocumentRef:doc data:@{ @"metadata.deep.field" : @100, @"metadata.added" : @200 }]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects( + result.data, (@{ + @"name" : @"room 1", + @"metadata" : @{@"createdAt" : @1, @"deep" : @{@"field" : @100}, @"added" : @200} + })); +} + +- (void)testNestedFieldsCanBeUsedInQueryFilters { + NSDictionary *> *testDocs = @{ + @"1" : [self testNestedDataNumbered:300], + @"2" : [self testNestedDataNumbered:100], + @"3" : [self testNestedDataNumbered:200] + }; + + // inequality adds implicit sort on field + NSArray *> *expected = + @[ [self testNestedDataNumbered:200], [self testNestedDataNumbered:300] ]; + FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; + + FIRQuery *q = [coll queryWhereField:@"metadata.createdAt" isGreaterThanOrEqualTo:@200]; + FIRQuerySnapshot *results = [self readDocumentSetForRef:q]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (expected)); +} + +- (void)testNestedFieldsCanBeUsedInOrderBy { + NSDictionary *> *testDocs = @{ + @"1" : [self testNestedDataNumbered:300], + @"2" : [self testNestedDataNumbered:100], + @"3" : [self testNestedDataNumbered:200] + }; + FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; + + XCTestExpectation *queryCompletion = [self expectationWithDescription:@"query"]; + FIRQuery *q = [coll queryOrderedByField:@"metadata.createdAt"]; + [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[ + [self testNestedDataNumbered:100], [self testNestedDataNumbered:200], + [self testNestedDataNumbered:300] + ])); + [queryCompletion fulfill]; + }]; + [self awaitExpectations]; +} + +/** + * Creates test data with special characters in field names. Datastore currently prohibits mixing + * nested data with special characters so tests that use this data must be separate. + */ +- (NSDictionary *)testDottedDataNumbered:(int)number { + return @{ + @"a" : [NSString stringWithFormat:@"field %d", number], + @"b.dot" : @(number), + @"c\\slash" : @(number) + }; +} + +- (void)testFieldsWithSpecialCharsCanBeWrittenWithSet { + NSDictionary *testData = [self testDottedDataNumbered:1]; + + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:testData]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(result.data, testData); +} + +- (void)testFieldsWithSpecialCharsCanBeReadDirectly { + NSDictionary *testData = [self testDottedDataNumbered:1]; + + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:testData]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(result[@"a"], testData[@"a"]); + XCTAssertEqualObjects(result[[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]]], + testData[@"b.dot"]); + XCTAssertEqualObjects(result[@"c\\slash"], testData[@"c\\slash"]); +} + +- (void)testFieldsWithSpecialCharsCanBeUpdated { + NSDictionary *testData = [self testDottedDataNumbered:1]; + + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:testData]; + [self updateDocumentRef:doc + data:@{ + [[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]] : @100, + @"c\\slash" : @200 + }]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(result.data, (@{ @"a" : @"field 1", @"b.dot" : @100, @"c\\slash" : @200 })); +} + +- (void)testFieldsWithSpecialCharsCanBeUsedInQueryFilters { + NSDictionary *> *testDocs = @{ + @"1" : [self testDottedDataNumbered:300], + @"2" : [self testDottedDataNumbered:100], + @"3" : [self testDottedDataNumbered:200] + }; + + // inequality adds implicit sort on field + NSArray *> *expected = + @[ [self testDottedDataNumbered:200], [self testDottedDataNumbered:300] ]; + FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; + + XCTestExpectation *queryCompletion = [self expectationWithDescription:@"query"]; + FIRQuery *q = [coll queryWhereFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]] + isGreaterThanOrEqualTo:@200]; + [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected); + [queryCompletion fulfill]; + }]; + + [self awaitExpectations]; +} + +- (void)testFieldsWithSpecialCharsCanBeUsedInOrderBy { + NSDictionary *> *testDocs = @{ + @"1" : [self testDottedDataNumbered:300], + @"2" : [self testDottedDataNumbered:100], + @"3" : [self testDottedDataNumbered:200] + }; + + NSArray *> *expected = @[ + [self testDottedDataNumbered:100], [self testDottedDataNumbered:200], + [self testDottedDataNumbered:300] + ]; + FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; + + FIRQuery *q = [coll queryOrderedByFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]]]; + XCTestExpectation *queryDot = [self expectationWithDescription:@"query dot"]; + [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected); + [queryDot fulfill]; + }]; + [self awaitExpectations]; + + XCTestExpectation *querySlash = [self expectationWithDescription:@"query slash"]; + q = [coll queryOrderedByField:@"c\\slash"]; + [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected); + [querySlash fulfill]; + }]; + [self awaitExpectations]; +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m b/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m deleted file mode 100644 index 52d73b1..0000000 --- a/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 FirebaseFirestore; - -#import - -#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/Core/FSTFirestoreClient.h" - -@interface FIRListenerRegistrationTests : FSTIntegrationTestCase -@end - -@implementation FIRListenerRegistrationTests - -- (void)testCanBeRemoved { - FIRCollectionReference *collectionRef = [self collectionRef]; - FIRDocumentReference *docRef = [collectionRef documentWithAutoID]; - - __block int callbacks = 0; - id one = [collectionRef - addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) { - XCTAssertNil(error); - callbacks++; - }]; - - id two = [collectionRef - addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) { - XCTAssertNil(error); - callbacks++; - }]; - - // Wait for initial events - [self waitUntil:^BOOL { - return callbacks == 2; - }]; - - // Trigger new events - [self writeDocumentRef:docRef data:@{@"foo" : @"bar"}]; - - // Write events should have triggered - XCTAssertEqual(4, callbacks); - - // No more events should occur - [one remove]; - [two remove]; - - [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}]; - - // Assert no further events occurred - XCTAssertEqual(4, callbacks); -} - -- (void)testCanBeRemovedTwice { - FIRCollectionReference *collectionRef = [self collectionRef]; - FIRDocumentReference *docRef = [collectionRef documentWithAutoID]; - - id one = [collectionRef - addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error){ - }]; - id two = [docRef - addSnapshotListener:^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable error){ - }]; - - [one remove]; - [one remove]; - - [two remove]; - [two remove]; -} - -- (void)testCanBeRemovedIndependently { - FIRCollectionReference *collectionRef = [self collectionRef]; - FIRDocumentReference *docRef = [collectionRef documentWithAutoID]; - - __block int callbacksOne = 0; - __block int callbacksTwo = 0; - id one = [collectionRef - addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) { - XCTAssertNil(error); - callbacksOne++; - }]; - - id two = [collectionRef - addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) { - XCTAssertNil(error); - callbacksTwo++; - }]; - - // Wait for initial events - [self waitUntil:^BOOL { - return callbacksOne == 1 && callbacksTwo == 1; - }]; - - // Trigger new events - [self writeDocumentRef:docRef data:@{@"foo" : @"bar"}]; - - // Write events should have triggered - XCTAssertEqual(2, callbacksOne); - XCTAssertEqual(2, callbacksTwo); - - // Should leave "two" unaffected - [one remove]; - - [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}]; - - // Assert only events for "two" actually occurred - XCTAssertEqual(2, callbacksOne); - XCTAssertEqual(3, callbacksTwo); - - [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}]; - - // No more events should occur - [two remove]; -} - -@end diff --git a/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.mm b/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.mm new file mode 100644 index 0000000..036ab32 --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.mm @@ -0,0 +1,131 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import + +#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/Core/FSTFirestoreClient.h" + +@interface FIRListenerRegistrationTests : FSTIntegrationTestCase +@end + +@implementation FIRListenerRegistrationTests + +- (void)testCanBeRemoved { + FIRCollectionReference *collectionRef = [self collectionRef]; + FIRDocumentReference *docRef = [collectionRef documentWithAutoID]; + + __block int callbacks = 0; + id one = [collectionRef + addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) { + XCTAssertNil(error); + callbacks++; + }]; + + id two = [collectionRef + addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) { + XCTAssertNil(error); + callbacks++; + }]; + + // Wait for initial events + [self waitUntil:^BOOL { + return callbacks == 2; + }]; + + // Trigger new events + [self writeDocumentRef:docRef data:@{@"foo" : @"bar"}]; + + // Write events should have triggered + XCTAssertEqual(4, callbacks); + + // No more events should occur + [one remove]; + [two remove]; + + [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}]; + + // Assert no further events occurred + XCTAssertEqual(4, callbacks); +} + +- (void)testCanBeRemovedTwice { + FIRCollectionReference *collectionRef = [self collectionRef]; + FIRDocumentReference *docRef = [collectionRef documentWithAutoID]; + + id one = [collectionRef + addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error){ + }]; + id two = [docRef + addSnapshotListener:^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable error){ + }]; + + [one remove]; + [one remove]; + + [two remove]; + [two remove]; +} + +- (void)testCanBeRemovedIndependently { + FIRCollectionReference *collectionRef = [self collectionRef]; + FIRDocumentReference *docRef = [collectionRef documentWithAutoID]; + + __block int callbacksOne = 0; + __block int callbacksTwo = 0; + id one = [collectionRef + addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) { + XCTAssertNil(error); + callbacksOne++; + }]; + + id two = [collectionRef + addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) { + XCTAssertNil(error); + callbacksTwo++; + }]; + + // Wait for initial events + [self waitUntil:^BOOL { + return callbacksOne == 1 && callbacksTwo == 1; + }]; + + // Trigger new events + [self writeDocumentRef:docRef data:@{@"foo" : @"bar"}]; + + // Write events should have triggered + XCTAssertEqual(2, callbacksOne); + XCTAssertEqual(2, callbacksTwo); + + // Should leave "two" unaffected + [one remove]; + + [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}]; + + // Assert only events for "two" actually occurred + XCTAssertEqual(2, callbacksOne); + XCTAssertEqual(3, callbacksTwo); + + [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}]; + + // No more events should occur + [two remove]; +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRQueryTests.m b/Firestore/Example/Tests/Integration/API/FIRQueryTests.m deleted file mode 100644 index 58f57bc..0000000 --- a/Firestore/Example/Tests/Integration/API/FIRQueryTests.m +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 FirebaseFirestore; - -#import - -#import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" -#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/Core/FSTFirestoreClient.h" - -@interface FIRQueryTests : FSTIntegrationTestCase -@end - -@implementation FIRQueryTests - -- (void)testLimitQueries { - FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ - @"a" : @{@"k" : @"a"}, - @"b" : @{@"k" : @"b"}, - @"c" : @{@"k" : @"c"} - - }]; - FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[collRef queryLimitedTo:2]]; - - XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{@"k" : @"a"}, @{@"k" : @"b"} ])); -} - -- (void)testLimitQueriesWithDescendingSortOrder { - FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ - @"a" : @{@"k" : @"a", @"sort" : @0}, - @"b" : @{@"k" : @"b", @"sort" : @1}, - @"c" : @{@"k" : @"c", @"sort" : @1}, - @"d" : @{@"k" : @"d", @"sort" : @2}, - - }]; - FIRQuerySnapshot *snapshot = - [self readDocumentSetForRef:[[collRef queryOrderedByField:@"sort" descending:YES] - queryLimitedTo:2]]; - - XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ - @{ @"k" : @"d", - @"sort" : @2 }, - @{ @"k" : @"c", - @"sort" : @1 } - ])); -} - -- (void)testKeyOrderIsDescendingForDescendingInequality { - FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ - @"a" : @{@"foo" : @42}, - @"b" : @{@"foo" : @42.0}, - @"c" : @{@"foo" : @42}, - @"d" : @{@"foo" : @21}, - @"e" : @{@"foo" : @21.0}, - @"f" : @{@"foo" : @66}, - @"g" : @{@"foo" : @66.0}, - }]; - FIRQuerySnapshot *snapshot = - [self readDocumentSetForRef:[[collRef queryWhereField:@"foo" isGreaterThan:@21] - queryOrderedByField:@"foo" - descending:YES]]; - XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(snapshot), (@[ @"g", @"f", @"c", @"b", @"a" ])); -} - -- (void)testUnaryFilterQueries { - FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ - @"a" : @{@"null" : [NSNull null], @"nan" : @(NAN)}, - @"b" : @{@"null" : [NSNull null], @"nan" : @0}, - @"c" : @{@"null" : @NO, @"nan" : @(NAN)} - }]; - - FIRQuerySnapshot *results = - [self readDocumentSetForRef:[[collRef queryWhereField:@"null" isEqualTo:[NSNull null]] - queryWhereField:@"nan" - isEqualTo:@(NAN)]]; - - XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[ - @{ @"null" : [NSNull null], - @"nan" : @(NAN) } - ])); -} - -- (void)testQueryWithFieldPaths { - FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ - @"a" : @{@"a" : @1}, - @"b" : @{@"a" : @2}, - @"c" : @{@"a" : @3} - }]; - - FIRQuery *query = - [collRef queryWhereFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"a" ]] isLessThan:@3]; - query = [query queryOrderedByFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"a" ]] - descending:YES]; - - FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:query]; - - XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(snapshot), (@[ @"b", @"a" ])); -} - -- (void)testQueryWithPredicate { - FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ - @"a" : @{@"a" : @1}, - @"b" : @{@"a" : @2}, - @"c" : @{@"a" : @3} - }]; - - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"a < 3"]; - FIRQuery *query = [collRef queryFilteredUsingPredicate:predicate]; - query = [query queryOrderedByFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"a" ]] - descending:YES]; - - FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:query]; - - XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(snapshot), (@[ @"b", @"a" ])); -} - -- (void)testFilterOnInfinity { - FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ - @"a" : @{@"inf" : @(INFINITY)}, - @"b" : @{@"inf" : @(-INFINITY)} - }]; - - FIRQuerySnapshot *results = - [self readDocumentSetForRef:[collRef queryWhereField:@"inf" isEqualTo:@(INFINITY)]]; - - XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[ @{ @"inf" : @(INFINITY) } ])); -} - -- (void)testCanExplicitlySortByDocumentID { - NSDictionary *testDocs = @{ - @"a" : @{@"key" : @"a"}, - @"b" : @{@"key" : @"b"}, - @"c" : @{@"key" : @"c"}, - }; - FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; - - // Ideally this would be descending to validate it's different than - // the default, but that requires an extra index - FIRQuerySnapshot *docs = - [self readDocumentSetForRef:[collection queryOrderedByFieldPath:[FIRFieldPath documentID]]]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), - (@[ testDocs[@"a"], testDocs[@"b"], testDocs[@"c"] ])); -} - -- (void)testCanQueryByDocumentID { - NSDictionary *testDocs = @{ - @"aa" : @{@"key" : @"aa"}, - @"ab" : @{@"key" : @"ab"}, - @"ba" : @{@"key" : @"ba"}, - @"bb" : @{@"key" : @"bb"}, - }; - FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; - FIRQuerySnapshot *docs = - [self readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID] - isEqualTo:@"ab"]]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ])); -} - -- (void)testCanQueryByDocumentIDs { - NSDictionary *testDocs = @{ - @"aa" : @{@"key" : @"aa"}, - @"ab" : @{@"key" : @"ab"}, - @"ba" : @{@"key" : @"ba"}, - @"bb" : @{@"key" : @"bb"}, - }; - FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; - FIRQuerySnapshot *docs = - [self readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID] - isEqualTo:@"ab"]]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ])); - - docs = [self readDocumentSetForRef:[[collection queryWhereFieldPath:[FIRFieldPath documentID] - isGreaterThan:@"aa"] - queryWhereFieldPath:[FIRFieldPath documentID] - isLessThanOrEqualTo:@"ba"]]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"], testDocs[@"ba"] ])); -} - -- (void)testCanQueryByDocumentIDsUsingRefs { - NSDictionary *testDocs = @{ - @"aa" : @{@"key" : @"aa"}, - @"ab" : @{@"key" : @"ab"}, - @"ba" : @{@"key" : @"ba"}, - @"bb" : @{@"key" : @"bb"}, - }; - FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; - FIRQuerySnapshot *docs = [self - readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID] - isEqualTo:[collection documentWithPath:@"ab"]]]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ])); - - docs = [self - readDocumentSetForRef:[[collection queryWhereFieldPath:[FIRFieldPath documentID] - isGreaterThan:[collection documentWithPath:@"aa"]] - queryWhereFieldPath:[FIRFieldPath documentID] - isLessThanOrEqualTo:[collection documentWithPath:@"ba"]]]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"], testDocs[@"ba"] ])); -} - -- (void)testWatchSurvivesNetworkDisconnect { - XCTestExpectation *testExpectiation = - [self expectationWithDescription:@"testWatchSurvivesNetworkDisconnect"]; - - FIRCollectionReference *collectionRef = [self collectionRef]; - FIRDocumentReference *docRef = [collectionRef documentWithAutoID]; - - FIRFirestore *firestore = collectionRef.firestore; - - FIRQueryListenOptions *options = [[[FIRQueryListenOptions options] - includeDocumentMetadataChanges:YES] includeQueryMetadataChanges:YES]; - - [collectionRef addSnapshotListenerWithOptions:options - listener:^(FIRQuerySnapshot *snapshot, NSError *error) { - XCTAssertNil(error); - if (!snapshot.empty && !snapshot.metadata.fromCache) { - [testExpectiation fulfill]; - } - }]; - - [firestore disableNetworkWithCompletion:^(NSError *error) { - XCTAssertNil(error); - [docRef setData:@{@"foo" : @"bar"}]; - [firestore enableNetworkWithCompletion:^(NSError *error) { - XCTAssertNil(error); - }]; - }]; - - [self awaitExpectations]; -} - -- (void)testQueriesFireFromCacheWhenOffline { - NSDictionary *testDocs = @{ - @"a" : @{@"foo" : @1}, - }; - FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; - - FIRQueryListenOptions *options = [[[FIRQueryListenOptions options] - includeDocumentMetadataChanges:YES] includeQueryMetadataChanges:YES]; - id registration = - [collection addSnapshotListenerWithOptions:options - listener:self.eventAccumulator.valueEventHandler]; - - FIRQuerySnapshot *querySnap = [self.eventAccumulator awaitEventWithName:@"initial event"]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnap), @[ @{ @"foo" : @1 } ]); - XCTAssertEqual(querySnap.metadata.isFromCache, NO); - - [self disableNetwork]; - querySnap = [self.eventAccumulator awaitEventWithName:@"offline event with isFromCache=YES"]; - XCTAssertEqual(querySnap.metadata.isFromCache, YES); - - // TODO(b/70631617): There's currently a backend bug that prevents us from using a resume token - // right away (against hexa at least). So we sleep. :-( :-( Anything over ~10ms seems to be - // sufficient. - [NSThread sleepForTimeInterval:0.2f]; - - [self enableNetwork]; - querySnap = [self.eventAccumulator awaitEventWithName:@"back online event with isFromCache=NO"]; - XCTAssertEqual(querySnap.metadata.isFromCache, NO); - - [registration remove]; -} - -- (void)testCanHaveMultipleMutationsWhileOffline { - FIRCollectionReference *col = [self collectionRef]; - - // set a few docs to known values - NSDictionary *initialDocs = - @{ @"doc1" : @{@"key1" : @"value1"}, - @"doc2" : @{@"key2" : @"value2"} }; - [self writeAllDocuments:initialDocs toCollection:col]; - - // go offline for the rest of this test - [self disableNetwork]; - - // apply *multiple* mutations while offline - [[col documentWithPath:@"doc1"] setData:@{@"key1b" : @"value1b"}]; - [[col documentWithPath:@"doc2"] setData:@{@"key2b" : @"value2b"}]; - - FIRQuerySnapshot *result = [self readDocumentSetForRef:col]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(result), (@[ - @{@"key1b" : @"value1b"}, - @{@"key2b" : @"value2b"}, - ])); -} - -@end diff --git a/Firestore/Example/Tests/Integration/API/FIRQueryTests.mm b/Firestore/Example/Tests/Integration/API/FIRQueryTests.mm new file mode 100644 index 0000000..32d746e --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRQueryTests.mm @@ -0,0 +1,301 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import + +#import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" +#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/Core/FSTFirestoreClient.h" + +@interface FIRQueryTests : FSTIntegrationTestCase +@end + +@implementation FIRQueryTests + +- (void)testLimitQueries { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"k" : @"a"}, + @"b" : @{@"k" : @"b"}, + @"c" : @{@"k" : @"c"} + + }]; + FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[collRef queryLimitedTo:2]]; + + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{@"k" : @"a"}, @{@"k" : @"b"} ])); +} + +- (void)testLimitQueriesWithDescendingSortOrder { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"k" : @"a", @"sort" : @0}, + @"b" : @{@"k" : @"b", @"sort" : @1}, + @"c" : @{@"k" : @"c", @"sort" : @1}, + @"d" : @{@"k" : @"d", @"sort" : @2}, + + }]; + FIRQuerySnapshot *snapshot = + [self readDocumentSetForRef:[[collRef queryOrderedByField:@"sort" descending:YES] + queryLimitedTo:2]]; + + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ + @{ @"k" : @"d", + @"sort" : @2 }, + @{ @"k" : @"c", + @"sort" : @1 } + ])); +} + +- (void)testKeyOrderIsDescendingForDescendingInequality { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"foo" : @42}, + @"b" : @{@"foo" : @42.0}, + @"c" : @{@"foo" : @42}, + @"d" : @{@"foo" : @21}, + @"e" : @{@"foo" : @21.0}, + @"f" : @{@"foo" : @66}, + @"g" : @{@"foo" : @66.0}, + }]; + FIRQuerySnapshot *snapshot = + [self readDocumentSetForRef:[[collRef queryWhereField:@"foo" isGreaterThan:@21] + queryOrderedByField:@"foo" + descending:YES]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(snapshot), (@[ @"g", @"f", @"c", @"b", @"a" ])); +} + +- (void)testUnaryFilterQueries { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"null" : [NSNull null], @"nan" : @(NAN)}, + @"b" : @{@"null" : [NSNull null], @"nan" : @0}, + @"c" : @{@"null" : @NO, @"nan" : @(NAN)} + }]; + + FIRQuerySnapshot *results = + [self readDocumentSetForRef:[[collRef queryWhereField:@"null" isEqualTo:[NSNull null]] + queryWhereField:@"nan" + isEqualTo:@(NAN)]]; + + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[ + @{ @"null" : [NSNull null], + @"nan" : @(NAN) } + ])); +} + +- (void)testQueryWithFieldPaths { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"a" : @1}, + @"b" : @{@"a" : @2}, + @"c" : @{@"a" : @3} + }]; + + FIRQuery *query = + [collRef queryWhereFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"a" ]] isLessThan:@3]; + query = [query queryOrderedByFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"a" ]] + descending:YES]; + + FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:query]; + + XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(snapshot), (@[ @"b", @"a" ])); +} + +- (void)testQueryWithPredicate { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"a" : @1}, + @"b" : @{@"a" : @2}, + @"c" : @{@"a" : @3} + }]; + + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"a < 3"]; + FIRQuery *query = [collRef queryFilteredUsingPredicate:predicate]; + query = [query queryOrderedByFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"a" ]] + descending:YES]; + + FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:query]; + + XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(snapshot), (@[ @"b", @"a" ])); +} + +- (void)testFilterOnInfinity { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"inf" : @(INFINITY)}, + @"b" : @{@"inf" : @(-INFINITY)} + }]; + + FIRQuerySnapshot *results = + [self readDocumentSetForRef:[collRef queryWhereField:@"inf" isEqualTo:@(INFINITY)]]; + + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[ @{ @"inf" : @(INFINITY) } ])); +} + +- (void)testCanExplicitlySortByDocumentID { + NSDictionary *testDocs = @{ + @"a" : @{@"key" : @"a"}, + @"b" : @{@"key" : @"b"}, + @"c" : @{@"key" : @"c"}, + }; + FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; + + // Ideally this would be descending to validate it's different than + // the default, but that requires an extra index + FIRQuerySnapshot *docs = + [self readDocumentSetForRef:[collection queryOrderedByFieldPath:[FIRFieldPath documentID]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), + (@[ testDocs[@"a"], testDocs[@"b"], testDocs[@"c"] ])); +} + +- (void)testCanQueryByDocumentID { + NSDictionary *testDocs = @{ + @"aa" : @{@"key" : @"aa"}, + @"ab" : @{@"key" : @"ab"}, + @"ba" : @{@"key" : @"ba"}, + @"bb" : @{@"key" : @"bb"}, + }; + FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; + FIRQuerySnapshot *docs = + [self readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID] + isEqualTo:@"ab"]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ])); +} + +- (void)testCanQueryByDocumentIDs { + NSDictionary *testDocs = @{ + @"aa" : @{@"key" : @"aa"}, + @"ab" : @{@"key" : @"ab"}, + @"ba" : @{@"key" : @"ba"}, + @"bb" : @{@"key" : @"bb"}, + }; + FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; + FIRQuerySnapshot *docs = + [self readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID] + isEqualTo:@"ab"]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ])); + + docs = [self readDocumentSetForRef:[[collection queryWhereFieldPath:[FIRFieldPath documentID] + isGreaterThan:@"aa"] + queryWhereFieldPath:[FIRFieldPath documentID] + isLessThanOrEqualTo:@"ba"]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"], testDocs[@"ba"] ])); +} + +- (void)testCanQueryByDocumentIDsUsingRefs { + NSDictionary *testDocs = @{ + @"aa" : @{@"key" : @"aa"}, + @"ab" : @{@"key" : @"ab"}, + @"ba" : @{@"key" : @"ba"}, + @"bb" : @{@"key" : @"bb"}, + }; + FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; + FIRQuerySnapshot *docs = [self + readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID] + isEqualTo:[collection documentWithPath:@"ab"]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ])); + + docs = [self + readDocumentSetForRef:[[collection queryWhereFieldPath:[FIRFieldPath documentID] + isGreaterThan:[collection documentWithPath:@"aa"]] + queryWhereFieldPath:[FIRFieldPath documentID] + isLessThanOrEqualTo:[collection documentWithPath:@"ba"]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"], testDocs[@"ba"] ])); +} + +- (void)testWatchSurvivesNetworkDisconnect { + XCTestExpectation *testExpectiation = + [self expectationWithDescription:@"testWatchSurvivesNetworkDisconnect"]; + + FIRCollectionReference *collectionRef = [self collectionRef]; + FIRDocumentReference *docRef = [collectionRef documentWithAutoID]; + + FIRFirestore *firestore = collectionRef.firestore; + + FIRQueryListenOptions *options = [[[FIRQueryListenOptions options] + includeDocumentMetadataChanges:YES] includeQueryMetadataChanges:YES]; + + [collectionRef addSnapshotListenerWithOptions:options + listener:^(FIRQuerySnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + if (!snapshot.empty && !snapshot.metadata.fromCache) { + [testExpectiation fulfill]; + } + }]; + + [firestore disableNetworkWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [docRef setData:@{@"foo" : @"bar"}]; + [firestore enableNetworkWithCompletion:^(NSError *error) { + XCTAssertNil(error); + }]; + }]; + + [self awaitExpectations]; +} + +- (void)testQueriesFireFromCacheWhenOffline { + NSDictionary *testDocs = @{ + @"a" : @{@"foo" : @1}, + }; + FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; + + FIRQueryListenOptions *options = [[[FIRQueryListenOptions options] + includeDocumentMetadataChanges:YES] includeQueryMetadataChanges:YES]; + id registration = + [collection addSnapshotListenerWithOptions:options + listener:self.eventAccumulator.valueEventHandler]; + + FIRQuerySnapshot *querySnap = [self.eventAccumulator awaitEventWithName:@"initial event"]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnap), @[ @{ @"foo" : @1 } ]); + XCTAssertEqual(querySnap.metadata.isFromCache, NO); + + [self disableNetwork]; + querySnap = [self.eventAccumulator awaitEventWithName:@"offline event with isFromCache=YES"]; + XCTAssertEqual(querySnap.metadata.isFromCache, YES); + + // TODO(b/70631617): There's currently a backend bug that prevents us from using a resume token + // right away (against hexa at least). So we sleep. :-( :-( Anything over ~10ms seems to be + // sufficient. + [NSThread sleepForTimeInterval:0.2f]; + + [self enableNetwork]; + querySnap = [self.eventAccumulator awaitEventWithName:@"back online event with isFromCache=NO"]; + XCTAssertEqual(querySnap.metadata.isFromCache, NO); + + [registration remove]; +} + +- (void)testCanHaveMultipleMutationsWhileOffline { + FIRCollectionReference *col = [self collectionRef]; + + // set a few docs to known values + NSDictionary *initialDocs = + @{ @"doc1" : @{@"key1" : @"value1"}, + @"doc2" : @{@"key2" : @"value2"} }; + [self writeAllDocuments:initialDocs toCollection:col]; + + // go offline for the rest of this test + [self disableNetwork]; + + // apply *multiple* mutations while offline + [[col documentWithPath:@"doc1"] setData:@{@"key1b" : @"value1b"}]; + [[col documentWithPath:@"doc2"] setData:@{@"key2b" : @"value2b"}]; + + FIRQuerySnapshot *result = [self readDocumentSetForRef:col]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(result), (@[ + @{@"key1b" : @"value1b"}, + @{@"key2b" : @"value2b"}, + ])); +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m deleted file mode 100644 index cc0ab29..0000000 --- a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m +++ /dev/null @@ -1,318 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 FirebaseFirestore; - -#import - -#import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" -#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/Core/FSTFirestoreClient.h" - -@interface FIRServerTimestampTests : FSTIntegrationTestCase -@end - -@implementation FIRServerTimestampTests { - // Data written in tests via set. - NSDictionary *_setData; - - // Base and update data used for update tests. - NSDictionary *_initialData; - NSDictionary *_updateData; - - // A document reference to read and write to. - FIRDocumentReference *_docRef; - - // Accumulator used to capture events during the test. - FSTEventAccumulator *_accumulator; - - // Listener registration for a listener maintained during the course of the test. - id _listenerRegistration; - - // Snapshot options that return the previous value for pending server timestamps. - FIRSnapshotOptions *_returnPreviousValue; - FIRSnapshotOptions *_returnEstimatedValue; -} - -- (void)setUp { - [super setUp]; - - _returnPreviousValue = - [FIRSnapshotOptions serverTimestampBehavior:FIRServerTimestampBehaviorPrevious]; - _returnEstimatedValue = - [FIRSnapshotOptions serverTimestampBehavior:FIRServerTimestampBehaviorEstimate]; - - // Data written in tests via set. - _setData = @{ - @"a" : @42, - @"when" : [FIRFieldValue fieldValueForServerTimestamp], - @"deep" : @{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} - }; - - // Base and update data used for update tests. - _initialData = @{ @"a" : @42 }; - _updateData = @{ - @"when" : [FIRFieldValue fieldValueForServerTimestamp], - @"deep" : @{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} - }; - - _docRef = [self documentRef]; - _accumulator = [FSTEventAccumulator accumulatorForTest:self]; - _listenerRegistration = [_docRef addSnapshotListener:_accumulator.valueEventHandler]; - - // Wait for initial nil snapshot to avoid potential races. - FIRDocumentSnapshot *initialSnapshot = [_accumulator awaitEventWithName:@"initial event"]; - XCTAssertFalse(initialSnapshot.exists); -} - -- (void)tearDown { - [_listenerRegistration remove]; - - [super tearDown]; -} - -#pragma mark - Test Helpers - -/** Returns the expected data, with the specified timestamp substituted in. */ -- (NSDictionary *)expectedDataWithTimestamp:(nullable id)timestamp { - return @{ @"a" : @42, @"when" : timestamp, @"deep" : @{@"when" : timestamp} }; -} - -/** Writes _initialData and waits for the corresponding snapshot. */ -- (void)writeInitialData { - [self writeDocumentRef:_docRef data:_initialData]; - FIRDocumentSnapshot *initialDataSnap = [_accumulator awaitEventWithName:@"Initial data event."]; - XCTAssertEqualObjects(initialDataSnap.data, _initialData); -} - -/** Waits for a snapshot with local writes. */ -- (FIRDocumentSnapshot *)waitForLocalEvent { - FIRDocumentSnapshot *snapshot; - do { - snapshot = [_accumulator awaitEventWithName:@"Local event."]; - } while (!snapshot.metadata.hasPendingWrites); - return snapshot; -} - -/** Waits for a snapshot that has no pending writes */ -- (FIRDocumentSnapshot *)waitForRemoteEvent { - FIRDocumentSnapshot *snapshot; - do { - snapshot = [_accumulator awaitEventWithName:@"Remote event."]; - } while (snapshot.metadata.hasPendingWrites); - return snapshot; -} - -/** Verifies a snapshot containing _setData but with NSNull for the timestamps. */ -- (void)verifyTimestampsAreNullInSnapshot:(FIRDocumentSnapshot *)snapshot { - XCTAssertEqualObjects(snapshot.data, [self expectedDataWithTimestamp:[NSNull null]]); -} - -/** Verifies a snapshot containing _setData but with a local estimate for the timestamps. */ -- (void)verifyTimestampsAreEstimatedInSnapshot:(FIRDocumentSnapshot *)snapshot { - id timestamp = [snapshot valueForField:@"when" options:_returnEstimatedValue]; - XCTAssertTrue([timestamp isKindOfClass:[NSDate class]]); - XCTAssertEqualObjects([snapshot dataWithOptions:_returnEstimatedValue], - [self expectedDataWithTimestamp:timestamp]); -} - -/** - * Verifies a snapshot containing _setData but using the previous field value for server - * timestamps. - */ -- (void)verifyTimestampsInSnapshot:(FIRDocumentSnapshot *)snapshot - fromPreviousSnapshot:(nullable FIRDocumentSnapshot *)previousSnapshot { - if (previousSnapshot == nil) { - XCTAssertEqualObjects([snapshot dataWithOptions:_returnPreviousValue], - [self expectedDataWithTimestamp:[NSNull null]]); - } else { - XCTAssertEqualObjects([snapshot dataWithOptions:_returnPreviousValue], - [self expectedDataWithTimestamp:previousSnapshot[@"when"]]); - } -} - -/** Verifies a snapshot containing _setData but with resolved server timestamps. */ -- (void)verifySnapshotWithResolvedTimestamps:(FIRDocumentSnapshot *)snapshot { - XCTAssertTrue(snapshot.exists); - NSDate *when = snapshot[@"when"]; - XCTAssertTrue([when isKindOfClass:[NSDate class]]); - // Tolerate up to 10 seconds of clock skew between client and server. - XCTAssertEqualWithAccuracy(when.timeIntervalSinceNow, 0, 10); - - // Validate the rest of the document. - XCTAssertEqualObjects(snapshot.data, [self expectedDataWithTimestamp:when]); -} - -/** Runs a transaction block. */ -- (void)runTransactionBlock:(void (^)(FIRTransaction *transaction))transactionBlock { - XCTestExpectation *expectation = [self expectationWithDescription:@"transaction complete"]; - [_docRef.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { - transactionBlock(transaction); - return nil; - } - completion:^(id result, NSError *error) { - XCTAssertNil(error); - [expectation fulfill]; - }]; - [self awaitExpectations]; -} - -#pragma mark - Test Cases - -- (void)testServerTimestampsWorkViaSet { - [self writeDocumentRef:_docRef data:_setData]; - [self verifyTimestampsAreNullInSnapshot:[self waitForLocalEvent]]; - [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; -} - -- (void)testServerTimestampsWorkViaUpdate { - [self writeInitialData]; - [self updateDocumentRef:_docRef data:_updateData]; - [self verifyTimestampsAreNullInSnapshot:[self waitForLocalEvent]]; - [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; -} - -- (void)testServerTimestampsWithEstimatedValue { - [self writeDocumentRef:_docRef data:_setData]; - [self verifyTimestampsAreEstimatedInSnapshot:[self waitForLocalEvent]]; - [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; -} - -- (void)testServerTimestampsWithPreviousValue { - [self writeDocumentRef:_docRef data:_setData]; - [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:nil]; - FIRDocumentSnapshot *remoteSnapshot = [self waitForRemoteEvent]; - - [_docRef updateData:_updateData]; - [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:remoteSnapshot]; - - [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; -} - -- (void)testServerTimestampsWithPreviousValueOfDifferentType { - [self writeDocumentRef:_docRef data:_setData]; - [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:nil]; - [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; - - [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; - FIRDocumentSnapshot *localSnapshot = [self waitForLocalEvent]; - XCTAssertEqualObjects([localSnapshot valueForField:@"a"], [NSNull null]); - XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_returnPreviousValue], @42); - XCTAssertTrue([[localSnapshot valueForField:@"a" options:_returnEstimatedValue] - isKindOfClass:[NSDate class]]); - - FIRDocumentSnapshot *remoteSnapshot = [self waitForRemoteEvent]; - XCTAssertTrue([[remoteSnapshot valueForField:@"a"] isKindOfClass:[NSDate class]]); - XCTAssertTrue([[remoteSnapshot valueForField:@"a" options:_returnPreviousValue] - isKindOfClass:[NSDate class]]); - XCTAssertTrue([[remoteSnapshot valueForField:@"a" options:_returnEstimatedValue] - isKindOfClass:[NSDate class]]); -} - -- (void)testServerTimestampsWithConsecutiveUpdates { - [self writeDocumentRef:_docRef data:_setData]; - [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:nil]; - [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; - - [self disableNetwork]; - - [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; - FIRDocumentSnapshot *localSnapshot = [self waitForLocalEvent]; - XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_returnPreviousValue], @42); - - [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; - localSnapshot = [self waitForLocalEvent]; - XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_returnPreviousValue], @42); - - [self enableNetwork]; - - FIRDocumentSnapshot *remoteSnapshot = [self waitForRemoteEvent]; - XCTAssertTrue([[remoteSnapshot valueForField:@"a"] isKindOfClass:[NSDate class]]); -} - -- (void)testServerTimestampsPreviousValueFromLocalMutation { - [self writeDocumentRef:_docRef data:_setData]; - [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:nil]; - [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; - - [self disableNetwork]; - - [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; - FIRDocumentSnapshot *localSnapshot = [self waitForLocalEvent]; - XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_returnPreviousValue], @42); - - [_docRef updateData:@{ @"a" : @1337 }]; - localSnapshot = [self waitForLocalEvent]; - XCTAssertEqualObjects([localSnapshot valueForField:@"a"], @1337); - - [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; - localSnapshot = [self waitForLocalEvent]; - XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_returnPreviousValue], @1337); - - [self enableNetwork]; - - FIRDocumentSnapshot *remoteSnapshot = [self waitForRemoteEvent]; - XCTAssertTrue([[remoteSnapshot valueForField:@"a"] isKindOfClass:[NSDate class]]); -} - -- (void)testServerTimestampsWorkViaTransactionSet { - [self runTransactionBlock:^(FIRTransaction *transaction) { - [transaction setData:_setData forDocument:_docRef]; - }]; - - [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; -} - -- (void)testServerTimestampsWorkViaTransactionUpdate { - [self writeInitialData]; - [self runTransactionBlock:^(FIRTransaction *transaction) { - [transaction updateData:_updateData forDocument:_docRef]; - }]; - [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; -} - -- (void)testServerTimestampsFailViaUpdateOnNonexistentDocument { - XCTestExpectation *expectation = [self expectationWithDescription:@"update complete"]; - [_docRef updateData:_updateData - completion:^(NSError *error) { - XCTAssertNotNil(error); - XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); - XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound); - [expectation fulfill]; - }]; - [self awaitExpectations]; -} - -- (void)testServerTimestampsFailViaTransactionUpdateOnNonexistentDocument { - XCTestExpectation *expectation = [self expectationWithDescription:@"transaction complete"]; - [_docRef.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { - [transaction updateData:_updateData forDocument:_docRef]; - return nil; - } - completion:^(id result, NSError *error) { - XCTAssertNotNil(error); - XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); - // TODO(b/35201829): This should be NotFound, but right now we retry transactions on any - // error and so this turns into Aborted instead. - // TODO(mikelehen): Actually it's FailedPrecondition, unlike Android. What do we want??? - XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition); - [expectation fulfill]; - }]; - [self awaitExpectations]; -} - -@end diff --git a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm new file mode 100644 index 0000000..916ce7e --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm @@ -0,0 +1,318 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import + +#import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" +#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/Core/FSTFirestoreClient.h" + +@interface FIRServerTimestampTests : FSTIntegrationTestCase +@end + +@implementation FIRServerTimestampTests { + // Data written in tests via set. + NSDictionary *_setData; + + // Base and update data used for update tests. + NSDictionary *_initialData; + NSDictionary *_updateData; + + // A document reference to read and write to. + FIRDocumentReference *_docRef; + + // Accumulator used to capture events during the test. + FSTEventAccumulator *_accumulator; + + // Listener registration for a listener maintained during the course of the test. + id _listenerRegistration; + + // Snapshot options that return the previous value for pending server timestamps. + FIRSnapshotOptions *_returnPreviousValue; + FIRSnapshotOptions *_returnEstimatedValue; +} + +- (void)setUp { + [super setUp]; + + _returnPreviousValue = + [FIRSnapshotOptions serverTimestampBehavior:FIRServerTimestampBehaviorPrevious]; + _returnEstimatedValue = + [FIRSnapshotOptions serverTimestampBehavior:FIRServerTimestampBehaviorEstimate]; + + // Data written in tests via set. + _setData = @{ + @"a" : @42, + @"when" : [FIRFieldValue fieldValueForServerTimestamp], + @"deep" : @{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} + }; + + // Base and update data used for update tests. + _initialData = @{ @"a" : @42 }; + _updateData = @{ + @"when" : [FIRFieldValue fieldValueForServerTimestamp], + @"deep" : @{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} + }; + + _docRef = [self documentRef]; + _accumulator = [FSTEventAccumulator accumulatorForTest:self]; + _listenerRegistration = [_docRef addSnapshotListener:_accumulator.valueEventHandler]; + + // Wait for initial nil snapshot to avoid potential races. + FIRDocumentSnapshot *initialSnapshot = [_accumulator awaitEventWithName:@"initial event"]; + XCTAssertFalse(initialSnapshot.exists); +} + +- (void)tearDown { + [_listenerRegistration remove]; + + [super tearDown]; +} + +#pragma mark - Test Helpers + +/** Returns the expected data, with the specified timestamp substituted in. */ +- (NSDictionary *)expectedDataWithTimestamp:(nullable id)timestamp { + return @{ @"a" : @42, @"when" : timestamp, @"deep" : @{@"when" : timestamp} }; +} + +/** Writes _initialData and waits for the corresponding snapshot. */ +- (void)writeInitialData { + [self writeDocumentRef:_docRef data:_initialData]; + FIRDocumentSnapshot *initialDataSnap = [_accumulator awaitEventWithName:@"Initial data event."]; + XCTAssertEqualObjects(initialDataSnap.data, _initialData); +} + +/** Waits for a snapshot with local writes. */ +- (FIRDocumentSnapshot *)waitForLocalEvent { + FIRDocumentSnapshot *snapshot; + do { + snapshot = [_accumulator awaitEventWithName:@"Local event."]; + } while (!snapshot.metadata.hasPendingWrites); + return snapshot; +} + +/** Waits for a snapshot that has no pending writes */ +- (FIRDocumentSnapshot *)waitForRemoteEvent { + FIRDocumentSnapshot *snapshot; + do { + snapshot = [_accumulator awaitEventWithName:@"Remote event."]; + } while (snapshot.metadata.hasPendingWrites); + return snapshot; +} + +/** Verifies a snapshot containing _setData but with NSNull for the timestamps. */ +- (void)verifyTimestampsAreNullInSnapshot:(FIRDocumentSnapshot *)snapshot { + XCTAssertEqualObjects(snapshot.data, [self expectedDataWithTimestamp:[NSNull null]]); +} + +/** Verifies a snapshot containing _setData but with a local estimate for the timestamps. */ +- (void)verifyTimestampsAreEstimatedInSnapshot:(FIRDocumentSnapshot *)snapshot { + id timestamp = [snapshot valueForField:@"when" options:_returnEstimatedValue]; + XCTAssertTrue([timestamp isKindOfClass:[NSDate class]]); + XCTAssertEqualObjects([snapshot dataWithOptions:_returnEstimatedValue], + [self expectedDataWithTimestamp:timestamp]); +} + +/** + * Verifies a snapshot containing _setData but using the previous field value for server + * timestamps. + */ +- (void)verifyTimestampsInSnapshot:(FIRDocumentSnapshot *)snapshot + fromPreviousSnapshot:(nullable FIRDocumentSnapshot *)previousSnapshot { + if (previousSnapshot == nil) { + XCTAssertEqualObjects([snapshot dataWithOptions:_returnPreviousValue], + [self expectedDataWithTimestamp:[NSNull null]]); + } else { + XCTAssertEqualObjects([snapshot dataWithOptions:_returnPreviousValue], + [self expectedDataWithTimestamp:previousSnapshot[@"when"]]); + } +} + +/** Verifies a snapshot containing _setData but with resolved server timestamps. */ +- (void)verifySnapshotWithResolvedTimestamps:(FIRDocumentSnapshot *)snapshot { + XCTAssertTrue(snapshot.exists); + NSDate *when = snapshot[@"when"]; + XCTAssertTrue([when isKindOfClass:[NSDate class]]); + // Tolerate up to 10 seconds of clock skew between client and server. + XCTAssertEqualWithAccuracy(when.timeIntervalSinceNow, 0, 10); + + // Validate the rest of the document. + XCTAssertEqualObjects(snapshot.data, [self expectedDataWithTimestamp:when]); +} + +/** Runs a transaction block. */ +- (void)runTransactionBlock:(void (^)(FIRTransaction *transaction))transactionBlock { + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction complete"]; + [_docRef.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { + transactionBlock(transaction); + return nil; + } + completion:^(id result, NSError *error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +#pragma mark - Test Cases + +- (void)testServerTimestampsWorkViaSet { + [self writeDocumentRef:_docRef data:_setData]; + [self verifyTimestampsAreNullInSnapshot:[self waitForLocalEvent]]; + [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; +} + +- (void)testServerTimestampsWorkViaUpdate { + [self writeInitialData]; + [self updateDocumentRef:_docRef data:_updateData]; + [self verifyTimestampsAreNullInSnapshot:[self waitForLocalEvent]]; + [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; +} + +- (void)testServerTimestampsWithEstimatedValue { + [self writeDocumentRef:_docRef data:_setData]; + [self verifyTimestampsAreEstimatedInSnapshot:[self waitForLocalEvent]]; + [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; +} + +- (void)testServerTimestampsWithPreviousValue { + [self writeDocumentRef:_docRef data:_setData]; + [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:nil]; + FIRDocumentSnapshot *remoteSnapshot = [self waitForRemoteEvent]; + + [_docRef updateData:_updateData]; + [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:remoteSnapshot]; + + [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; +} + +- (void)testServerTimestampsWithPreviousValueOfDifferentType { + [self writeDocumentRef:_docRef data:_setData]; + [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:nil]; + [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; + + [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; + FIRDocumentSnapshot *localSnapshot = [self waitForLocalEvent]; + XCTAssertEqualObjects([localSnapshot valueForField:@"a"], [NSNull null]); + XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_returnPreviousValue], @42); + XCTAssertTrue([[localSnapshot valueForField:@"a" options:_returnEstimatedValue] + isKindOfClass:[NSDate class]]); + + FIRDocumentSnapshot *remoteSnapshot = [self waitForRemoteEvent]; + XCTAssertTrue([[remoteSnapshot valueForField:@"a"] isKindOfClass:[NSDate class]]); + XCTAssertTrue([[remoteSnapshot valueForField:@"a" options:_returnPreviousValue] + isKindOfClass:[NSDate class]]); + XCTAssertTrue([[remoteSnapshot valueForField:@"a" options:_returnEstimatedValue] + isKindOfClass:[NSDate class]]); +} + +- (void)testServerTimestampsWithConsecutiveUpdates { + [self writeDocumentRef:_docRef data:_setData]; + [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:nil]; + [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; + + [self disableNetwork]; + + [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; + FIRDocumentSnapshot *localSnapshot = [self waitForLocalEvent]; + XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_returnPreviousValue], @42); + + [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; + localSnapshot = [self waitForLocalEvent]; + XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_returnPreviousValue], @42); + + [self enableNetwork]; + + FIRDocumentSnapshot *remoteSnapshot = [self waitForRemoteEvent]; + XCTAssertTrue([[remoteSnapshot valueForField:@"a"] isKindOfClass:[NSDate class]]); +} + +- (void)testServerTimestampsPreviousValueFromLocalMutation { + [self writeDocumentRef:_docRef data:_setData]; + [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:nil]; + [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; + + [self disableNetwork]; + + [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; + FIRDocumentSnapshot *localSnapshot = [self waitForLocalEvent]; + XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_returnPreviousValue], @42); + + [_docRef updateData:@{ @"a" : @1337 }]; + localSnapshot = [self waitForLocalEvent]; + XCTAssertEqualObjects([localSnapshot valueForField:@"a"], @1337); + + [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; + localSnapshot = [self waitForLocalEvent]; + XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_returnPreviousValue], @1337); + + [self enableNetwork]; + + FIRDocumentSnapshot *remoteSnapshot = [self waitForRemoteEvent]; + XCTAssertTrue([[remoteSnapshot valueForField:@"a"] isKindOfClass:[NSDate class]]); +} + +- (void)testServerTimestampsWorkViaTransactionSet { + [self runTransactionBlock:^(FIRTransaction *transaction) { + [transaction setData:_setData forDocument:_docRef]; + }]; + + [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; +} + +- (void)testServerTimestampsWorkViaTransactionUpdate { + [self writeInitialData]; + [self runTransactionBlock:^(FIRTransaction *transaction) { + [transaction updateData:_updateData forDocument:_docRef]; + }]; + [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; +} + +- (void)testServerTimestampsFailViaUpdateOnNonexistentDocument { + XCTestExpectation *expectation = [self expectationWithDescription:@"update complete"]; + [_docRef updateData:_updateData + completion:^(NSError *error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testServerTimestampsFailViaTransactionUpdateOnNonexistentDocument { + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction complete"]; + [_docRef.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { + [transaction updateData:_updateData forDocument:_docRef]; + return nil; + } + completion:^(id result, NSError *error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + // TODO(b/35201829): This should be NotFound, but right now we retry transactions on any + // error and so this turns into Aborted instead. + // TODO(mikelehen): Actually it's FailedPrecondition, unlike Android. What do we want??? + XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRTypeTests.m b/Firestore/Example/Tests/Integration/API/FIRTypeTests.m deleted file mode 100644 index 1874f00..0000000 --- a/Firestore/Example/Tests/Integration/API/FIRTypeTests.m +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 FirebaseFirestore; - -#import - -#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" - -@interface FIRTypeTests : FSTIntegrationTestCase -@end - -@implementation FIRTypeTests - -- (void)assertSuccessfulRoundtrip:(NSDictionary *)data { - FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"]; - - [self writeDocumentRef:doc data:data]; - FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; - XCTAssertTrue(document.exists); - XCTAssertEqualObjects(document.data, data); -} - -- (void)testCanReadAndWriteNullFields { - [self assertSuccessfulRoundtrip:@{ @"a" : @1, @"b" : [NSNull null] }]; -} - -- (void)testCanReadAndWriteArrayFields { - [self assertSuccessfulRoundtrip:@{ - @"array" : @[ @1, @"foo", - @{ @"deep" : @YES }, [NSNull null] ] - }]; -} - -- (void)testCanReadAndWriteBlobFields { - NSData *data = [NSData dataWithBytes:"\0\1\2" length:3]; - [self assertSuccessfulRoundtrip:@{@"blob" : data}]; -} - -- (void)testCanReadAndWriteGeoPointFields { - [self assertSuccessfulRoundtrip:@{ - @"geoPoint" : [[FIRGeoPoint alloc] initWithLatitude:1.23 longitude:4.56] - }]; -} - -- (void)testCanReadAndWriteTimestampFields { - // Choose a value that can be converted losslessly between fixed point and double - NSDate *timestamp = [NSDate dateWithTimeIntervalSince1970:1491847082.125]; - [self assertSuccessfulRoundtrip:@{@"timestamp" : timestamp}]; -} - -- (void)testCanReadAndWriteDocumentReferences { - FIRDocumentReference *docRef = [self documentRef]; - [self assertSuccessfulRoundtrip:@{ @"a" : @42, @"ref" : docRef }]; -} - -- (void)testCanReadAndWriteDocumentReferencesInArrays { - FIRDocumentReference *docRef = [self documentRef]; - [self assertSuccessfulRoundtrip:@{ @"a" : @42, @"refs" : @[ docRef ] }]; -} - -@end diff --git a/Firestore/Example/Tests/Integration/API/FIRTypeTests.mm b/Firestore/Example/Tests/Integration/API/FIRTypeTests.mm new file mode 100644 index 0000000..5140b90 --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRTypeTests.mm @@ -0,0 +1,75 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import + +#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" + +@interface FIRTypeTests : FSTIntegrationTestCase +@end + +@implementation FIRTypeTests + +- (void)assertSuccessfulRoundtrip:(NSDictionary *)data { + FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"]; + + [self writeDocumentRef:doc data:data]; + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertTrue(document.exists); + XCTAssertEqualObjects(document.data, data); +} + +- (void)testCanReadAndWriteNullFields { + [self assertSuccessfulRoundtrip:@{ @"a" : @1, @"b" : [NSNull null] }]; +} + +- (void)testCanReadAndWriteArrayFields { + [self assertSuccessfulRoundtrip:@{ + @"array" : @[ @1, @"foo", + @{ @"deep" : @YES }, [NSNull null] ] + }]; +} + +- (void)testCanReadAndWriteBlobFields { + NSData *data = [NSData dataWithBytes:"\0\1\2" length:3]; + [self assertSuccessfulRoundtrip:@{@"blob" : data}]; +} + +- (void)testCanReadAndWriteGeoPointFields { + [self assertSuccessfulRoundtrip:@{ + @"geoPoint" : [[FIRGeoPoint alloc] initWithLatitude:1.23 longitude:4.56] + }]; +} + +- (void)testCanReadAndWriteTimestampFields { + // Choose a value that can be converted losslessly between fixed point and double + NSDate *timestamp = [NSDate dateWithTimeIntervalSince1970:1491847082.125]; + [self assertSuccessfulRoundtrip:@{@"timestamp" : timestamp}]; +} + +- (void)testCanReadAndWriteDocumentReferences { + FIRDocumentReference *docRef = [self documentRef]; + [self assertSuccessfulRoundtrip:@{ @"a" : @42, @"ref" : docRef }]; +} + +- (void)testCanReadAndWriteDocumentReferencesInArrays { + FIRDocumentReference *docRef = [self documentRef]; + [self assertSuccessfulRoundtrip:@{ @"a" : @42, @"refs" : @[ docRef ] }]; +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRValidationTests.m b/Firestore/Example/Tests/Integration/API/FIRValidationTests.m deleted file mode 100644 index 8b760c9..0000000 --- a/Firestore/Example/Tests/Integration/API/FIRValidationTests.m +++ /dev/null @@ -1,615 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 FirebaseFirestore; - -#import - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" -#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" - -// We have tests for passing nil when nil is not supposed to be allowed. So suppress the warnings. -#pragma clang diagnostic ignored "-Wnonnull" - -@interface FIRValidationTests : FSTIntegrationTestCase -@end - -@implementation FIRValidationTests - -#pragma mark - FIRFirestoreSettings Validation - -- (void)testNilHostFails { - FIRFirestoreSettings *settings = self.db.settings; - FSTAssertThrows(settings.host = nil, - @"host setting may not be nil. You should generally just use the default value " - "(which is firestore.googleapis.com)"); -} - -- (void)testNilDispatchQueueFails { - FIRFirestoreSettings *settings = self.db.settings; - FSTAssertThrows(settings.dispatchQueue = nil, - @"dispatch queue setting may not be nil. Create a new dispatch queue with " - "dispatch_queue_create(\"com.example.MyQueue\", NULL) or just use the default " - "(which is the main queue, returned from dispatch_get_main_queue())"); -} - -- (void)testChangingSettingsAfterUseFails { - FIRFirestoreSettings *settings = self.db.settings; - [[self.db documentWithPath:@"foo/bar"] setData:@{ @"a" : @42 }]; - settings.host = @"example.com"; - FSTAssertThrows(self.db.settings = settings, - @"Firestore instance has already been started and its settings can no longer be " - @"changed. You can only set settings before calling any other methods on " - @"a Firestore instance."); -} - -#pragma mark - FIRFirestore Validation - -- (void)testNilFIRAppFails { - FSTAssertThrows( - [FIRFirestore firestoreForApp:nil], - @"FirebaseApp instance may not be nil. Use FirebaseApp.app() if you'd like to use the " - "default FirebaseApp instance."); -} - -// TODO(b/62410906): Test for firestoreForApp:database: with nil DatabaseID. - -- (void)testNilTransactionBlocksFail { - FSTAssertThrows([self.db runTransactionWithBlock:nil - completion:^(id result, NSError *error) { - XCTFail(@"Completion shouldn't run."); - }], - @"Transaction block cannot be nil."); - - FSTAssertThrows( - [self.db runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { - XCTFail(@"Transaction block shouldn't run."); - return nil; - } - completion:nil], - @"Transaction completion block cannot be nil."); -} - -#pragma mark - Collection and Document Path Validation - -- (void)testNilCollectionPathsFail { - FIRDocumentReference *baseDocRef = [self.db documentWithPath:@"foo/bar"]; - NSString *nilError = @"Collection path cannot be nil."; - FSTAssertThrows([self.db collectionWithPath:nil], nilError); - FSTAssertThrows([baseDocRef collectionWithPath:nil], nilError); -} - -- (void)testWrongLengthCollectionPathsFail { - FIRDocumentReference *baseDocRef = [self.db documentWithPath:@"foo/bar"]; - NSArray *badAbsolutePaths = @[ @"foo/bar", @"foo/bar/baz/quu" ]; - NSArray *badRelativePaths = @[ @"", @"baz/quu" ]; - NSArray *badPathLengths = @[ @2, @4 ]; - NSString *errorFormat = - @"Invalid collection reference. Collection references must have an odd " - @"number of segments, but %@ has %@"; - for (NSUInteger i = 0; i < badAbsolutePaths.count; i++) { - NSString *error = - [NSString stringWithFormat:errorFormat, badAbsolutePaths[i], badPathLengths[i]]; - FSTAssertThrows([self.db collectionWithPath:badAbsolutePaths[i]], error); - FSTAssertThrows([baseDocRef collectionWithPath:badRelativePaths[i]], error); - } -} - -- (void)testNilDocumentPathsFail { - FIRCollectionReference *baseCollectionRef = [self.db collectionWithPath:@"foo"]; - NSString *nilError = @"Document path cannot be nil."; - FSTAssertThrows([self.db documentWithPath:nil], nilError); - FSTAssertThrows([baseCollectionRef documentWithPath:nil], nilError); -} - -- (void)testWrongLengthDocumentPathsFail { - FIRCollectionReference *baseCollectionRef = [self.db collectionWithPath:@"foo"]; - NSArray *badAbsolutePaths = @[ @"foo", @"foo/bar/baz" ]; - NSArray *badRelativePaths = @[ @"", @"bar/baz" ]; - NSArray *badPathLengths = @[ @1, @3 ]; - NSString *errorFormat = - @"Invalid document reference. Document references must have an even " - @"number of segments, but %@ has %@"; - for (NSUInteger i = 0; i < badAbsolutePaths.count; i++) { - NSString *error = - [NSString stringWithFormat:errorFormat, badAbsolutePaths[i], badPathLengths[i]]; - FSTAssertThrows([self.db documentWithPath:badAbsolutePaths[i]], error); - FSTAssertThrows([baseCollectionRef documentWithPath:badRelativePaths[i]], error); - } -} - -- (void)testPathsWithEmptySegmentsFail { - // We're only testing using collectionWithPath since the validation happens in FSTPath which is - // shared by all methods that accept paths. - - // leading / trailing slashes are okay. - [self.db collectionWithPath:@"/foo/"]; - [self.db collectionWithPath:@"/foo"]; - [self.db collectionWithPath:@"foo/"]; - - FSTAssertThrows([self.db collectionWithPath:@"foo//bar/baz"], - @"Invalid path (foo//bar/baz). Paths must not contain // in them."); - FSTAssertThrows([self.db collectionWithPath:@"//foo"], - @"Invalid path (//foo). Paths must not contain // in them."); - FSTAssertThrows([self.db collectionWithPath:@"foo//"], - @"Invalid path (foo//). Paths must not contain // in them."); -} - -#pragma mark - Write Validation - -- (void)testWritesWithNonDictionaryValuesFail { - NSArray *badData = @[ - @42, @"test", @[ @1 ], [NSDate date], [NSNull null], [FIRFieldValue fieldValueForDelete], - [FIRFieldValue fieldValueForServerTimestamp] - ]; - - for (id data in badData) { - [self expectWrite:data toFailWithReason:@"Data to be written must be an NSDictionary."]; - } -} - -- (void)testWritesWithDirectlyNestedArraysFail { - [self expectWrite:@{ - @"nested-array" : @[ @1, @[ @2 ] ] - } - toFailWithReason:@"Nested arrays are not supported"]; -} - -- (void)testWritesWithIndirectlyNestedArraysSucceed { - NSDictionary *data = @{ @"nested-array" : @[ @1, @{ @"foo" : @[ @2 ] } ] }; - - FIRDocumentReference *ref = [self documentRef]; - FIRDocumentReference *ref2 = [self documentRef]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"setData"]; - [ref setData:data - completion:^(NSError *_Nullable error) { - XCTAssertNil(error); - [expectation fulfill]; - }]; - [self awaitExpectations]; - - expectation = [self expectationWithDescription:@"batch.setData"]; - [[[ref.firestore batch] setData:data forDocument:ref] - commitWithCompletion:^(NSError *_Nullable error) { - XCTAssertNil(error); - [expectation fulfill]; - }]; - [self awaitExpectations]; - - expectation = [self expectationWithDescription:@"updateData"]; - [ref updateData:data - completion:^(NSError *_Nullable error) { - XCTAssertNil(error); - [expectation fulfill]; - }]; - [self awaitExpectations]; - - expectation = [self expectationWithDescription:@"batch.updateData"]; - [[[ref.firestore batch] updateData:data forDocument:ref] - commitWithCompletion:^(NSError *_Nullable error) { - XCTAssertNil(error); - [expectation fulfill]; - }]; - [self awaitExpectations]; - - XCTestExpectation *transactionDone = [self expectationWithDescription:@"transaction done"]; - [ref.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { - // Note ref2 does not exist at this point so set that and update ref. - [transaction updateData:data forDocument:ref]; - [transaction setData:data forDocument:ref2]; - return nil; - } - completion:^(id result, NSError *error) { - // ends up being a no-op transaction. - XCTAssertNil(error); - [transactionDone fulfill]; - }]; - [self awaitExpectations]; -} - -- (void)testWritesWithInvalidTypesFail { - [self expectWrite:@{ - @"foo" : @{@"bar" : self} - } - toFailWithReason:@"Unsupported type: FIRValidationTests (found in field foo.bar)"]; -} - -- (void)testWritesWithLargeNumbersFail { - NSNumber *num = @((unsigned long long)LONG_MAX + 1); - NSString *reason = - [NSString stringWithFormat:@"NSNumber (%@) is too large (found in field num)", num]; - [self expectWrite:@{@"num" : num} toFailWithReason:reason]; -} - -- (void)testWritesWithReferencesToADifferentDatabaseFail { - FIRDocumentReference *ref = - [[self firestoreWithProjectID:@"different-db"] documentWithPath:@"baz/quu"]; - id data = @{@"foo" : ref}; - [self expectWrite:data - toFailWithReason: - [NSString - stringWithFormat:@"Document Reference is for database different-db/(default) but " - "should be for database %@/(default) (found in field foo)", - [FSTIntegrationTestCase projectID]]]; -} - -- (void)testWritesWithReservedFieldsFail { - [self expectWrite:@{ - @"__baz__" : @1 - } - toFailWithReason:@"Document fields cannot begin and end with __ (found in field __baz__)"]; - [self expectWrite:@{ - @"foo" : @{@"__baz__" : @1} - } - toFailWithReason: - @"Document fields cannot begin and end with __ (found in field foo.__baz__)"]; - [self expectWrite:@{ - @"__baz__" : @{@"foo" : @1} - } - toFailWithReason:@"Document fields cannot begin and end with __ (found in field __baz__)"]; - - [self expectUpdate:@{ - @"foo.__baz__" : @1 - } - toFailWithReason: - @"Document fields cannot begin and end with __ (found in field foo.__baz__)"]; - [self expectUpdate:@{ - @"__baz__.foo" : @1 - } - toFailWithReason: - @"Document fields cannot begin and end with __ (found in field __baz__.foo)"]; - [self expectUpdate:@{ - @1 : @1 - } - toFailWithReason:@"Dictionary keys in updateData: must be NSStrings or FIRFieldPaths."]; -} - -- (void)testSetsWithFieldValueDeleteFail { - [self expectSet:@{@"foo" : [FIRFieldValue fieldValueForDelete]} - toFailWithReason: - @"FieldValue.delete() can only be used with updateData() and setData() with " - @"SetOptions.merge()."]; -} - -- (void)testUpdatesWithNestedFieldValueDeleteFail { - [self expectUpdate:@{ - @"foo" : @{@"bar" : [FIRFieldValue fieldValueForDelete]} - } - toFailWithReason: - @"FieldValue.delete() can only appear at the top level of your update data " - "(found in field foo.bar)"]; -} - -- (void)testBatchWritesWithIncorrectReferencesFail { - FIRFirestore *db1 = [self firestore]; - FIRFirestore *db2 = [self firestore]; - XCTAssertNotEqual(db1, db2); - - NSString *reason = @"Provided document reference is from a different Firestore instance."; - id data = @{ @"foo" : @1 }; - FIRDocumentReference *badRef = [db2 documentWithPath:@"foo/bar"]; - FIRWriteBatch *batch = [db1 batch]; - FSTAssertThrows([batch setData:data forDocument:badRef], reason); - FSTAssertThrows([batch setData:data forDocument:badRef options:[FIRSetOptions merge]], reason); - FSTAssertThrows([batch updateData:data forDocument:badRef], reason); - FSTAssertThrows([batch deleteDocument:badRef], reason); -} - -- (void)testTransactionWritesWithIncorrectReferencesFail { - FIRFirestore *db1 = [self firestore]; - FIRFirestore *db2 = [self firestore]; - XCTAssertNotEqual(db1, db2); - - NSString *reason = @"Provided document reference is from a different Firestore instance."; - id data = @{ @"foo" : @1 }; - FIRDocumentReference *badRef = [db2 documentWithPath:@"foo/bar"]; - - XCTestExpectation *transactionDone = [self expectationWithDescription:@"transaction done"]; - [db1 runTransactionWithBlock:^id(FIRTransaction *txn, NSError **pError) { - FSTAssertThrows([txn getDocument:badRef error:nil], reason); - FSTAssertThrows([txn setData:data forDocument:badRef], reason); - FSTAssertThrows([txn setData:data forDocument:badRef options:[FIRSetOptions merge]], reason); - FSTAssertThrows([txn updateData:data forDocument:badRef], reason); - FSTAssertThrows([txn deleteDocument:badRef], reason); - return nil; - } - completion:^(id result, NSError *error) { - // ends up being a no-op transaction. - XCTAssertNil(error); - [transactionDone fulfill]; - }]; - [self awaitExpectations]; -} - -#pragma mark - Field Path validation -// TODO(b/37244157): More validation for invalid field paths. - -- (void)testFieldPathsWithEmptySegmentsFail { - NSArray *badFieldPaths = @[ @"", @"foo..baz", @".foo", @"foo." ]; - - for (NSString *fieldPath in badFieldPaths) { - NSString *reason = - [NSString stringWithFormat: - @"Invalid field path (%@). Paths must not be empty, begin with " - @"'.', end with '.', or contain '..'", - fieldPath]; - [self expectFieldPath:fieldPath toFailWithReason:reason]; - } -} - -- (void)testFieldPathsWithInvalidSegmentsFail { - NSArray *badFieldPaths = @[ @"foo~bar", @"foo*bar", @"foo/bar", @"foo[1", @"foo]1", @"foo[1]" ]; - - for (NSString *fieldPath in badFieldPaths) { - NSString *reason = - [NSString stringWithFormat: - @"Invalid field path (%@). Paths must not contain '~', '*', '/', '[', or ']'", - fieldPath]; - [self expectFieldPath:fieldPath toFailWithReason:reason]; - } -} - -#pragma mark - Query Validation - -- (void)testQueryWithNonPositiveLimitFails { - FSTAssertThrows([[self collectionRef] queryLimitedTo:0], - @"Invalid Query. Query limit (0) is invalid. Limit must be positive."); - FSTAssertThrows([[self collectionRef] queryLimitedTo:-1], - @"Invalid Query. Query limit (-1) is invalid. Limit must be positive."); -} - -- (void)testQueryInequalityOnNullOrNaNFails { - FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:nil], - @"Invalid Query. You can only perform equality comparisons on nil / NSNull."); - FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:[NSNull null]], - @"Invalid Query. You can only perform equality comparisons on nil / NSNull."); - - FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:@(NAN)], - @"Invalid Query. You can only perform equality comparisons on NaN."); -} - -- (void)testQueryCannotBeCreatedFromDocumentsMissingSortValues { - FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ - @"f" : @{@"v" : @"f", @"nosort" : @1.0} - }]; - - FIRQuery *query = [testCollection queryOrderedByField:@"sort"]; - FIRDocumentSnapshot *snapshot = [self readDocumentForRef:[testCollection documentWithPath:@"f"]]; - XCTAssertTrue(snapshot.exists); - - NSString *reason = - @"Invalid query. You are trying to start or end a query using a document for " - "which the field 'sort' (used as the order by) does not exist."; - FSTAssertThrows([query queryStartingAtDocument:snapshot], reason); - FSTAssertThrows([query queryStartingAfterDocument:snapshot], reason); - FSTAssertThrows([query queryEndingBeforeDocument:snapshot], reason); - FSTAssertThrows([query queryEndingAtDocument:snapshot], reason); -} - -- (void)testQueryBoundMustNotHaveMoreComponentsThanSortOrders { - FIRCollectionReference *testCollection = [self collectionRef]; - FIRQuery *query = [testCollection queryOrderedByField:@"foo"]; - - NSString *reason = - @"Invalid query. You are trying to start or end a query using more values " - "than were specified in the order by."; - // More elements than order by - FSTAssertThrows(([query queryStartingAtValues:@[ @1, @2 ]]), reason); - FSTAssertThrows(([[query queryOrderedByField:@"bar"] queryStartingAtValues:@[ @1, @2, @3 ]]), - reason); -} - -- (void)testQueryOrderedByKeyBoundMustBeAStringWithoutSlashes { - FIRCollectionReference *testCollection = [self collectionRef]; - FIRQuery *query = [testCollection queryOrderedByFieldPath:[FIRFieldPath documentID]]; - FSTAssertThrows([query queryStartingAtValues:@[ @1 ]], - @"Invalid query. Expected a string for the document ID."); - FSTAssertThrows([query queryStartingAtValues:@[ @"foo/bar" ]], - @"Invalid query. Document ID 'foo/bar' contains a slash."); -} - -- (void)testQueryMustNotSpecifyStartingOrEndingPointAfterOrder { - FIRCollectionReference *testCollection = [self collectionRef]; - FIRQuery *query = [testCollection queryOrderedByField:@"foo"]; - NSString *reason = - @"Invalid query. You must not specify a starting point before specifying the order by."; - FSTAssertThrows([[query queryStartingAtValues:@[ @1 ]] queryOrderedByField:@"bar"], reason); - FSTAssertThrows([[query queryStartingAfterValues:@[ @1 ]] queryOrderedByField:@"bar"], reason); - reason = @"Invalid query. You must not specify an ending point before specifying the order by."; - FSTAssertThrows([[query queryEndingAtValues:@[ @1 ]] queryOrderedByField:@"bar"], reason); - FSTAssertThrows([[query queryEndingBeforeValues:@[ @1 ]] queryOrderedByField:@"bar"], reason); -} - -- (void)testQueriesFilteredByDocumentIDMustUseStringsOrDocumentReferences { - FIRCollectionReference *collection = [self collectionRef]; - NSString *reason = - @"Invalid query. When querying by document ID you must provide a valid " - "document ID, but it was an empty string."; - FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@""], reason); - - reason = - @"Invalid query. When querying by document ID you must provide a valid document ID, " - "but 'foo/bar/baz' contains a '/' character."; - FSTAssertThrows( - [collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@"foo/bar/baz"], reason); - - reason = - @"Invalid query. When querying by document ID you must provide a valid string or " - "DocumentReference, but it was of type: __NSCFNumber"; - FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@1], reason); -} - -- (void)testQueryInequalityFieldMustMatchFirstOrderByField { - FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"]; - FIRQuery *base = [coll queryWhereField:@"x" isGreaterThanOrEqualTo:@32]; - - FSTAssertThrows([base queryWhereField:@"y" isLessThan:@"cat"], - @"Invalid Query. All where filters with an inequality (lessThan, " - "lessThanOrEqual, greaterThan, or greaterThanOrEqual) must be on the same " - "field. But you have inequality filters on 'x' and 'y'"); - - NSString *reason = - @"Invalid query. You have a where filter with " - "an inequality (lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) " - "on field 'x' and so you must also use 'x' as your first queryOrderedBy field, " - "but your first queryOrderedBy is currently on field 'y' instead."; - FSTAssertThrows([base queryOrderedByField:@"y"], reason); - FSTAssertThrows([[coll queryOrderedByField:@"y"] queryWhereField:@"x" isGreaterThan:@32], reason); - FSTAssertThrows([[base queryOrderedByField:@"y"] queryOrderedByField:@"x"], reason); - FSTAssertThrows([[[coll queryOrderedByField:@"y"] queryOrderedByField:@"x"] queryWhereField:@"x" - isGreaterThan:@32], - reason); - - XCTAssertNoThrow([base queryWhereField:@"x" isLessThanOrEqualTo:@"cat"], - @"Same inequality fields work"); - - XCTAssertNoThrow([base queryWhereField:@"y" isEqualTo:@"cat"], - @"Inequality and equality on different fields works"); - - XCTAssertNoThrow([base queryOrderedByField:@"x"], @"inequality same as order by works"); - XCTAssertNoThrow([[coll queryOrderedByField:@"x"] queryWhereField:@"x" isGreaterThan:@32], - @"inequality same as order by works"); - XCTAssertNoThrow([[base queryOrderedByField:@"x"] queryOrderedByField:@"y"], - @"inequality same as first order by works."); - XCTAssertNoThrow([[[coll queryOrderedByField:@"x"] queryOrderedByField:@"y"] queryWhereField:@"x" - isGreaterThan:@32], - @"inequality same as first order by works."); -} - -#pragma mark - GeoPoint Validation - -- (void)testInvalidGeoPointParameters { - [self verifyExceptionForInvalidLatitude:NAN]; - [self verifyExceptionForInvalidLatitude:-INFINITY]; - [self verifyExceptionForInvalidLatitude:INFINITY]; - [self verifyExceptionForInvalidLatitude:-90.1]; - [self verifyExceptionForInvalidLatitude:90.1]; - - [self verifyExceptionForInvalidLongitude:NAN]; - [self verifyExceptionForInvalidLongitude:-INFINITY]; - [self verifyExceptionForInvalidLongitude:INFINITY]; - [self verifyExceptionForInvalidLongitude:-180.1]; - [self verifyExceptionForInvalidLongitude:180.1]; -} - -#pragma mark - Helpers - -/** Performs a write using each write API and makes sure it fails with the expected reason. */ -- (void)expectWrite:(id)data toFailWithReason:(NSString *)reason { - [self expectWrite:data toFailWithReason:reason includeSets:YES includeUpdates:YES]; -} - -/** Performs a write using each set API and makes sure it fails with the expected reason. */ -- (void)expectSet:(id)data toFailWithReason:(NSString *)reason { - [self expectWrite:data toFailWithReason:reason includeSets:YES includeUpdates:NO]; -} - -/** Performs a write using each update API and makes sure it fails with the expected reason. */ -- (void)expectUpdate:(id)data toFailWithReason:(NSString *)reason { - [self expectWrite:data toFailWithReason:reason includeSets:NO includeUpdates:YES]; -} - -/** - * Performs a write using each set and/or update API and makes sure it fails with the expected - * reason. - */ -- (void)expectWrite:(id)data - toFailWithReason:(NSString *)reason - includeSets:(BOOL)includeSets - includeUpdates:(BOOL)includeUpdates { - FIRDocumentReference *ref = [self documentRef]; - if (includeSets) { - FSTAssertThrows([ref setData:data], reason, @"for %@", data); - FSTAssertThrows([[ref.firestore batch] setData:data forDocument:ref], reason, @"for %@", data); - } - - if (includeUpdates) { - FSTAssertThrows([ref updateData:data], reason, @"for %@", data); - FSTAssertThrows([[ref.firestore batch] updateData:data forDocument:ref], reason, @"for %@", - data); - } - - XCTestExpectation *transactionDone = [self expectationWithDescription:@"transaction done"]; - [ref.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { - if (includeSets) { - FSTAssertThrows([transaction setData:data forDocument:ref], reason, @"for %@", data); - } - if (includeUpdates) { - FSTAssertThrows([transaction updateData:data forDocument:ref], reason, @"for %@", data); - } - return nil; - } - completion:^(id result, NSError *error) { - // ends up being a no-op transaction. - XCTAssertNil(error); - [transactionDone fulfill]; - }]; - [self awaitExpectations]; -} - -- (void)testFieldNamesMustNotBeEmpty { - NSString *reason = @"Invalid field path. Provided names must not be empty."; - FSTAssertThrows([[FIRFieldPath alloc] initWithFields:@[]], reason); - - reason = @"Invalid field name at index 0. Field names must not be empty."; - FSTAssertThrows([[FIRFieldPath alloc] initWithFields:@[ @"" ]], reason); - - reason = @"Invalid field name at index 1. Field names must not be empty."; - FSTAssertThrows(([[FIRFieldPath alloc] initWithFields:@[ @"foo", @"" ]]), reason); -} - -/** - * Tests a field path with all of our APIs that accept field paths and ensures they fail with the - * specified reason. - */ -- (void)expectFieldPath:(NSString *)fieldPath toFailWithReason:(NSString *)reason { - // Get an arbitrary snapshot we can use for testing. - FIRDocumentReference *docRef = [self documentRef]; - [self writeDocumentRef:docRef data:@{ @"test" : @1 }]; - FIRDocumentSnapshot *snapshot = [self readDocumentForRef:docRef]; - - // Update paths. - NSMutableDictionary *dict = [NSMutableDictionary dictionary]; - dict[fieldPath] = @1; - [self expectUpdate:dict toFailWithReason:reason]; - - // Snapshot fields. - FSTAssertThrows(snapshot[fieldPath], reason); - - // Query filter / order fields. - FIRCollectionReference *collection = [self collectionRef]; - FSTAssertThrows([collection queryWhereField:fieldPath isEqualTo:@1], reason); - // isLessThan, etc. omitted for brevity since the code path is trivially shared. - FSTAssertThrows([collection queryOrderedByField:fieldPath], reason); -} - -- (void)verifyExceptionForInvalidLatitude:(double)latitude { - NSString *reason = [NSString - stringWithFormat:@"GeoPoint requires a latitude value in the range of [-90, 90], but was %f", - latitude]; - FSTAssertThrows([[FIRGeoPoint alloc] initWithLatitude:latitude longitude:0], reason); -} - -- (void)verifyExceptionForInvalidLongitude:(double)longitude { - NSString *reason = - [NSString stringWithFormat: - @"GeoPoint requires a longitude value in the range of [-180, 180], but was %f", - longitude]; - FSTAssertThrows([[FIRGeoPoint alloc] initWithLatitude:0 longitude:longitude], reason); -} - -@end diff --git a/Firestore/Example/Tests/Integration/API/FIRValidationTests.mm b/Firestore/Example/Tests/Integration/API/FIRValidationTests.mm new file mode 100644 index 0000000..49e572a --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRValidationTests.mm @@ -0,0 +1,615 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" +#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" + +// We have tests for passing nil when nil is not supposed to be allowed. So suppress the warnings. +#pragma clang diagnostic ignored "-Wnonnull" + +@interface FIRValidationTests : FSTIntegrationTestCase +@end + +@implementation FIRValidationTests + +#pragma mark - FIRFirestoreSettings Validation + +- (void)testNilHostFails { + FIRFirestoreSettings *settings = self.db.settings; + FSTAssertThrows(settings.host = nil, + @"host setting may not be nil. You should generally just use the default value " + "(which is firestore.googleapis.com)"); +} + +- (void)testNilDispatchQueueFails { + FIRFirestoreSettings *settings = self.db.settings; + FSTAssertThrows(settings.dispatchQueue = nil, + @"dispatch queue setting may not be nil. Create a new dispatch queue with " + "dispatch_queue_create(\"com.example.MyQueue\", NULL) or just use the default " + "(which is the main queue, returned from dispatch_get_main_queue())"); +} + +- (void)testChangingSettingsAfterUseFails { + FIRFirestoreSettings *settings = self.db.settings; + [[self.db documentWithPath:@"foo/bar"] setData:@{ @"a" : @42 }]; + settings.host = @"example.com"; + FSTAssertThrows(self.db.settings = settings, + @"Firestore instance has already been started and its settings can no longer be " + @"changed. You can only set settings before calling any other methods on " + @"a Firestore instance."); +} + +#pragma mark - FIRFirestore Validation + +- (void)testNilFIRAppFails { + FSTAssertThrows( + [FIRFirestore firestoreForApp:nil], + @"FirebaseApp instance may not be nil. Use FirebaseApp.app() if you'd like to use the " + "default FirebaseApp instance."); +} + +// TODO(b/62410906): Test for firestoreForApp:database: with nil DatabaseID. + +- (void)testNilTransactionBlocksFail { + FSTAssertThrows([self.db runTransactionWithBlock:nil + completion:^(id result, NSError *error) { + XCTFail(@"Completion shouldn't run."); + }], + @"Transaction block cannot be nil."); + + FSTAssertThrows( + [self.db runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { + XCTFail(@"Transaction block shouldn't run."); + return nil; + } + completion:nil], + @"Transaction completion block cannot be nil."); +} + +#pragma mark - Collection and Document Path Validation + +- (void)testNilCollectionPathsFail { + FIRDocumentReference *baseDocRef = [self.db documentWithPath:@"foo/bar"]; + NSString *nilError = @"Collection path cannot be nil."; + FSTAssertThrows([self.db collectionWithPath:nil], nilError); + FSTAssertThrows([baseDocRef collectionWithPath:nil], nilError); +} + +- (void)testWrongLengthCollectionPathsFail { + FIRDocumentReference *baseDocRef = [self.db documentWithPath:@"foo/bar"]; + NSArray *badAbsolutePaths = @[ @"foo/bar", @"foo/bar/baz/quu" ]; + NSArray *badRelativePaths = @[ @"", @"baz/quu" ]; + NSArray *badPathLengths = @[ @2, @4 ]; + NSString *errorFormat = + @"Invalid collection reference. Collection references must have an odd " + @"number of segments, but %@ has %@"; + for (NSUInteger i = 0; i < badAbsolutePaths.count; i++) { + NSString *error = + [NSString stringWithFormat:errorFormat, badAbsolutePaths[i], badPathLengths[i]]; + FSTAssertThrows([self.db collectionWithPath:badAbsolutePaths[i]], error); + FSTAssertThrows([baseDocRef collectionWithPath:badRelativePaths[i]], error); + } +} + +- (void)testNilDocumentPathsFail { + FIRCollectionReference *baseCollectionRef = [self.db collectionWithPath:@"foo"]; + NSString *nilError = @"Document path cannot be nil."; + FSTAssertThrows([self.db documentWithPath:nil], nilError); + FSTAssertThrows([baseCollectionRef documentWithPath:nil], nilError); +} + +- (void)testWrongLengthDocumentPathsFail { + FIRCollectionReference *baseCollectionRef = [self.db collectionWithPath:@"foo"]; + NSArray *badAbsolutePaths = @[ @"foo", @"foo/bar/baz" ]; + NSArray *badRelativePaths = @[ @"", @"bar/baz" ]; + NSArray *badPathLengths = @[ @1, @3 ]; + NSString *errorFormat = + @"Invalid document reference. Document references must have an even " + @"number of segments, but %@ has %@"; + for (NSUInteger i = 0; i < badAbsolutePaths.count; i++) { + NSString *error = + [NSString stringWithFormat:errorFormat, badAbsolutePaths[i], badPathLengths[i]]; + FSTAssertThrows([self.db documentWithPath:badAbsolutePaths[i]], error); + FSTAssertThrows([baseCollectionRef documentWithPath:badRelativePaths[i]], error); + } +} + +- (void)testPathsWithEmptySegmentsFail { + // We're only testing using collectionWithPath since the validation happens in FSTPath which is + // shared by all methods that accept paths. + + // leading / trailing slashes are okay. + [self.db collectionWithPath:@"/foo/"]; + [self.db collectionWithPath:@"/foo"]; + [self.db collectionWithPath:@"foo/"]; + + FSTAssertThrows([self.db collectionWithPath:@"foo//bar/baz"], + @"Invalid path (foo//bar/baz). Paths must not contain // in them."); + FSTAssertThrows([self.db collectionWithPath:@"//foo"], + @"Invalid path (//foo). Paths must not contain // in them."); + FSTAssertThrows([self.db collectionWithPath:@"foo//"], + @"Invalid path (foo//). Paths must not contain // in them."); +} + +#pragma mark - Write Validation + +- (void)testWritesWithNonDictionaryValuesFail { + NSArray *badData = @[ + @42, @"test", @[ @1 ], [NSDate date], [NSNull null], [FIRFieldValue fieldValueForDelete], + [FIRFieldValue fieldValueForServerTimestamp] + ]; + + for (id data in badData) { + [self expectWrite:data toFailWithReason:@"Data to be written must be an NSDictionary."]; + } +} + +- (void)testWritesWithDirectlyNestedArraysFail { + [self expectWrite:@{ + @"nested-array" : @[ @1, @[ @2 ] ] + } + toFailWithReason:@"Nested arrays are not supported"]; +} + +- (void)testWritesWithIndirectlyNestedArraysSucceed { + NSDictionary *data = @{ @"nested-array" : @[ @1, @{ @"foo" : @[ @2 ] } ] }; + + FIRDocumentReference *ref = [self documentRef]; + FIRDocumentReference *ref2 = [self documentRef]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"setData"]; + [ref setData:data + completion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + expectation = [self expectationWithDescription:@"batch.setData"]; + [[[ref.firestore batch] setData:data forDocument:ref] + commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + expectation = [self expectationWithDescription:@"updateData"]; + [ref updateData:data + completion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + expectation = [self expectationWithDescription:@"batch.updateData"]; + [[[ref.firestore batch] updateData:data forDocument:ref] + commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + XCTestExpectation *transactionDone = [self expectationWithDescription:@"transaction done"]; + [ref.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { + // Note ref2 does not exist at this point so set that and update ref. + [transaction updateData:data forDocument:ref]; + [transaction setData:data forDocument:ref2]; + return nil; + } + completion:^(id result, NSError *error) { + // ends up being a no-op transaction. + XCTAssertNil(error); + [transactionDone fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testWritesWithInvalidTypesFail { + [self expectWrite:@{ + @"foo" : @{@"bar" : self} + } + toFailWithReason:@"Unsupported type: FIRValidationTests (found in field foo.bar)"]; +} + +- (void)testWritesWithLargeNumbersFail { + NSNumber *num = @((unsigned long long)LONG_MAX + 1); + NSString *reason = + [NSString stringWithFormat:@"NSNumber (%@) is too large (found in field num)", num]; + [self expectWrite:@{@"num" : num} toFailWithReason:reason]; +} + +- (void)testWritesWithReferencesToADifferentDatabaseFail { + FIRDocumentReference *ref = + [[self firestoreWithProjectID:@"different-db"] documentWithPath:@"baz/quu"]; + id data = @{@"foo" : ref}; + [self expectWrite:data + toFailWithReason: + [NSString + stringWithFormat:@"Document Reference is for database different-db/(default) but " + "should be for database %@/(default) (found in field foo)", + [FSTIntegrationTestCase projectID]]]; +} + +- (void)testWritesWithReservedFieldsFail { + [self expectWrite:@{ + @"__baz__" : @1 + } + toFailWithReason:@"Document fields cannot begin and end with __ (found in field __baz__)"]; + [self expectWrite:@{ + @"foo" : @{@"__baz__" : @1} + } + toFailWithReason: + @"Document fields cannot begin and end with __ (found in field foo.__baz__)"]; + [self expectWrite:@{ + @"__baz__" : @{@"foo" : @1} + } + toFailWithReason:@"Document fields cannot begin and end with __ (found in field __baz__)"]; + + [self expectUpdate:@{ + @"foo.__baz__" : @1 + } + toFailWithReason: + @"Document fields cannot begin and end with __ (found in field foo.__baz__)"]; + [self expectUpdate:@{ + @"__baz__.foo" : @1 + } + toFailWithReason: + @"Document fields cannot begin and end with __ (found in field __baz__.foo)"]; + [self expectUpdate:@{ + @1 : @1 + } + toFailWithReason:@"Dictionary keys in updateData: must be NSStrings or FIRFieldPaths."]; +} + +- (void)testSetsWithFieldValueDeleteFail { + [self expectSet:@{@"foo" : [FIRFieldValue fieldValueForDelete]} + toFailWithReason: + @"FieldValue.delete() can only be used with updateData() and setData() with " + @"SetOptions.merge()."]; +} + +- (void)testUpdatesWithNestedFieldValueDeleteFail { + [self expectUpdate:@{ + @"foo" : @{@"bar" : [FIRFieldValue fieldValueForDelete]} + } + toFailWithReason: + @"FieldValue.delete() can only appear at the top level of your update data " + "(found in field foo.bar)"]; +} + +- (void)testBatchWritesWithIncorrectReferencesFail { + FIRFirestore *db1 = [self firestore]; + FIRFirestore *db2 = [self firestore]; + XCTAssertNotEqual(db1, db2); + + NSString *reason = @"Provided document reference is from a different Firestore instance."; + id data = @{ @"foo" : @1 }; + FIRDocumentReference *badRef = [db2 documentWithPath:@"foo/bar"]; + FIRWriteBatch *batch = [db1 batch]; + FSTAssertThrows([batch setData:data forDocument:badRef], reason); + FSTAssertThrows([batch setData:data forDocument:badRef options:[FIRSetOptions merge]], reason); + FSTAssertThrows([batch updateData:data forDocument:badRef], reason); + FSTAssertThrows([batch deleteDocument:badRef], reason); +} + +- (void)testTransactionWritesWithIncorrectReferencesFail { + FIRFirestore *db1 = [self firestore]; + FIRFirestore *db2 = [self firestore]; + XCTAssertNotEqual(db1, db2); + + NSString *reason = @"Provided document reference is from a different Firestore instance."; + id data = @{ @"foo" : @1 }; + FIRDocumentReference *badRef = [db2 documentWithPath:@"foo/bar"]; + + XCTestExpectation *transactionDone = [self expectationWithDescription:@"transaction done"]; + [db1 runTransactionWithBlock:^id(FIRTransaction *txn, NSError **pError) { + FSTAssertThrows([txn getDocument:badRef error:nil], reason); + FSTAssertThrows([txn setData:data forDocument:badRef], reason); + FSTAssertThrows([txn setData:data forDocument:badRef options:[FIRSetOptions merge]], reason); + FSTAssertThrows([txn updateData:data forDocument:badRef], reason); + FSTAssertThrows([txn deleteDocument:badRef], reason); + return nil; + } + completion:^(id result, NSError *error) { + // ends up being a no-op transaction. + XCTAssertNil(error); + [transactionDone fulfill]; + }]; + [self awaitExpectations]; +} + +#pragma mark - Field Path validation +// TODO(b/37244157): More validation for invalid field paths. + +- (void)testFieldPathsWithEmptySegmentsFail { + NSArray *badFieldPaths = @[ @"", @"foo..baz", @".foo", @"foo." ]; + + for (NSString *fieldPath in badFieldPaths) { + NSString *reason = + [NSString stringWithFormat: + @"Invalid field path (%@). Paths must not be empty, begin with " + @"'.', end with '.', or contain '..'", + fieldPath]; + [self expectFieldPath:fieldPath toFailWithReason:reason]; + } +} + +- (void)testFieldPathsWithInvalidSegmentsFail { + NSArray *badFieldPaths = @[ @"foo~bar", @"foo*bar", @"foo/bar", @"foo[1", @"foo]1", @"foo[1]" ]; + + for (NSString *fieldPath in badFieldPaths) { + NSString *reason = + [NSString stringWithFormat: + @"Invalid field path (%@). Paths must not contain '~', '*', '/', '[', or ']'", + fieldPath]; + [self expectFieldPath:fieldPath toFailWithReason:reason]; + } +} + +#pragma mark - Query Validation + +- (void)testQueryWithNonPositiveLimitFails { + FSTAssertThrows([[self collectionRef] queryLimitedTo:0], + @"Invalid Query. Query limit (0) is invalid. Limit must be positive."); + FSTAssertThrows([[self collectionRef] queryLimitedTo:-1], + @"Invalid Query. Query limit (-1) is invalid. Limit must be positive."); +} + +- (void)testQueryInequalityOnNullOrNaNFails { + FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:nil], + @"Invalid Query. You can only perform equality comparisons on nil / NSNull."); + FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:[NSNull null]], + @"Invalid Query. You can only perform equality comparisons on nil / NSNull."); + + FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:@(NAN)], + @"Invalid Query. You can only perform equality comparisons on NaN."); +} + +- (void)testQueryCannotBeCreatedFromDocumentsMissingSortValues { + FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ + @"f" : @{@"v" : @"f", @"nosort" : @1.0} + }]; + + FIRQuery *query = [testCollection queryOrderedByField:@"sort"]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:[testCollection documentWithPath:@"f"]]; + XCTAssertTrue(snapshot.exists); + + NSString *reason = + @"Invalid query. You are trying to start or end a query using a document for " + "which the field 'sort' (used as the order by) does not exist."; + FSTAssertThrows([query queryStartingAtDocument:snapshot], reason); + FSTAssertThrows([query queryStartingAfterDocument:snapshot], reason); + FSTAssertThrows([query queryEndingBeforeDocument:snapshot], reason); + FSTAssertThrows([query queryEndingAtDocument:snapshot], reason); +} + +- (void)testQueryBoundMustNotHaveMoreComponentsThanSortOrders { + FIRCollectionReference *testCollection = [self collectionRef]; + FIRQuery *query = [testCollection queryOrderedByField:@"foo"]; + + NSString *reason = + @"Invalid query. You are trying to start or end a query using more values " + "than were specified in the order by."; + // More elements than order by + FSTAssertThrows(([query queryStartingAtValues:@[ @1, @2 ]]), reason); + FSTAssertThrows(([[query queryOrderedByField:@"bar"] queryStartingAtValues:@[ @1, @2, @3 ]]), + reason); +} + +- (void)testQueryOrderedByKeyBoundMustBeAStringWithoutSlashes { + FIRCollectionReference *testCollection = [self collectionRef]; + FIRQuery *query = [testCollection queryOrderedByFieldPath:[FIRFieldPath documentID]]; + FSTAssertThrows([query queryStartingAtValues:@[ @1 ]], + @"Invalid query. Expected a string for the document ID."); + FSTAssertThrows([query queryStartingAtValues:@[ @"foo/bar" ]], + @"Invalid query. Document ID 'foo/bar' contains a slash."); +} + +- (void)testQueryMustNotSpecifyStartingOrEndingPointAfterOrder { + FIRCollectionReference *testCollection = [self collectionRef]; + FIRQuery *query = [testCollection queryOrderedByField:@"foo"]; + NSString *reason = + @"Invalid query. You must not specify a starting point before specifying the order by."; + FSTAssertThrows([[query queryStartingAtValues:@[ @1 ]] queryOrderedByField:@"bar"], reason); + FSTAssertThrows([[query queryStartingAfterValues:@[ @1 ]] queryOrderedByField:@"bar"], reason); + reason = @"Invalid query. You must not specify an ending point before specifying the order by."; + FSTAssertThrows([[query queryEndingAtValues:@[ @1 ]] queryOrderedByField:@"bar"], reason); + FSTAssertThrows([[query queryEndingBeforeValues:@[ @1 ]] queryOrderedByField:@"bar"], reason); +} + +- (void)testQueriesFilteredByDocumentIDMustUseStringsOrDocumentReferences { + FIRCollectionReference *collection = [self collectionRef]; + NSString *reason = + @"Invalid query. When querying by document ID you must provide a valid " + "document ID, but it was an empty string."; + FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@""], reason); + + reason = + @"Invalid query. When querying by document ID you must provide a valid document ID, " + "but 'foo/bar/baz' contains a '/' character."; + FSTAssertThrows( + [collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@"foo/bar/baz"], reason); + + reason = + @"Invalid query. When querying by document ID you must provide a valid string or " + "DocumentReference, but it was of type: __NSCFNumber"; + FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@1], reason); +} + +- (void)testQueryInequalityFieldMustMatchFirstOrderByField { + FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"]; + FIRQuery *base = [coll queryWhereField:@"x" isGreaterThanOrEqualTo:@32]; + + FSTAssertThrows([base queryWhereField:@"y" isLessThan:@"cat"], + @"Invalid Query. All where filters with an inequality (lessThan, " + "lessThanOrEqual, greaterThan, or greaterThanOrEqual) must be on the same " + "field. But you have inequality filters on 'x' and 'y'"); + + NSString *reason = + @"Invalid query. You have a where filter with " + "an inequality (lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) " + "on field 'x' and so you must also use 'x' as your first queryOrderedBy field, " + "but your first queryOrderedBy is currently on field 'y' instead."; + FSTAssertThrows([base queryOrderedByField:@"y"], reason); + FSTAssertThrows([[coll queryOrderedByField:@"y"] queryWhereField:@"x" isGreaterThan:@32], reason); + FSTAssertThrows([[base queryOrderedByField:@"y"] queryOrderedByField:@"x"], reason); + FSTAssertThrows([[[coll queryOrderedByField:@"y"] queryOrderedByField:@"x"] queryWhereField:@"x" + isGreaterThan:@32], + reason); + + XCTAssertNoThrow([base queryWhereField:@"x" isLessThanOrEqualTo:@"cat"], + @"Same inequality fields work"); + + XCTAssertNoThrow([base queryWhereField:@"y" isEqualTo:@"cat"], + @"Inequality and equality on different fields works"); + + XCTAssertNoThrow([base queryOrderedByField:@"x"], @"inequality same as order by works"); + XCTAssertNoThrow([[coll queryOrderedByField:@"x"] queryWhereField:@"x" isGreaterThan:@32], + @"inequality same as order by works"); + XCTAssertNoThrow([[base queryOrderedByField:@"x"] queryOrderedByField:@"y"], + @"inequality same as first order by works."); + XCTAssertNoThrow([[[coll queryOrderedByField:@"x"] queryOrderedByField:@"y"] queryWhereField:@"x" + isGreaterThan:@32], + @"inequality same as first order by works."); +} + +#pragma mark - GeoPoint Validation + +- (void)testInvalidGeoPointParameters { + [self verifyExceptionForInvalidLatitude:NAN]; + [self verifyExceptionForInvalidLatitude:-INFINITY]; + [self verifyExceptionForInvalidLatitude:INFINITY]; + [self verifyExceptionForInvalidLatitude:-90.1]; + [self verifyExceptionForInvalidLatitude:90.1]; + + [self verifyExceptionForInvalidLongitude:NAN]; + [self verifyExceptionForInvalidLongitude:-INFINITY]; + [self verifyExceptionForInvalidLongitude:INFINITY]; + [self verifyExceptionForInvalidLongitude:-180.1]; + [self verifyExceptionForInvalidLongitude:180.1]; +} + +#pragma mark - Helpers + +/** Performs a write using each write API and makes sure it fails with the expected reason. */ +- (void)expectWrite:(id)data toFailWithReason:(NSString *)reason { + [self expectWrite:data toFailWithReason:reason includeSets:YES includeUpdates:YES]; +} + +/** Performs a write using each set API and makes sure it fails with the expected reason. */ +- (void)expectSet:(id)data toFailWithReason:(NSString *)reason { + [self expectWrite:data toFailWithReason:reason includeSets:YES includeUpdates:NO]; +} + +/** Performs a write using each update API and makes sure it fails with the expected reason. */ +- (void)expectUpdate:(id)data toFailWithReason:(NSString *)reason { + [self expectWrite:data toFailWithReason:reason includeSets:NO includeUpdates:YES]; +} + +/** + * Performs a write using each set and/or update API and makes sure it fails with the expected + * reason. + */ +- (void)expectWrite:(id)data + toFailWithReason:(NSString *)reason + includeSets:(BOOL)includeSets + includeUpdates:(BOOL)includeUpdates { + FIRDocumentReference *ref = [self documentRef]; + if (includeSets) { + FSTAssertThrows([ref setData:data], reason, @"for %@", data); + FSTAssertThrows([[ref.firestore batch] setData:data forDocument:ref], reason, @"for %@", data); + } + + if (includeUpdates) { + FSTAssertThrows([ref updateData:data], reason, @"for %@", data); + FSTAssertThrows([[ref.firestore batch] updateData:data forDocument:ref], reason, @"for %@", + data); + } + + XCTestExpectation *transactionDone = [self expectationWithDescription:@"transaction done"]; + [ref.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { + if (includeSets) { + FSTAssertThrows([transaction setData:data forDocument:ref], reason, @"for %@", data); + } + if (includeUpdates) { + FSTAssertThrows([transaction updateData:data forDocument:ref], reason, @"for %@", data); + } + return nil; + } + completion:^(id result, NSError *error) { + // ends up being a no-op transaction. + XCTAssertNil(error); + [transactionDone fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testFieldNamesMustNotBeEmpty { + NSString *reason = @"Invalid field path. Provided names must not be empty."; + FSTAssertThrows([[FIRFieldPath alloc] initWithFields:@[]], reason); + + reason = @"Invalid field name at index 0. Field names must not be empty."; + FSTAssertThrows([[FIRFieldPath alloc] initWithFields:@[ @"" ]], reason); + + reason = @"Invalid field name at index 1. Field names must not be empty."; + FSTAssertThrows(([[FIRFieldPath alloc] initWithFields:@[ @"foo", @"" ]]), reason); +} + +/** + * Tests a field path with all of our APIs that accept field paths and ensures they fail with the + * specified reason. + */ +- (void)expectFieldPath:(NSString *)fieldPath toFailWithReason:(NSString *)reason { + // Get an arbitrary snapshot we can use for testing. + FIRDocumentReference *docRef = [self documentRef]; + [self writeDocumentRef:docRef data:@{ @"test" : @1 }]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:docRef]; + + // Update paths. + NSMutableDictionary *dict = [NSMutableDictionary dictionary]; + dict[fieldPath] = @1; + [self expectUpdate:dict toFailWithReason:reason]; + + // Snapshot fields. + FSTAssertThrows(snapshot[fieldPath], reason); + + // Query filter / order fields. + FIRCollectionReference *collection = [self collectionRef]; + FSTAssertThrows([collection queryWhereField:fieldPath isEqualTo:@1], reason); + // isLessThan, etc. omitted for brevity since the code path is trivially shared. + FSTAssertThrows([collection queryOrderedByField:fieldPath], reason); +} + +- (void)verifyExceptionForInvalidLatitude:(double)latitude { + NSString *reason = [NSString + stringWithFormat:@"GeoPoint requires a latitude value in the range of [-90, 90], but was %f", + latitude]; + FSTAssertThrows([[FIRGeoPoint alloc] initWithLatitude:latitude longitude:0], reason); +} + +- (void)verifyExceptionForInvalidLongitude:(double)longitude { + NSString *reason = + [NSString stringWithFormat: + @"GeoPoint requires a longitude value in the range of [-180, 180], but was %f", + longitude]; + FSTAssertThrows([[FIRGeoPoint alloc] initWithLatitude:0 longitude:longitude], reason); +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m deleted file mode 100644 index 5e7f6d7..0000000 --- a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m +++ /dev/null @@ -1,336 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 FirebaseFirestore; - -#import - -#import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" -#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" - -@interface FIRWriteBatchTests : FSTIntegrationTestCase -@end - -@implementation FIRWriteBatchTests - -- (void)testSupportEmptyBatches { - XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; - [[[self firestore] batch] commitWithCompletion:^(NSError *error) { - XCTAssertNil(error); - [expectation fulfill]; - }]; - [self awaitExpectations]; -} - -- (void)testCommitWithoutCompletionHandler { - FIRDocumentReference *doc = [self documentRef]; - FIRWriteBatch *batch1 = [doc.firestore batch]; - [batch1 setData:@{@"aa" : @"bb"} forDocument:doc]; - [batch1 commitWithCompletion:nil]; - FIRDocumentSnapshot *snapshot1 = [self readDocumentForRef:doc]; - XCTAssertTrue(snapshot1.exists); - XCTAssertEqualObjects(snapshot1.data, @{@"aa" : @"bb"}); - - FIRWriteBatch *batch2 = [doc.firestore batch]; - [batch2 setData:@{@"cc" : @"dd"} forDocument:doc]; - [batch2 commit]; - - // TODO(b/70631617): There's currently a backend bug that prevents us from using a resume token - // right away (against hexa at least). So we sleep. :-( :-( Anything over ~10ms seems to be - // sufficient. - [NSThread sleepForTimeInterval:0.2f]; - - FIRDocumentSnapshot *snapshot2 = [self readDocumentForRef:doc]; - XCTAssertTrue(snapshot2.exists); - XCTAssertEqualObjects(snapshot2.data, @{@"cc" : @"dd"}); -} - -- (void)testSetDocuments { - FIRDocumentReference *doc = [self documentRef]; - XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; - FIRWriteBatch *batch = [doc.firestore batch]; - [batch setData:@{@"a" : @"b"} forDocument:doc]; - [batch setData:@{@"c" : @"d"} forDocument:doc]; - [batch commitWithCompletion:^(NSError *error) { - XCTAssertNil(error); - [batchExpectation fulfill]; - }]; - [self awaitExpectations]; - FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; - XCTAssertTrue(snapshot.exists); - XCTAssertEqualObjects(snapshot.data, @{@"c" : @"d"}); -} - -- (void)testSetDocumentWithMerge { - FIRDocumentReference *doc = [self documentRef]; - XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; - FIRWriteBatch *batch = [doc.firestore batch]; - [batch setData:@{ @"a" : @"b", @"nested" : @{@"a" : @"b"} } forDocument:doc]; - [batch setData:@{ - @"c" : @"d", - @"nested" : @{@"c" : @"d"} - } - forDocument:doc - options:[FIRSetOptions merge]]; - [batch commitWithCompletion:^(NSError *error) { - XCTAssertNil(error); - [batchExpectation fulfill]; - }]; - [self awaitExpectations]; - FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; - XCTAssertTrue(snapshot.exists); - XCTAssertEqualObjects( - snapshot.data, ( - @{ @"a" : @"b", - @"c" : @"d", - @"nested" : @{@"a" : @"b", @"c" : @"d"} })); -} - -- (void)testUpdateDocuments { - FIRDocumentReference *doc = [self documentRef]; - [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; - XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; - FIRWriteBatch *batch = [doc.firestore batch]; - [batch updateData:@{ @"baz" : @42 } forDocument:doc]; - [batch commitWithCompletion:^(NSError *error) { - XCTAssertNil(error); - [batchExpectation fulfill]; - }]; - [self awaitExpectations]; - FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; - XCTAssertTrue(snapshot.exists); - XCTAssertEqualObjects(snapshot.data, (@{ @"foo" : @"bar", @"baz" : @42 })); -} - -- (void)testCannotUpdateNonexistentDocuments { - FIRDocumentReference *doc = [self documentRef]; - XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; - FIRWriteBatch *batch = [doc.firestore batch]; - [batch updateData:@{ @"baz" : @42 } forDocument:doc]; - [batch commitWithCompletion:^(NSError *error) { - XCTAssertNotNil(error); - [batchExpectation fulfill]; - }]; - [self awaitExpectations]; - FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; - XCTAssertFalse(result.exists); -} - -- (void)testDeleteDocuments { - FIRDocumentReference *doc = [self documentRef]; - [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; - FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; - - XCTAssertTrue(snapshot.exists); - XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; - FIRWriteBatch *batch = [doc.firestore batch]; - [batch deleteDocument:doc]; - [batch commitWithCompletion:^(NSError *error) { - XCTAssertNil(error); - [batchExpectation fulfill]; - }]; - [self awaitExpectations]; - snapshot = [self readDocumentForRef:doc]; - XCTAssertFalse(snapshot.exists); -} - -- (void)testBatchesCommitAtomicallyRaisingCorrectEvents { - FIRCollectionReference *collection = [self collectionRef]; - FIRDocumentReference *docA = [collection documentWithPath:@"a"]; - FIRDocumentReference *docB = [collection documentWithPath:@"b"]; - FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; - [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options] - includeQueryMetadataChanges:YES] - listener:accumulator.valueEventHandler]; - FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; - XCTAssertEqual(initialSnap.count, 0); - - // Atomically write two documents. - XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; - FIRWriteBatch *batch = [collection.firestore batch]; - [batch setData:@{ @"a" : @1 } forDocument:docA]; - [batch setData:@{ @"b" : @2 } forDocument:docB]; - [batch commitWithCompletion:^(NSError *_Nullable error) { - XCTAssertNil(error); - [expectation fulfill]; - }]; - - FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; - XCTAssertTrue(localSnap.metadata.hasPendingWrites); - XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap), (@[ @{ @"a" : @1 }, @{ @"b" : @2 } ])); - - FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"]; - XCTAssertFalse(serverSnap.metadata.hasPendingWrites); - XCTAssertEqualObjects(FIRQuerySnapshotGetData(serverSnap), (@[ @{ @"a" : @1 }, @{ @"b" : @2 } ])); -} - -- (void)testBatchesFailAtomicallyRaisingCorrectEvents { - FIRCollectionReference *collection = [self collectionRef]; - FIRDocumentReference *docA = [collection documentWithPath:@"a"]; - FIRDocumentReference *docB = [collection documentWithPath:@"b"]; - FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; - [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options] - includeQueryMetadataChanges:YES] - listener:accumulator.valueEventHandler]; - FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; - XCTAssertEqual(initialSnap.count, 0); - - // Atomically write 1 document and update a nonexistent document. - XCTestExpectation *expectation = [self expectationWithDescription:@"batch failed"]; - FIRWriteBatch *batch = [collection.firestore batch]; - [batch setData:@{ @"a" : @1 } forDocument:docA]; - [batch updateData:@{ @"b" : @2 } forDocument:docB]; - [batch commitWithCompletion:^(NSError *_Nullable error) { - XCTAssertNotNil(error); - XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); - XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound); - [expectation fulfill]; - }]; - - // Local event with the set document. - FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; - XCTAssertTrue(localSnap.metadata.hasPendingWrites); - XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap), (@[ @{ @"a" : @1 } ])); - - // Server event with the set reverted. - FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"]; - XCTAssertFalse(serverSnap.metadata.hasPendingWrites); - XCTAssertEqual(serverSnap.count, 0); -} - -- (void)testWriteTheSameServerTimestampAcrossWrites { - FIRCollectionReference *collection = [self collectionRef]; - FIRDocumentReference *docA = [collection documentWithPath:@"a"]; - FIRDocumentReference *docB = [collection documentWithPath:@"b"]; - FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; - [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options] - includeQueryMetadataChanges:YES] - listener:accumulator.valueEventHandler]; - FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; - XCTAssertEqual(initialSnap.count, 0); - - // Atomically write 2 documents with server timestamps. - XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; - FIRWriteBatch *batch = [collection.firestore batch]; - [batch setData:@{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} forDocument:docA]; - [batch setData:@{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} forDocument:docB]; - [batch commitWithCompletion:^(NSError *_Nullable error) { - XCTAssertNil(error); - [expectation fulfill]; - }]; - - FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; - XCTAssertTrue(localSnap.metadata.hasPendingWrites); - XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap), - (@[ @{@"when" : [NSNull null]}, @{@"when" : [NSNull null]} ])); - - FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"]; - XCTAssertFalse(serverSnap.metadata.hasPendingWrites); - XCTAssertEqual(serverSnap.count, 2); - NSDate *when = serverSnap.documents[0][@"when"]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(serverSnap), - (@[ @{@"when" : when}, @{@"when" : when} ])); -} - -- (void)testCanWriteTheSameDocumentMultipleTimes { - FIRDocumentReference *doc = [self documentRef]; - FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; - [doc - addSnapshotListenerWithOptions:[[FIRDocumentListenOptions options] includeMetadataChanges:YES] - listener:accumulator.valueEventHandler]; - FIRDocumentSnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; - XCTAssertFalse(initialSnap.exists); - - XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; - FIRWriteBatch *batch = [doc.firestore batch]; - [batch deleteDocument:doc]; - [batch setData:@{ @"a" : @1, @"b" : @1, @"when" : @"when" } forDocument:doc]; - [batch updateData:@{ - @"b" : @2, - @"when" : [FIRFieldValue fieldValueForServerTimestamp] - } - forDocument:doc]; - [batch commitWithCompletion:^(NSError *_Nullable error) { - XCTAssertNil(error); - [expectation fulfill]; - }]; - - FIRDocumentSnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; - XCTAssertTrue(localSnap.metadata.hasPendingWrites); - XCTAssertEqualObjects(localSnap.data, (@{ @"a" : @1, @"b" : @2, @"when" : [NSNull null] })); - - FIRDocumentSnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"]; - XCTAssertFalse(serverSnap.metadata.hasPendingWrites); - NSDate *when = serverSnap[@"when"]; - XCTAssertEqualObjects(serverSnap.data, (@{ @"a" : @1, @"b" : @2, @"when" : when })); -} - -- (void)testUpdateFieldsWithDots { - FIRDocumentReference *doc = [self documentRef]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateFieldsWithDots"]; - FIRWriteBatch *batch = [doc.firestore batch]; - [batch setData:@{@"a.b" : @"old", @"c.d" : @"old"} forDocument:doc]; - [batch updateData:@{ - [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new" - } - forDocument:doc]; - - [batch commitWithCompletion:^(NSError *_Nullable error) { - XCTAssertNil(error); - [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { - XCTAssertNil(error); - XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"})); - }]; - [expectation fulfill]; - }]; - - [self awaitExpectations]; -} - -- (void)testUpdateNestedFields { - FIRDocumentReference *doc = [self documentRef]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateNestedFields"]; - FIRWriteBatch *batch = [doc.firestore batch]; - [batch setData:@{ - @"a" : @{@"b" : @"old"}, - @"c" : @{@"d" : @"old"}, - @"e" : @{@"f" : @"old"} - } - forDocument:doc]; - [batch updateData:@{ - @"a.b" : @"new", - [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new" - } - forDocument:doc]; - [batch commitWithCompletion:^(NSError *_Nullable error) { - XCTAssertNil(error); - [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { - XCTAssertNil(error); - XCTAssertEqualObjects(snapshot.data, (@{ - @"a" : @{@"b" : @"new"}, - @"c" : @{@"d" : @"new"}, - @"e" : @{@"f" : @"old"} - })); - }]; - [expectation fulfill]; - }]; - - [self awaitExpectations]; -} - -@end diff --git a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm new file mode 100644 index 0000000..9a2fef1 --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm @@ -0,0 +1,336 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import + +#import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" +#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" + +@interface FIRWriteBatchTests : FSTIntegrationTestCase +@end + +@implementation FIRWriteBatchTests + +- (void)testSupportEmptyBatches { + XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; + [[[self firestore] batch] commitWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testCommitWithoutCompletionHandler { + FIRDocumentReference *doc = [self documentRef]; + FIRWriteBatch *batch1 = [doc.firestore batch]; + [batch1 setData:@{@"aa" : @"bb"} forDocument:doc]; + [batch1 commitWithCompletion:nil]; + FIRDocumentSnapshot *snapshot1 = [self readDocumentForRef:doc]; + XCTAssertTrue(snapshot1.exists); + XCTAssertEqualObjects(snapshot1.data, @{@"aa" : @"bb"}); + + FIRWriteBatch *batch2 = [doc.firestore batch]; + [batch2 setData:@{@"cc" : @"dd"} forDocument:doc]; + [batch2 commit]; + + // TODO(b/70631617): There's currently a backend bug that prevents us from using a resume token + // right away (against hexa at least). So we sleep. :-( :-( Anything over ~10ms seems to be + // sufficient. + [NSThread sleepForTimeInterval:0.2f]; + + FIRDocumentSnapshot *snapshot2 = [self readDocumentForRef:doc]; + XCTAssertTrue(snapshot2.exists); + XCTAssertEqualObjects(snapshot2.data, @{@"cc" : @"dd"}); +} + +- (void)testSetDocuments { + FIRDocumentReference *doc = [self documentRef]; + XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch setData:@{@"a" : @"b"} forDocument:doc]; + [batch setData:@{@"c" : @"d"} forDocument:doc]; + [batch commitWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [batchExpectation fulfill]; + }]; + [self awaitExpectations]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertTrue(snapshot.exists); + XCTAssertEqualObjects(snapshot.data, @{@"c" : @"d"}); +} + +- (void)testSetDocumentWithMerge { + FIRDocumentReference *doc = [self documentRef]; + XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch setData:@{ @"a" : @"b", @"nested" : @{@"a" : @"b"} } forDocument:doc]; + [batch setData:@{ + @"c" : @"d", + @"nested" : @{@"c" : @"d"} + } + forDocument:doc + options:[FIRSetOptions merge]]; + [batch commitWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [batchExpectation fulfill]; + }]; + [self awaitExpectations]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertTrue(snapshot.exists); + XCTAssertEqualObjects( + snapshot.data, ( + @{ @"a" : @"b", + @"c" : @"d", + @"nested" : @{@"a" : @"b", @"c" : @"d"} })); +} + +- (void)testUpdateDocuments { + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; + XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch updateData:@{ @"baz" : @42 } forDocument:doc]; + [batch commitWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [batchExpectation fulfill]; + }]; + [self awaitExpectations]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertTrue(snapshot.exists); + XCTAssertEqualObjects(snapshot.data, (@{ @"foo" : @"bar", @"baz" : @42 })); +} + +- (void)testCannotUpdateNonexistentDocuments { + FIRDocumentReference *doc = [self documentRef]; + XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch updateData:@{ @"baz" : @42 } forDocument:doc]; + [batch commitWithCompletion:^(NSError *error) { + XCTAssertNotNil(error); + [batchExpectation fulfill]; + }]; + [self awaitExpectations]; + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertFalse(result.exists); +} + +- (void)testDeleteDocuments { + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + + XCTAssertTrue(snapshot.exists); + XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch deleteDocument:doc]; + [batch commitWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [batchExpectation fulfill]; + }]; + [self awaitExpectations]; + snapshot = [self readDocumentForRef:doc]; + XCTAssertFalse(snapshot.exists); +} + +- (void)testBatchesCommitAtomicallyRaisingCorrectEvents { + FIRCollectionReference *collection = [self collectionRef]; + FIRDocumentReference *docA = [collection documentWithPath:@"a"]; + FIRDocumentReference *docB = [collection documentWithPath:@"b"]; + FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; + [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options] + includeQueryMetadataChanges:YES] + listener:accumulator.valueEventHandler]; + FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; + XCTAssertEqual(initialSnap.count, 0); + + // Atomically write two documents. + XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [collection.firestore batch]; + [batch setData:@{ @"a" : @1 } forDocument:docA]; + [batch setData:@{ @"b" : @2 } forDocument:docB]; + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + + FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; + XCTAssertTrue(localSnap.metadata.hasPendingWrites); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap), (@[ @{ @"a" : @1 }, @{ @"b" : @2 } ])); + + FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"]; + XCTAssertFalse(serverSnap.metadata.hasPendingWrites); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(serverSnap), (@[ @{ @"a" : @1 }, @{ @"b" : @2 } ])); +} + +- (void)testBatchesFailAtomicallyRaisingCorrectEvents { + FIRCollectionReference *collection = [self collectionRef]; + FIRDocumentReference *docA = [collection documentWithPath:@"a"]; + FIRDocumentReference *docB = [collection documentWithPath:@"b"]; + FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; + [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options] + includeQueryMetadataChanges:YES] + listener:accumulator.valueEventHandler]; + FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; + XCTAssertEqual(initialSnap.count, 0); + + // Atomically write 1 document and update a nonexistent document. + XCTestExpectation *expectation = [self expectationWithDescription:@"batch failed"]; + FIRWriteBatch *batch = [collection.firestore batch]; + [batch setData:@{ @"a" : @1 } forDocument:docA]; + [batch updateData:@{ @"b" : @2 } forDocument:docB]; + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound); + [expectation fulfill]; + }]; + + // Local event with the set document. + FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; + XCTAssertTrue(localSnap.metadata.hasPendingWrites); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap), (@[ @{ @"a" : @1 } ])); + + // Server event with the set reverted. + FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"]; + XCTAssertFalse(serverSnap.metadata.hasPendingWrites); + XCTAssertEqual(serverSnap.count, 0); +} + +- (void)testWriteTheSameServerTimestampAcrossWrites { + FIRCollectionReference *collection = [self collectionRef]; + FIRDocumentReference *docA = [collection documentWithPath:@"a"]; + FIRDocumentReference *docB = [collection documentWithPath:@"b"]; + FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; + [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options] + includeQueryMetadataChanges:YES] + listener:accumulator.valueEventHandler]; + FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; + XCTAssertEqual(initialSnap.count, 0); + + // Atomically write 2 documents with server timestamps. + XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [collection.firestore batch]; + [batch setData:@{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} forDocument:docA]; + [batch setData:@{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} forDocument:docB]; + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + + FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; + XCTAssertTrue(localSnap.metadata.hasPendingWrites); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap), + (@[ @{@"when" : [NSNull null]}, @{@"when" : [NSNull null]} ])); + + FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"]; + XCTAssertFalse(serverSnap.metadata.hasPendingWrites); + XCTAssertEqual(serverSnap.count, 2); + NSDate *when = serverSnap.documents[0][@"when"]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(serverSnap), + (@[ @{@"when" : when}, @{@"when" : when} ])); +} + +- (void)testCanWriteTheSameDocumentMultipleTimes { + FIRDocumentReference *doc = [self documentRef]; + FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; + [doc + addSnapshotListenerWithOptions:[[FIRDocumentListenOptions options] includeMetadataChanges:YES] + listener:accumulator.valueEventHandler]; + FIRDocumentSnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; + XCTAssertFalse(initialSnap.exists); + + XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch deleteDocument:doc]; + [batch setData:@{ @"a" : @1, @"b" : @1, @"when" : @"when" } forDocument:doc]; + [batch updateData:@{ + @"b" : @2, + @"when" : [FIRFieldValue fieldValueForServerTimestamp] + } + forDocument:doc]; + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + + FIRDocumentSnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; + XCTAssertTrue(localSnap.metadata.hasPendingWrites); + XCTAssertEqualObjects(localSnap.data, (@{ @"a" : @1, @"b" : @2, @"when" : [NSNull null] })); + + FIRDocumentSnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"]; + XCTAssertFalse(serverSnap.metadata.hasPendingWrites); + NSDate *when = serverSnap[@"when"]; + XCTAssertEqualObjects(serverSnap.data, (@{ @"a" : @1, @"b" : @2, @"when" : when })); +} + +- (void)testUpdateFieldsWithDots { + FIRDocumentReference *doc = [self documentRef]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateFieldsWithDots"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch setData:@{@"a.b" : @"old", @"c.d" : @"old"} forDocument:doc]; + [batch updateData:@{ + [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new" + } + forDocument:doc]; + + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"})); + }]; + [expectation fulfill]; + }]; + + [self awaitExpectations]; +} + +- (void)testUpdateNestedFields { + FIRDocumentReference *doc = [self documentRef]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateNestedFields"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch setData:@{ + @"a" : @{@"b" : @"old"}, + @"c" : @{@"d" : @"old"}, + @"e" : @{@"f" : @"old"} + } + forDocument:doc]; + [batch updateData:@{ + @"a.b" : @"new", + [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new" + } + forDocument:doc]; + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{ + @"a" : @{@"b" : @"new"}, + @"c" : @{@"d" : @"new"}, + @"e" : @{@"f" : @"old"} + })); + }]; + [expectation fulfill]; + }]; + + [self awaitExpectations]; +} + +@end diff --git a/Firestore/Example/Tests/Integration/FSTDatastoreTests.m b/Firestore/Example/Tests/Integration/FSTDatastoreTests.m deleted file mode 100644 index 047f059..0000000 --- a/Firestore/Example/Tests/Integration/FSTDatastoreTests.m +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 FirebaseFirestore; - -#import -#import -#import - -#import "Firestore/Source/API/FIRDocumentReference+Internal.h" -#import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Auth/FSTEmptyCredentialsProvider.h" -#import "Firestore/Source/Core/FSTDatabaseInfo.h" -#import "Firestore/Source/Core/FSTFirestoreClient.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Core/FSTTimestamp.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTDatabaseID.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Remote/FSTDatastore.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" -#import "Firestore/Source/Remote/FSTRemoteStore.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTDispatchQueue.h" - -#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTRemoteStore (Tests) -- (void)commitBatch:(FSTMutationBatch *)batch; -@end - -#pragma mark - FSTRemoteStoreEventCapture - -@interface FSTRemoteStoreEventCapture : NSObject - -- (instancetype)init __attribute__((unavailable("Use initWithTestCase:"))); - -- (instancetype)initWithTestCase:(XCTestCase *_Nullable)testCase NS_DESIGNATED_INITIALIZER; - -- (void)expectWriteEventWithDescription:(NSString *)description; -- (void)expectListenEventWithDescription:(NSString *)description; - -@property(nonatomic, weak, nullable) XCTestCase *testCase; -@property(nonatomic, strong) NSMutableArray *writeEvents; -@property(nonatomic, strong) NSMutableArray *listenEvents; -@property(nonatomic, strong) NSMutableArray *writeEventExpectations; -@property(nonatomic, strong) NSMutableArray *listenEventExpectations; -@end - -@implementation FSTRemoteStoreEventCapture - -- (instancetype)initWithTestCase:(XCTestCase *_Nullable)testCase { - if (self = [super init]) { - _writeEvents = [NSMutableArray array]; - _listenEvents = [NSMutableArray array]; - _testCase = testCase; - _writeEventExpectations = [NSMutableArray array]; - _listenEventExpectations = [NSMutableArray array]; - } - return self; -} - -- (void)expectWriteEventWithDescription:(NSString *)description { - [self.writeEventExpectations - addObject:[self.testCase - expectationWithDescription:[NSString - stringWithFormat:@"write event %lu: %@", - (unsigned long) - self.writeEventExpectations - .count, - description]]]; -} - -- (void)expectListenEventWithDescription:(NSString *)description { - [self.listenEventExpectations - addObject:[self.testCase - expectationWithDescription:[NSString - stringWithFormat:@"listen event %lu: %@", - (unsigned long) - self.listenEventExpectations - .count, - description]]]; -} - -- (void)applySuccessfulWriteWithResult:(FSTMutationBatchResult *)batchResult { - [self.writeEvents addObject:batchResult]; - XCTestExpectation *expectation = [self.writeEventExpectations objectAtIndex:0]; - [self.writeEventExpectations removeObjectAtIndex:0]; - [expectation fulfill]; -} - -- (void)rejectFailedWriteWithBatchID:(FSTBatchID)batchID error:(NSError *)error { - FSTFail(@"Not implemented"); -} - -- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { - [self.listenEvents addObject:remoteEvent]; - XCTestExpectation *expectation = [self.listenEventExpectations objectAtIndex:0]; - [self.listenEventExpectations removeObjectAtIndex:0]; - [expectation fulfill]; -} - -- (void)rejectListenWithTargetID:(FSTBoxedTargetID *)targetID error:(NSError *)error { - FSTFail(@"Not implemented"); -} - -@end - -#pragma mark - FSTDatastoreTests - -@interface FSTDatastoreTests : XCTestCase - -@end - -@implementation FSTDatastoreTests { - FSTDispatchQueue *_testWorkerQueue; - FSTLocalStore *_localStore; - id _credentials; - - FSTDatastore *_datastore; - FSTRemoteStore *_remoteStore; -} - -- (void)setUp { - [super setUp]; - - NSString *projectID = [[NSProcessInfo processInfo] environment][@"PROJECT_ID"]; - if (!projectID) { - projectID = @"test-db"; - } - - FIRFirestoreSettings *settings = [FSTIntegrationTestCase settings]; - if (!settings.sslEnabled) { - [GRPCCall useInsecureConnectionsForHost:settings.host]; - } - - FSTDatabaseID *databaseID = - [FSTDatabaseID databaseIDWithProject:projectID database:kDefaultDatabaseID]; - - FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID - persistenceKey:@"test-key" - host:settings.host - sslEnabled:settings.sslEnabled]; - - _testWorkerQueue = [FSTDispatchQueue - queueWith:dispatch_queue_create("com.google.firestore.FSTDatastoreTestsWorkerQueue", - DISPATCH_QUEUE_SERIAL)]; - - _credentials = [[FSTEmptyCredentialsProvider alloc] init]; - - _datastore = [FSTDatastore datastoreWithDatabase:databaseInfo - workerDispatchQueue:_testWorkerQueue - credentials:_credentials]; - - _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:_datastore]; - - [_testWorkerQueue dispatchAsync:^() { - [_remoteStore start]; - }]; -} - -- (void)tearDown { - XCTestExpectation *completion = [self expectationWithDescription:@"shutdown"]; - [_testWorkerQueue dispatchAsync:^{ - [_remoteStore shutdown]; - [completion fulfill]; - }]; - [self awaitExpectations]; - - [super tearDown]; -} - -- (void)testCommit { - XCTestExpectation *expectation = [self expectationWithDescription:@"commitWithCompletion"]; - - [_datastore commitMutations:@[] - completion:^(NSError *_Nullable error) { - XCTAssertNil(error, @"Failed to commit"); - [expectation fulfill]; - }]; - - [self awaitExpectations]; -} - -- (void)testStreamingWrite { - FSTRemoteStoreEventCapture *capture = [[FSTRemoteStoreEventCapture alloc] initWithTestCase:self]; - [capture expectWriteEventWithDescription:@"write mutations"]; - - _remoteStore.syncEngine = capture; - - FSTSetMutation *mutation = [self setMutation]; - FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:23 - localWriteTime:[FSTTimestamp timestamp] - mutations:@[ mutation ]]; - [_testWorkerQueue dispatchAsync:^{ - [_remoteStore commitBatch:batch]; - }]; - - [self awaitExpectations]; -} - -- (void)awaitExpectations { - [self waitForExpectationsWithTimeout:4.0 - handler:^(NSError *_Nullable expectationError) { - if (expectationError) { - XCTFail(@"Error waiting for timeout: %@", expectationError); - } - }]; -} - -- (FSTSetMutation *)setMutation { - return [[FSTSetMutation alloc] - initWithKey:[FSTDocumentKey keyWithPathString:@"rooms/eros"] - value:[[FSTObjectValue alloc] - initWithDictionary:@{@"name" : [FSTStringValue stringValue:@"Eros"]}] - precondition:[FSTPrecondition none]]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm new file mode 100644 index 0000000..bf56367 --- /dev/null +++ b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm @@ -0,0 +1,241 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import +#import +#import + +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FSTUserDataConverter.h" +#import "Firestore/Source/Auth/FSTEmptyCredentialsProvider.h" +#import "Firestore/Source/Core/FSTDatabaseInfo.h" +#import "Firestore/Source/Core/FSTFirestoreClient.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Core/FSTTimestamp.h" +#import "Firestore/Source/Local/FSTQueryData.h" +#import "Firestore/Source/Model/FSTDatabaseID.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Model/FSTMutationBatch.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Remote/FSTDatastore.h" +#import "Firestore/Source/Remote/FSTRemoteEvent.h" +#import "Firestore/Source/Remote/FSTRemoteStore.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTDispatchQueue.h" + +#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTRemoteStore (Tests) +- (void)commitBatch:(FSTMutationBatch *)batch; +@end + +#pragma mark - FSTRemoteStoreEventCapture + +@interface FSTRemoteStoreEventCapture : NSObject + +- (instancetype)init __attribute__((unavailable("Use initWithTestCase:"))); + +- (instancetype)initWithTestCase:(XCTestCase *_Nullable)testCase NS_DESIGNATED_INITIALIZER; + +- (void)expectWriteEventWithDescription:(NSString *)description; +- (void)expectListenEventWithDescription:(NSString *)description; + +@property(nonatomic, weak, nullable) XCTestCase *testCase; +@property(nonatomic, strong) NSMutableArray *writeEvents; +@property(nonatomic, strong) NSMutableArray *listenEvents; +@property(nonatomic, strong) NSMutableArray *writeEventExpectations; +@property(nonatomic, strong) NSMutableArray *listenEventExpectations; +@end + +@implementation FSTRemoteStoreEventCapture + +- (instancetype)initWithTestCase:(XCTestCase *_Nullable)testCase { + if (self = [super init]) { + _writeEvents = [NSMutableArray array]; + _listenEvents = [NSMutableArray array]; + _testCase = testCase; + _writeEventExpectations = [NSMutableArray array]; + _listenEventExpectations = [NSMutableArray array]; + } + return self; +} + +- (void)expectWriteEventWithDescription:(NSString *)description { + [self.writeEventExpectations + addObject:[self.testCase + expectationWithDescription:[NSString + stringWithFormat:@"write event %lu: %@", + (unsigned long) + self.writeEventExpectations + .count, + description]]]; +} + +- (void)expectListenEventWithDescription:(NSString *)description { + [self.listenEventExpectations + addObject:[self.testCase + expectationWithDescription:[NSString + stringWithFormat:@"listen event %lu: %@", + (unsigned long) + self.listenEventExpectations + .count, + description]]]; +} + +- (void)applySuccessfulWriteWithResult:(FSTMutationBatchResult *)batchResult { + [self.writeEvents addObject:batchResult]; + XCTestExpectation *expectation = [self.writeEventExpectations objectAtIndex:0]; + [self.writeEventExpectations removeObjectAtIndex:0]; + [expectation fulfill]; +} + +- (void)rejectFailedWriteWithBatchID:(FSTBatchID)batchID error:(NSError *)error { + FSTFail(@"Not implemented"); +} + +- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { + [self.listenEvents addObject:remoteEvent]; + XCTestExpectation *expectation = [self.listenEventExpectations objectAtIndex:0]; + [self.listenEventExpectations removeObjectAtIndex:0]; + [expectation fulfill]; +} + +- (void)rejectListenWithTargetID:(FSTBoxedTargetID *)targetID error:(NSError *)error { + FSTFail(@"Not implemented"); +} + +@end + +#pragma mark - FSTDatastoreTests + +@interface FSTDatastoreTests : XCTestCase + +@end + +@implementation FSTDatastoreTests { + FSTDispatchQueue *_testWorkerQueue; + FSTLocalStore *_localStore; + id _credentials; + + FSTDatastore *_datastore; + FSTRemoteStore *_remoteStore; +} + +- (void)setUp { + [super setUp]; + + NSString *projectID = [[NSProcessInfo processInfo] environment][@"PROJECT_ID"]; + if (!projectID) { + projectID = @"test-db"; + } + + FIRFirestoreSettings *settings = [FSTIntegrationTestCase settings]; + if (!settings.sslEnabled) { + [GRPCCall useInsecureConnectionsForHost:settings.host]; + } + + FSTDatabaseID *databaseID = + [FSTDatabaseID databaseIDWithProject:projectID database:kDefaultDatabaseID]; + + FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID + persistenceKey:@"test-key" + host:settings.host + sslEnabled:settings.sslEnabled]; + + _testWorkerQueue = [FSTDispatchQueue + queueWith:dispatch_queue_create("com.google.firestore.FSTDatastoreTestsWorkerQueue", + DISPATCH_QUEUE_SERIAL)]; + + _credentials = [[FSTEmptyCredentialsProvider alloc] init]; + + _datastore = [FSTDatastore datastoreWithDatabase:databaseInfo + workerDispatchQueue:_testWorkerQueue + credentials:_credentials]; + + _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:_datastore]; + + [_testWorkerQueue dispatchAsync:^() { + [_remoteStore start]; + }]; +} + +- (void)tearDown { + XCTestExpectation *completion = [self expectationWithDescription:@"shutdown"]; + [_testWorkerQueue dispatchAsync:^{ + [_remoteStore shutdown]; + [completion fulfill]; + }]; + [self awaitExpectations]; + + [super tearDown]; +} + +- (void)testCommit { + XCTestExpectation *expectation = [self expectationWithDescription:@"commitWithCompletion"]; + + [_datastore commitMutations:@[] + completion:^(NSError *_Nullable error) { + XCTAssertNil(error, @"Failed to commit"); + [expectation fulfill]; + }]; + + [self awaitExpectations]; +} + +- (void)testStreamingWrite { + FSTRemoteStoreEventCapture *capture = [[FSTRemoteStoreEventCapture alloc] initWithTestCase:self]; + [capture expectWriteEventWithDescription:@"write mutations"]; + + _remoteStore.syncEngine = capture; + + FSTSetMutation *mutation = [self setMutation]; + FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:23 + localWriteTime:[FSTTimestamp timestamp] + mutations:@[ mutation ]]; + [_testWorkerQueue dispatchAsync:^{ + [_remoteStore commitBatch:batch]; + }]; + + [self awaitExpectations]; +} + +- (void)awaitExpectations { + [self waitForExpectationsWithTimeout:4.0 + handler:^(NSError *_Nullable expectationError) { + if (expectationError) { + XCTFail(@"Error waiting for timeout: %@", expectationError); + } + }]; +} + +- (FSTSetMutation *)setMutation { + return [[FSTSetMutation alloc] + initWithKey:[FSTDocumentKey keyWithPathString:@"rooms/eros"] + value:[[FSTObjectValue alloc] + initWithDictionary:@{@"name" : [FSTStringValue stringValue:@"Eros"]}] + precondition:[FSTPrecondition none]]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Integration/FSTSmokeTests.m b/Firestore/Example/Tests/Integration/FSTSmokeTests.m deleted file mode 100644 index ad75e50..0000000 --- a/Firestore/Example/Tests/Integration/FSTSmokeTests.m +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 FirebaseFirestore; - -#import - -#import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" -#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" - -@interface FSTSmokeTests : FSTIntegrationTestCase -@end - -@implementation FSTSmokeTests - -- (void)testCanWriteASingleDocument { - FIRDocumentReference *ref = [self documentRef]; - [self writeDocumentRef:ref data:[self chatMessage]]; -} - -- (void)testCanReadAWrittenDocument { - NSDictionary *data = [self chatMessage]; - - FIRDocumentReference *ref = [self documentRef]; - [self writeDocumentRef:ref data:data]; - - FIRDocumentSnapshot *doc = [self readDocumentForRef:ref]; - XCTAssertEqualObjects(doc.data, data); -} - -- (void)testObservesExistingDocument { - [self readerAndWriterOnDocumentRef:^(NSString *path, FIRDocumentReference *readerRef, - FIRDocumentReference *writerRef) { - NSDictionary *data = [self chatMessage]; - [self writeDocumentRef:writerRef data:data]; - - id listenerRegistration = - [readerRef addSnapshotListener:self.eventAccumulator.valueEventHandler]; - - FIRDocumentSnapshot *doc = [self.eventAccumulator awaitEventWithName:@"snapshot"]; - XCTAssertEqual([doc class], [FIRDocumentSnapshot class]); - XCTAssertEqualObjects(doc.data, data); - - [listenerRegistration remove]; - }]; -} - -- (void)testObservesNewDocument { - [self readerAndWriterOnDocumentRef:^(NSString *path, FIRDocumentReference *readerRef, - FIRDocumentReference *writerRef) { - id listenerRegistration = - [readerRef addSnapshotListener:self.eventAccumulator.valueEventHandler]; - - FIRDocumentSnapshot *doc1 = [self.eventAccumulator awaitEventWithName:@"null snapshot"]; - XCTAssertFalse(doc1.exists); - // TODO(b/36366944): add tests for doc1.path) - - NSDictionary *data = [self chatMessage]; - [self writeDocumentRef:writerRef data:data]; - - FIRDocumentSnapshot *doc2 = [self.eventAccumulator awaitEventWithName:@"full snapshot"]; - XCTAssertEqual([doc2 class], [FIRDocumentSnapshot class]); - XCTAssertEqualObjects(doc2.data, data); - - [listenerRegistration remove]; - }]; -} - -- (void)testWillFireValueEventsForEmptyCollections { - FIRCollectionReference *collection = [self.db collectionWithPath:@"empty-collection"]; - id listenerRegistration = - [collection addSnapshotListener:self.eventAccumulator.valueEventHandler]; - - FIRQuerySnapshot *snap = [self.eventAccumulator awaitEventWithName:@"empty query snapshot"]; - XCTAssertEqual([snap class], [FIRQuerySnapshot class]); - XCTAssertEqual(snap.count, 0); - - [listenerRegistration remove]; -} - -- (void)testGetCollectionQuery { - NSDictionary *testDocs = @{ - @"1" : @{@"name" : @"Patryk", @"message" : @"Real data, yo!"}, - @"2" : @{@"name" : @"Gil", @"message" : @"Yep!"}, - @"3" : @{@"name" : @"Jonny", @"message" : @"Back to work!"}, - }; - - FIRCollectionReference *docs = [self collectionRefWithDocuments:testDocs]; - FIRQuerySnapshot *result = [self readDocumentSetForRef:docs]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(result), - (@[ testDocs[@"1"], testDocs[@"2"], testDocs[@"3"] ])); -} - -// TODO(klimt): This test is disabled because we can't create compound indexes programmatically. -- (void)xtestQueryByFieldAndUseOrderBy { - NSDictionary *testDocs = @{ - @"1" : @{@"sort" : @1, @"filter" : @YES, @"key" : @"1"}, - @"2" : @{@"sort" : @2, @"filter" : @YES, @"key" : @"2"}, - @"3" : @{@"sort" : @2, @"filter" : @YES, @"key" : @"3"}, - @"4" : @{@"sort" : @3, @"filter" : @NO, @"key" : @"4"} - }; - - FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; - - FIRQuery *query = - [[coll queryWhereField:@"filter" isEqualTo:@YES] queryOrderedByField:@"sort" descending:YES]; - FIRQuerySnapshot *result = [self readDocumentSetForRef:query]; - XCTAssertEqualObjects(FIRQuerySnapshotGetData(result), - (@[ testDocs[@"2"], testDocs[@"3"], testDocs[@"1"] ])); -} - -- (NSDictionary *)chatMessage { - return @{@"name" : @"Patryk", @"message" : @"We are actually writing data!"}; -} - -@end diff --git a/Firestore/Example/Tests/Integration/FSTSmokeTests.mm b/Firestore/Example/Tests/Integration/FSTSmokeTests.mm new file mode 100644 index 0000000..cb726b8 --- /dev/null +++ b/Firestore/Example/Tests/Integration/FSTSmokeTests.mm @@ -0,0 +1,129 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import + +#import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" +#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" + +@interface FSTSmokeTests : FSTIntegrationTestCase +@end + +@implementation FSTSmokeTests + +- (void)testCanWriteASingleDocument { + FIRDocumentReference *ref = [self documentRef]; + [self writeDocumentRef:ref data:[self chatMessage]]; +} + +- (void)testCanReadAWrittenDocument { + NSDictionary *data = [self chatMessage]; + + FIRDocumentReference *ref = [self documentRef]; + [self writeDocumentRef:ref data:data]; + + FIRDocumentSnapshot *doc = [self readDocumentForRef:ref]; + XCTAssertEqualObjects(doc.data, data); +} + +- (void)testObservesExistingDocument { + [self readerAndWriterOnDocumentRef:^(NSString *path, FIRDocumentReference *readerRef, + FIRDocumentReference *writerRef) { + NSDictionary *data = [self chatMessage]; + [self writeDocumentRef:writerRef data:data]; + + id listenerRegistration = + [readerRef addSnapshotListener:self.eventAccumulator.valueEventHandler]; + + FIRDocumentSnapshot *doc = [self.eventAccumulator awaitEventWithName:@"snapshot"]; + XCTAssertEqual([doc class], [FIRDocumentSnapshot class]); + XCTAssertEqualObjects(doc.data, data); + + [listenerRegistration remove]; + }]; +} + +- (void)testObservesNewDocument { + [self readerAndWriterOnDocumentRef:^(NSString *path, FIRDocumentReference *readerRef, + FIRDocumentReference *writerRef) { + id listenerRegistration = + [readerRef addSnapshotListener:self.eventAccumulator.valueEventHandler]; + + FIRDocumentSnapshot *doc1 = [self.eventAccumulator awaitEventWithName:@"null snapshot"]; + XCTAssertFalse(doc1.exists); + // TODO(b/36366944): add tests for doc1.path) + + NSDictionary *data = [self chatMessage]; + [self writeDocumentRef:writerRef data:data]; + + FIRDocumentSnapshot *doc2 = [self.eventAccumulator awaitEventWithName:@"full snapshot"]; + XCTAssertEqual([doc2 class], [FIRDocumentSnapshot class]); + XCTAssertEqualObjects(doc2.data, data); + + [listenerRegistration remove]; + }]; +} + +- (void)testWillFireValueEventsForEmptyCollections { + FIRCollectionReference *collection = [self.db collectionWithPath:@"empty-collection"]; + id listenerRegistration = + [collection addSnapshotListener:self.eventAccumulator.valueEventHandler]; + + FIRQuerySnapshot *snap = [self.eventAccumulator awaitEventWithName:@"empty query snapshot"]; + XCTAssertEqual([snap class], [FIRQuerySnapshot class]); + XCTAssertEqual(snap.count, 0); + + [listenerRegistration remove]; +} + +- (void)testGetCollectionQuery { + NSDictionary *testDocs = @{ + @"1" : @{@"name" : @"Patryk", @"message" : @"Real data, yo!"}, + @"2" : @{@"name" : @"Gil", @"message" : @"Yep!"}, + @"3" : @{@"name" : @"Jonny", @"message" : @"Back to work!"}, + }; + + FIRCollectionReference *docs = [self collectionRefWithDocuments:testDocs]; + FIRQuerySnapshot *result = [self readDocumentSetForRef:docs]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(result), + (@[ testDocs[@"1"], testDocs[@"2"], testDocs[@"3"] ])); +} + +// TODO(klimt): This test is disabled because we can't create compound indexes programmatically. +- (void)xtestQueryByFieldAndUseOrderBy { + NSDictionary *testDocs = @{ + @"1" : @{@"sort" : @1, @"filter" : @YES, @"key" : @"1"}, + @"2" : @{@"sort" : @2, @"filter" : @YES, @"key" : @"2"}, + @"3" : @{@"sort" : @2, @"filter" : @YES, @"key" : @"3"}, + @"4" : @{@"sort" : @3, @"filter" : @NO, @"key" : @"4"} + }; + + FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; + + FIRQuery *query = + [[coll queryWhereField:@"filter" isEqualTo:@YES] queryOrderedByField:@"sort" descending:YES]; + FIRQuerySnapshot *result = [self readDocumentSetForRef:query]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(result), + (@[ testDocs[@"2"], testDocs[@"3"], testDocs[@"1"] ])); +} + +- (NSDictionary *)chatMessage { + return @{@"name" : @"Patryk", @"message" : @"We are actually writing data!"}; +} + +@end diff --git a/Firestore/Example/Tests/Integration/FSTStreamTests.m b/Firestore/Example/Tests/Integration/FSTStreamTests.m deleted file mode 100644 index bbdf372..0000000 --- a/Firestore/Example/Tests/Integration/FSTStreamTests.m +++ /dev/null @@ -1,312 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#import - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" -#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" -#import "Firestore/Example/Tests/Util/FSTTestDispatchQueue.h" -#import "Firestore/Source/Auth/FSTEmptyCredentialsProvider.h" -#import "Firestore/Source/Core/FSTDatabaseInfo.h" -#import "Firestore/Source/Model/FSTDatabaseID.h" -#import "Firestore/Source/Remote/FSTDatastore.h" -#import "Firestore/Source/Remote/FSTStream.h" -#import "Firestore/Source/Util/FSTAssert.h" - -/** Exposes otherwise private methods for testing. */ -@interface FSTStream (Testing) -- (void)writesFinishedWithError:(NSError *_Nullable)error; -@end - -/** - * Implements FSTWatchStreamDelegate and FSTWriteStreamDelegate and supports waiting on callbacks - * via `fulfillOnCallback`. - */ -@interface FSTStreamStatusDelegate : NSObject - -- (instancetype)initWithTestCase:(XCTestCase *)testCase - queue:(FSTDispatchQueue *)dispatchQueue NS_DESIGNATED_INITIALIZER; -- (instancetype)init NS_UNAVAILABLE; - -@property(nonatomic, weak, readonly) XCTestCase *testCase; -@property(nonatomic, strong, readonly) FSTDispatchQueue *dispatchQueue; -@property(nonatomic, readonly) NSMutableArray *states; -@property(nonatomic, strong) XCTestExpectation *expectation; - -@end - -@implementation FSTStreamStatusDelegate - -- (instancetype)initWithTestCase:(XCTestCase *)testCase queue:(FSTDispatchQueue *)dispatchQueue { - if (self = [super init]) { - _testCase = testCase; - _dispatchQueue = dispatchQueue; - _states = [NSMutableArray new]; - } - - return self; -} - -- (void)watchStreamDidOpen { - [_states addObject:@"watchStreamDidOpen"]; - [_expectation fulfill]; - _expectation = nil; -} - -- (void)writeStreamDidOpen { - [_states addObject:@"writeStreamDidOpen"]; - [_expectation fulfill]; - _expectation = nil; -} - -- (void)writeStreamDidCompleteHandshake { - [_states addObject:@"writeStreamDidCompleteHandshake"]; - [_expectation fulfill]; - _expectation = nil; -} - -- (void)writeStreamWasInterruptedWithError:(nullable NSError *)error { - [_states addObject:@"writeStreamWasInterrupted"]; - [_expectation fulfill]; - _expectation = nil; -} - -- (void)watchStreamWasInterruptedWithError:(nullable NSError *)error { - [_states addObject:@"watchStreamWasInterrupted"]; - [_expectation fulfill]; - _expectation = nil; -} - -- (void)watchStreamDidChange:(FSTWatchChange *)change - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion { - [_states addObject:@"watchStreamDidChange"]; - [_expectation fulfill]; - _expectation = nil; -} - -- (void)writeStreamDidReceiveResponseWithVersion:(FSTSnapshotVersion *)commitVersion - mutationResults:(NSArray *)results { - [_states addObject:@"writeStreamDidReceiveResponseWithVersion"]; - [_expectation fulfill]; - _expectation = nil; -} - -/** - * Executes 'block' using the provided FSTDispatchQueue and waits for any callback on this delegate - * to be called. - */ -- (void)awaitNotificationFromBlock:(void (^)(void))block { - FSTAssert(_expectation == nil, @"Previous expectation still active"); - XCTestExpectation *expectation = - [self.testCase expectationWithDescription:@"awaitCallbackInBlock"]; - _expectation = expectation; - [self.dispatchQueue dispatchAsync:block]; - [self.testCase awaitExpectations]; -} - -@end - -@interface FSTStreamTests : XCTestCase - -@end - -@implementation FSTStreamTests { - dispatch_queue_t _testQueue; - FSTTestDispatchQueue *_workerDispatchQueue; - FSTDatabaseInfo *_databaseInfo; - FSTEmptyCredentialsProvider *_credentials; - FSTStreamStatusDelegate *_delegate; - - /** Single mutation to send to the write stream. */ - NSArray *_mutations; -} - -- (void)setUp { - [super setUp]; - - FIRFirestoreSettings *settings = [FSTIntegrationTestCase settings]; - FSTDatabaseID *databaseID = - [FSTDatabaseID databaseIDWithProject:[FSTIntegrationTestCase projectID] - database:kDefaultDatabaseID]; - - _testQueue = dispatch_queue_create("FSTStreamTestWorkerQueue", DISPATCH_QUEUE_SERIAL); - _workerDispatchQueue = [[FSTTestDispatchQueue alloc] initWithQueue:_testQueue]; - - _databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID - persistenceKey:@"test-key" - host:settings.host - sslEnabled:settings.sslEnabled]; - _credentials = [[FSTEmptyCredentialsProvider alloc] init]; - - _delegate = [[FSTStreamStatusDelegate alloc] initWithTestCase:self queue:_workerDispatchQueue]; - - _mutations = @[ FSTTestSetMutation(@"foo/bar", @{}) ]; -} - -- (FSTWriteStream *)setUpWriteStream { - FSTDatastore *datastore = [[FSTDatastore alloc] initWithDatabaseInfo:_databaseInfo - workerDispatchQueue:_workerDispatchQueue - credentials:_credentials]; - return [datastore createWriteStream]; -} - -- (FSTWatchStream *)setUpWatchStream { - FSTDatastore *datastore = [[FSTDatastore alloc] initWithDatabaseInfo:_databaseInfo - workerDispatchQueue:_workerDispatchQueue - credentials:_credentials]; - return [datastore createWatchStream]; -} - -/** - * Drains the test queue and asserts that all the observed callbacks (up to this point) match - * 'expectedStates'. Clears the list of observed callbacks on completion. - */ -- (void)verifyDelegateObservedStates:(NSArray *)expectedStates { - // Drain queue - dispatch_sync(_testQueue, ^{ - }); - - XCTAssertEqualObjects(_delegate.states, expectedStates); - [_delegate.states removeAllObjects]; -} - -/** Verifies that the watch stream does not issue an onClose callback after a call to stop(). */ -- (void)testWatchStreamStopBeforeHandshake { - FSTWatchStream *watchStream = [self setUpWatchStream]; - - [_delegate awaitNotificationFromBlock:^{ - [watchStream startWithDelegate:_delegate]; - }]; - - // Stop must not call watchStreamDidClose because the full implementation of the delegate could - // attempt to restart the stream in the event it had pending watches. - [_workerDispatchQueue dispatchAsync:^{ - [watchStream stop]; - }]; - - // Simulate a final callback from GRPC - [watchStream writesFinishedWithError:nil]; - - [self verifyDelegateObservedStates:@[ @"watchStreamDidOpen" ]]; -} - -/** Verifies that the write stream does not issue an onClose callback after a call to stop(). */ -- (void)testWriteStreamStopBeforeHandshake { - FSTWriteStream *writeStream = [self setUpWriteStream]; - - [_delegate awaitNotificationFromBlock:^{ - [writeStream startWithDelegate:_delegate]; - }]; - - // Don't start the handshake. - - // Stop must not call watchStreamDidClose because the full implementation of the delegate could - // attempt to restart the stream in the event it had pending watches. - [_workerDispatchQueue dispatchAsync:^{ - [writeStream stop]; - }]; - - // Simulate a final callback from GRPC - [writeStream writesFinishedWithError:nil]; - - [self verifyDelegateObservedStates:@[ @"writeStreamDidOpen" ]]; -} - -- (void)testWriteStreamStopAfterHandshake { - FSTWriteStream *writeStream = [self setUpWriteStream]; - - [_delegate awaitNotificationFromBlock:^{ - [writeStream startWithDelegate:_delegate]; - }]; - - // Writing before the handshake should throw - dispatch_sync(_testQueue, ^{ - XCTAssertThrows([writeStream writeMutations:_mutations]); - }); - - [_delegate awaitNotificationFromBlock:^{ - [writeStream writeHandshake]; - }]; - - // Now writes should succeed - [_delegate awaitNotificationFromBlock:^{ - [writeStream writeMutations:_mutations]; - }]; - - [_workerDispatchQueue dispatchAsync:^{ - [writeStream stop]; - }]; - - [self verifyDelegateObservedStates:@[ - @"writeStreamDidOpen", @"writeStreamDidCompleteHandshake", - @"writeStreamDidReceiveResponseWithVersion" - ]]; -} - -- (void)testStreamClosesWhenIdle { - FSTWriteStream *writeStream = [self setUpWriteStream]; - - [_delegate awaitNotificationFromBlock:^{ - [writeStream startWithDelegate:_delegate]; - }]; - - [_delegate awaitNotificationFromBlock:^{ - [writeStream writeHandshake]; - }]; - - [_delegate awaitNotificationFromBlock:^{ - [writeStream markIdle]; - }]; - - dispatch_sync(_testQueue, ^{ - XCTAssertFalse([writeStream isOpen]); - }); - - [self verifyDelegateObservedStates:@[ - @"writeStreamDidOpen", @"writeStreamDidCompleteHandshake", @"writeStreamWasInterrupted" - ]]; -} - -- (void)testStreamCancelsIdleOnWrite { - FSTWriteStream *writeStream = [self setUpWriteStream]; - - [_delegate awaitNotificationFromBlock:^{ - [writeStream startWithDelegate:_delegate]; - }]; - - [_delegate awaitNotificationFromBlock:^{ - [writeStream writeHandshake]; - }]; - - // Mark the stream idle, but immediately cancel the idle timer by issuing another write. - [_delegate awaitNotificationFromBlock:^{ - [writeStream markIdle]; - [writeStream writeMutations:_mutations]; - }]; - - dispatch_sync(_testQueue, ^{ - XCTAssertTrue([writeStream isOpen]); - }); - - [self verifyDelegateObservedStates:@[ - @"writeStreamDidOpen", @"writeStreamDidCompleteHandshake", - @"writeStreamDidReceiveResponseWithVersion" - ]]; -} - -@end diff --git a/Firestore/Example/Tests/Integration/FSTStreamTests.mm b/Firestore/Example/Tests/Integration/FSTStreamTests.mm new file mode 100644 index 0000000..bbdf372 --- /dev/null +++ b/Firestore/Example/Tests/Integration/FSTStreamTests.mm @@ -0,0 +1,312 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" +#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" +#import "Firestore/Example/Tests/Util/FSTTestDispatchQueue.h" +#import "Firestore/Source/Auth/FSTEmptyCredentialsProvider.h" +#import "Firestore/Source/Core/FSTDatabaseInfo.h" +#import "Firestore/Source/Model/FSTDatabaseID.h" +#import "Firestore/Source/Remote/FSTDatastore.h" +#import "Firestore/Source/Remote/FSTStream.h" +#import "Firestore/Source/Util/FSTAssert.h" + +/** Exposes otherwise private methods for testing. */ +@interface FSTStream (Testing) +- (void)writesFinishedWithError:(NSError *_Nullable)error; +@end + +/** + * Implements FSTWatchStreamDelegate and FSTWriteStreamDelegate and supports waiting on callbacks + * via `fulfillOnCallback`. + */ +@interface FSTStreamStatusDelegate : NSObject + +- (instancetype)initWithTestCase:(XCTestCase *)testCase + queue:(FSTDispatchQueue *)dispatchQueue NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@property(nonatomic, weak, readonly) XCTestCase *testCase; +@property(nonatomic, strong, readonly) FSTDispatchQueue *dispatchQueue; +@property(nonatomic, readonly) NSMutableArray *states; +@property(nonatomic, strong) XCTestExpectation *expectation; + +@end + +@implementation FSTStreamStatusDelegate + +- (instancetype)initWithTestCase:(XCTestCase *)testCase queue:(FSTDispatchQueue *)dispatchQueue { + if (self = [super init]) { + _testCase = testCase; + _dispatchQueue = dispatchQueue; + _states = [NSMutableArray new]; + } + + return self; +} + +- (void)watchStreamDidOpen { + [_states addObject:@"watchStreamDidOpen"]; + [_expectation fulfill]; + _expectation = nil; +} + +- (void)writeStreamDidOpen { + [_states addObject:@"writeStreamDidOpen"]; + [_expectation fulfill]; + _expectation = nil; +} + +- (void)writeStreamDidCompleteHandshake { + [_states addObject:@"writeStreamDidCompleteHandshake"]; + [_expectation fulfill]; + _expectation = nil; +} + +- (void)writeStreamWasInterruptedWithError:(nullable NSError *)error { + [_states addObject:@"writeStreamWasInterrupted"]; + [_expectation fulfill]; + _expectation = nil; +} + +- (void)watchStreamWasInterruptedWithError:(nullable NSError *)error { + [_states addObject:@"watchStreamWasInterrupted"]; + [_expectation fulfill]; + _expectation = nil; +} + +- (void)watchStreamDidChange:(FSTWatchChange *)change + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion { + [_states addObject:@"watchStreamDidChange"]; + [_expectation fulfill]; + _expectation = nil; +} + +- (void)writeStreamDidReceiveResponseWithVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray *)results { + [_states addObject:@"writeStreamDidReceiveResponseWithVersion"]; + [_expectation fulfill]; + _expectation = nil; +} + +/** + * Executes 'block' using the provided FSTDispatchQueue and waits for any callback on this delegate + * to be called. + */ +- (void)awaitNotificationFromBlock:(void (^)(void))block { + FSTAssert(_expectation == nil, @"Previous expectation still active"); + XCTestExpectation *expectation = + [self.testCase expectationWithDescription:@"awaitCallbackInBlock"]; + _expectation = expectation; + [self.dispatchQueue dispatchAsync:block]; + [self.testCase awaitExpectations]; +} + +@end + +@interface FSTStreamTests : XCTestCase + +@end + +@implementation FSTStreamTests { + dispatch_queue_t _testQueue; + FSTTestDispatchQueue *_workerDispatchQueue; + FSTDatabaseInfo *_databaseInfo; + FSTEmptyCredentialsProvider *_credentials; + FSTStreamStatusDelegate *_delegate; + + /** Single mutation to send to the write stream. */ + NSArray *_mutations; +} + +- (void)setUp { + [super setUp]; + + FIRFirestoreSettings *settings = [FSTIntegrationTestCase settings]; + FSTDatabaseID *databaseID = + [FSTDatabaseID databaseIDWithProject:[FSTIntegrationTestCase projectID] + database:kDefaultDatabaseID]; + + _testQueue = dispatch_queue_create("FSTStreamTestWorkerQueue", DISPATCH_QUEUE_SERIAL); + _workerDispatchQueue = [[FSTTestDispatchQueue alloc] initWithQueue:_testQueue]; + + _databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID + persistenceKey:@"test-key" + host:settings.host + sslEnabled:settings.sslEnabled]; + _credentials = [[FSTEmptyCredentialsProvider alloc] init]; + + _delegate = [[FSTStreamStatusDelegate alloc] initWithTestCase:self queue:_workerDispatchQueue]; + + _mutations = @[ FSTTestSetMutation(@"foo/bar", @{}) ]; +} + +- (FSTWriteStream *)setUpWriteStream { + FSTDatastore *datastore = [[FSTDatastore alloc] initWithDatabaseInfo:_databaseInfo + workerDispatchQueue:_workerDispatchQueue + credentials:_credentials]; + return [datastore createWriteStream]; +} + +- (FSTWatchStream *)setUpWatchStream { + FSTDatastore *datastore = [[FSTDatastore alloc] initWithDatabaseInfo:_databaseInfo + workerDispatchQueue:_workerDispatchQueue + credentials:_credentials]; + return [datastore createWatchStream]; +} + +/** + * Drains the test queue and asserts that all the observed callbacks (up to this point) match + * 'expectedStates'. Clears the list of observed callbacks on completion. + */ +- (void)verifyDelegateObservedStates:(NSArray *)expectedStates { + // Drain queue + dispatch_sync(_testQueue, ^{ + }); + + XCTAssertEqualObjects(_delegate.states, expectedStates); + [_delegate.states removeAllObjects]; +} + +/** Verifies that the watch stream does not issue an onClose callback after a call to stop(). */ +- (void)testWatchStreamStopBeforeHandshake { + FSTWatchStream *watchStream = [self setUpWatchStream]; + + [_delegate awaitNotificationFromBlock:^{ + [watchStream startWithDelegate:_delegate]; + }]; + + // Stop must not call watchStreamDidClose because the full implementation of the delegate could + // attempt to restart the stream in the event it had pending watches. + [_workerDispatchQueue dispatchAsync:^{ + [watchStream stop]; + }]; + + // Simulate a final callback from GRPC + [watchStream writesFinishedWithError:nil]; + + [self verifyDelegateObservedStates:@[ @"watchStreamDidOpen" ]]; +} + +/** Verifies that the write stream does not issue an onClose callback after a call to stop(). */ +- (void)testWriteStreamStopBeforeHandshake { + FSTWriteStream *writeStream = [self setUpWriteStream]; + + [_delegate awaitNotificationFromBlock:^{ + [writeStream startWithDelegate:_delegate]; + }]; + + // Don't start the handshake. + + // Stop must not call watchStreamDidClose because the full implementation of the delegate could + // attempt to restart the stream in the event it had pending watches. + [_workerDispatchQueue dispatchAsync:^{ + [writeStream stop]; + }]; + + // Simulate a final callback from GRPC + [writeStream writesFinishedWithError:nil]; + + [self verifyDelegateObservedStates:@[ @"writeStreamDidOpen" ]]; +} + +- (void)testWriteStreamStopAfterHandshake { + FSTWriteStream *writeStream = [self setUpWriteStream]; + + [_delegate awaitNotificationFromBlock:^{ + [writeStream startWithDelegate:_delegate]; + }]; + + // Writing before the handshake should throw + dispatch_sync(_testQueue, ^{ + XCTAssertThrows([writeStream writeMutations:_mutations]); + }); + + [_delegate awaitNotificationFromBlock:^{ + [writeStream writeHandshake]; + }]; + + // Now writes should succeed + [_delegate awaitNotificationFromBlock:^{ + [writeStream writeMutations:_mutations]; + }]; + + [_workerDispatchQueue dispatchAsync:^{ + [writeStream stop]; + }]; + + [self verifyDelegateObservedStates:@[ + @"writeStreamDidOpen", @"writeStreamDidCompleteHandshake", + @"writeStreamDidReceiveResponseWithVersion" + ]]; +} + +- (void)testStreamClosesWhenIdle { + FSTWriteStream *writeStream = [self setUpWriteStream]; + + [_delegate awaitNotificationFromBlock:^{ + [writeStream startWithDelegate:_delegate]; + }]; + + [_delegate awaitNotificationFromBlock:^{ + [writeStream writeHandshake]; + }]; + + [_delegate awaitNotificationFromBlock:^{ + [writeStream markIdle]; + }]; + + dispatch_sync(_testQueue, ^{ + XCTAssertFalse([writeStream isOpen]); + }); + + [self verifyDelegateObservedStates:@[ + @"writeStreamDidOpen", @"writeStreamDidCompleteHandshake", @"writeStreamWasInterrupted" + ]]; +} + +- (void)testStreamCancelsIdleOnWrite { + FSTWriteStream *writeStream = [self setUpWriteStream]; + + [_delegate awaitNotificationFromBlock:^{ + [writeStream startWithDelegate:_delegate]; + }]; + + [_delegate awaitNotificationFromBlock:^{ + [writeStream writeHandshake]; + }]; + + // Mark the stream idle, but immediately cancel the idle timer by issuing another write. + [_delegate awaitNotificationFromBlock:^{ + [writeStream markIdle]; + [writeStream writeMutations:_mutations]; + }]; + + dispatch_sync(_testQueue, ^{ + XCTAssertTrue([writeStream isOpen]); + }); + + [self verifyDelegateObservedStates:@[ + @"writeStreamDidOpen", @"writeStreamDidCompleteHandshake", + @"writeStreamDidReceiveResponseWithVersion" + ]]; +} + +@end diff --git a/Firestore/Example/Tests/Integration/FSTTransactionTests.m b/Firestore/Example/Tests/Integration/FSTTransactionTests.m deleted file mode 100644 index 2e828c9..0000000 --- a/Firestore/Example/Tests/Integration/FSTTransactionTests.m +++ /dev/null @@ -1,541 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 FirebaseFirestore; - -#import -#include - -#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" - -@interface FSTTransactionTests : FSTIntegrationTestCase -@end - -@implementation FSTTransactionTests - -// We currently require every document read to also be written. -// TODO(b/34879758): Re-enable this test once we fix it. -- (void)xtestGetDocuments { - FIRFirestore *firestore = [self firestore]; - FIRDocumentReference *doc = [[firestore collectionWithPath:@"spaces"] documentWithAutoID]; - [self writeDocumentRef:doc data:@{ @"foo" : @1, @"desc" : @"Stuff", @"owner" : @"Jonny" }]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; - [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { - [transaction getDocument:doc error:error]; - XCTAssertNil(*error); - return @YES; - } - completion:^(id _Nullable result, NSError *_Nullable error) { - XCTAssertNil(result); - // We currently require every document read to also be written. - // TODO(b/34879758): Fix this check once we drop that requirement. - XCTAssertNotNil(error); - [expectation fulfill]; - }]; - [self awaitExpectations]; -} - -- (void)testDeleteDocument { - FIRFirestore *firestore = [self firestore]; - FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; - [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; - FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; - XCTAssertEqualObjects(@"bar", snapshot[@"foo"]); - - XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; - [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { - [transaction deleteDocument:doc]; - return @YES; - } - completion:^(id _Nullable result, NSError *_Nullable error) { - XCTAssertEqualObjects(@YES, result); - XCTAssertNil(error); - [expectation fulfill]; - }]; - [self awaitExpectations]; - - snapshot = [self readDocumentForRef:doc]; - XCTAssertFalse(snapshot.exists); -} - -- (void)testGetNonexistentDocumentThenCreate { - FIRFirestore *firestore = [self firestore]; - FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; - [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { - FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; - XCTAssertNil(*error); - XCTAssertFalse(snapshot.exists); - [transaction setData:@{@"foo" : @"bar"} forDocument:doc]; - return @YES; - } - completion:^(id _Nullable result, NSError *_Nullable error) { - XCTAssertEqualObjects(@YES, result); - XCTAssertNil(error); - [expectation fulfill]; - }]; - [self awaitExpectations]; - - FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; - XCTAssertTrue(snapshot.exists); - XCTAssertEqualObjects(@"bar", snapshot[@"foo"]); -} - -- (void)testGetNonexistentDocumentThenFailPatch { - FIRFirestore *firestore = [self firestore]; - FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; - [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { - FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; - XCTAssertNil(*error); - XCTAssertFalse(snapshot.exists); - [transaction updateData:@{@"foo" : @"bar"} forDocument:doc]; - return @YES; - } - completion:^(id _Nullable result, NSError *_Nullable error) { - XCTAssertNil(result); - XCTAssertNotNil(error); - XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); - // TODO(dimond): This is probably the wrong error code, but it's what we use today. We - // should update the code once the underlying error was fixed. - XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition); - [expectation fulfill]; - }]; - [self awaitExpectations]; -} - -- (void)testDeleteDocumentAndPatch { - FIRFirestore *firestore = [self firestore]; - FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; - [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; - [firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) { - FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; - XCTAssertNil(*error); - XCTAssertTrue(snapshot.exists); - [transaction deleteDocument:doc]; - // Since we deleted the doc, the update will fail - [transaction updateData:@{@"foo" : @"bar"} forDocument:doc]; - return @YES; - } - completion:^(id _Nullable result, NSError *_Nullable error) { - XCTAssertNil(result); - XCTAssertNotNil(error); - XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); - // TODO(dimond): This is probably the wrong error code, but it's what we use today. We - // should update the code once the underlying error was fixed. - XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition); - [expectation fulfill]; - }]; - [self awaitExpectations]; -} - -- (void)testDeleteDocumentAndSet { - FIRFirestore *firestore = [self firestore]; - FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; - [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; - [firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) { - FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; - XCTAssertNil(*error); - XCTAssertTrue(snapshot.exists); - [transaction deleteDocument:doc]; - // TODO(dimond): In theory this should work, but it's complex to make it work, so instead we - // just let the transaction fail and verify it's unsupported for now - [transaction setData:@{@"foo" : @"new-bar"} forDocument:doc]; - return @YES; - } - completion:^(id _Nullable result, NSError *_Nullable error) { - XCTAssertNil(result); - XCTAssertNotNil(error); - XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); - // TODO(dimond): This is probably the wrong error code, but it's what we use today. We - // should update the code once the underlying error was fixed. - XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition); - [expectation fulfill]; - }]; - [self awaitExpectations]; -} - -- (void)testWriteDocumentTwice { - FIRFirestore *firestore = [self firestore]; - FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; - [firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) { - [transaction setData:@{@"a" : @"b"} forDocument:doc]; - [transaction setData:@{@"c" : @"d"} forDocument:doc]; - return @YES; - } - completion:^(id _Nullable result, NSError *_Nullable error) { - XCTAssertEqualObjects(@YES, result); - XCTAssertNil(error); - [expectation fulfill]; - }]; - [self awaitExpectations]; - - FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; - XCTAssertEqualObjects(snapshot.data, @{@"c" : @"d"}); -} - -- (void)testSetDocumentWithMerge { - FIRFirestore *firestore = [self firestore]; - FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; - [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { - [transaction setData:@{ @"a" : @"b", @"nested" : @{@"a" : @"b"} } forDocument:doc]; - [transaction setData:@{ - @"c" : @"d", - @"nested" : @{@"c" : @"d"} - } - forDocument:doc - options:[FIRSetOptions merge]]; - return @YES; - } - completion:^(id _Nullable result, NSError *_Nullable error) { - XCTAssertEqualObjects(@YES, result); - XCTAssertNil(error); - [expectation fulfill]; - }]; - [self awaitExpectations]; - - FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; - XCTAssertEqualObjects( - snapshot.data, ( - @{ @"a" : @"b", - @"c" : @"d", - @"nested" : @{@"a" : @"b", @"c" : @"d"} })); -} - -- (void)testCannotUpdateNonExistentDocument { - FIRFirestore *firestore = [self firestore]; - FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; - [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { - [transaction updateData:@{@"foo" : @"bar"} forDocument:doc]; - return nil; - } - completion:^(id _Nullable result, NSError *_Nullable error) { - XCTAssertNotNil(error); - [expectation fulfill]; - }]; - [self awaitExpectations]; - - FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; - XCTAssertFalse(result.exists); -} - -- (void)testIncrementTransactionally { - // A barrier to make sure every transaction reaches the same spot. - dispatch_semaphore_t writeBarrier = dispatch_semaphore_create(0); - __block volatile int32_t started = 0; - - FIRFirestore *firestore = [self firestore]; - FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; - [self writeDocumentRef:doc data:@{ @"count" : @(5.0) }]; - - // Make 3 transactions that will all increment. - int total = 3; - for (int i = 0; i < total; i++) { - XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; - [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { - FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; - XCTAssertNil(*error); - int32_t nowStarted = OSAtomicIncrement32(&started); - // Once all of the transactions have read, allow the first write. - if (nowStarted == total) { - dispatch_semaphore_signal(writeBarrier); - } - - dispatch_semaphore_wait(writeBarrier, DISPATCH_TIME_FOREVER); - // Refill the barrier so that the other transactions and retries succeed. - dispatch_semaphore_signal(writeBarrier); - - double newCount = ((NSNumber *)snapshot[@"count"]).doubleValue + 1.0; - [transaction setData:@{ @"count" : @(newCount) } forDocument:doc]; - return @YES; - - } - completion:^(id _Nullable result, NSError *_Nullable error) { - [expectation fulfill]; - }]; - } - - [self awaitExpectations]; - // Now all transaction should be completed, so check the result. - FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; - XCTAssertEqualObjects(@(5.0 + total), snapshot[@"count"]); -} - -- (void)testUpdateTransactionally { - // A barrier to make sure every transaction reaches the same spot. - dispatch_semaphore_t writeBarrier = dispatch_semaphore_create(0); - __block volatile int32_t started = 0; - - FIRFirestore *firestore = [self firestore]; - FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; - [self writeDocumentRef:doc data:@{ @"count" : @(5.0), @"other" : @"yes" }]; - - // Make 3 transactions that will all increment. - int total = 3; - for (int i = 0; i < total; i++) { - XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; - [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { - FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; - XCTAssertNil(*error); - int32_t nowStarted = OSAtomicIncrement32(&started); - // Once all of the transactions have read, allow the first write. - if (nowStarted == total) { - dispatch_semaphore_signal(writeBarrier); - } - - dispatch_semaphore_wait(writeBarrier, DISPATCH_TIME_FOREVER); - // Refill the barrier so that the other transactions and retries succeed. - dispatch_semaphore_signal(writeBarrier); - - double newCount = ((NSNumber *)snapshot[@"count"]).doubleValue + 1.0; - [transaction updateData:@{ @"count" : @(newCount) } forDocument:doc]; - return @YES; - - } - completion:^(id _Nullable result, NSError *_Nullable error) { - [expectation fulfill]; - }]; - } - - [self awaitExpectations]; - // Now all transaction should be completed, so check the result. - FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; - XCTAssertEqualObjects(@(5.0 + total), snapshot[@"count"]); - XCTAssertEqualObjects(@"yes", snapshot[@"other"]); -} - -// We currently require every document read to also be written. -// TODO(b/34879758): Re-enable this test once we fix it. -- (void)xtestHandleReadingOneDocAndWritingAnother { - FIRFirestore *firestore = [self firestore]; - FIRDocumentReference *doc1 = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; - FIRDocumentReference *doc2 = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; - - [self writeDocumentRef:doc1 data:@{ @"count" : @(15.0) }]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; - [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { - // Get the first doc. - [transaction getDocument:doc1 error:error]; - XCTAssertNil(*error); - // Do a write outside of the transaction. The first time the - // transaction is tried, this will bump the version, which - // will cause the write to doc2 to fail. The second time, it - // will be a no-op and not bump the version. - dispatch_semaphore_t writeSemaphore = dispatch_semaphore_create(0); - [doc1 setData:@{ - @"count" : @(1234) - } - completion:^(NSError *_Nullable error) { - dispatch_semaphore_signal(writeSemaphore); - }]; - // We can block on it, because transactions run on a background queue. - dispatch_semaphore_wait(writeSemaphore, DISPATCH_TIME_FOREVER); - // Now try to update the other doc from within the transaction. - // This should fail once, because we read 15 earlier. - [transaction setData:@{ @"count" : @(16) } forDocument:doc2]; - return nil; - } - completion:^(id _Nullable result, NSError *_Nullable error) { - // We currently require every document read to also be written. - // TODO(b/34879758): Add this check back once we drop that. - // NSError *error = nil; - // FIRDocument *snapshot = [transaction getDocument:doc1 error:&error]; - // XCTAssertNil(error); - // XCTAssertEquals(0, tries); - // XCTAssertEqualObjects(@(1234), snapshot[@"count"]); - // snapshot = [transaction getDocument:doc2 error:&error]; - // XCTAssertNil(error); - // XCTAssertEqualObjects(@(16), snapshot[@"count"]); - XCTAssertNotNil(error); - [expectation fulfill]; - }]; - [self awaitExpectations]; -} - -- (void)testReadingADocTwiceWithDifferentVersions { - FIRFirestore *firestore = [self firestore]; - FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; - [self writeDocumentRef:doc data:@{ @"count" : @(15.0) }]; - XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; - [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { - // Get the doc once. - FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; - XCTAssertNil(*error); - XCTAssertEqualObjects(@(15), snapshot[@"count"]); - // Do a write outside of the transaction. - dispatch_semaphore_t writeSemaphore = dispatch_semaphore_create(0); - [doc setData:@{ - @"count" : @(1234) - } - completion:^(NSError *_Nullable error) { - dispatch_semaphore_signal(writeSemaphore); - }]; - // We can block on it, because transactions run on a background queue. - dispatch_semaphore_wait(writeSemaphore, DISPATCH_TIME_FOREVER); - // Get the doc again in the transaction with the new version. - snapshot = [transaction getDocument:doc error:error]; - // The get itself will fail, because we already read an earlier version of this document. - // TODO(klimt): Perhaps we shouldn't fail reads for this, but should wait and fail the - // whole transaction? It's an edge-case anyway, as developers shouldn't be reading the same - // do multiple times. But they need to handle read errors anyway. - XCTAssertNotNil(*error); - return nil; - } - completion:^(id _Nullable result, NSError *_Nullable error) { - [expectation fulfill]; - }]; - [self awaitExpectations]; - - FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; - XCTAssertEqualObjects(@(1234.0), snapshot[@"count"]); -} - -// We currently require every document read to also be written. -// TODO(b/34879758): Add this test back once we fix that. -- (void)xtestCannotHaveAGetWithoutMutations { - FIRFirestore *firestore = [self firestore]; - FIRDocumentReference *doc = [[firestore collectionWithPath:@"foo"] documentWithAutoID]; - [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; - [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { - FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; - XCTAssertTrue(snapshot.exists); - XCTAssertNil(*error); - return nil; - } - completion:^(id _Nullable result, NSError *_Nullable error) { - XCTAssertNotNil(error); - [expectation fulfill]; - }]; - [self awaitExpectations]; -} - -- (void)testSuccessWithNoTransactionOperations { - FIRFirestore *firestore = [self firestore]; - XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; - [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { - return @"yes"; - } - completion:^(id _Nullable result, NSError *_Nullable error) { - XCTAssertEqualObjects(@"yes", result); - XCTAssertNil(error); - [expectation fulfill]; - }]; - [self awaitExpectations]; -} - -- (void)testCancellationOnError { - FIRFirestore *firestore = [self firestore]; - FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; - __block volatile int32_t count = 0; - XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; - [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { - OSAtomicIncrement32(&count); - [transaction setData:@{@"foo" : @"bar"} forDocument:doc]; - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:35 userInfo:@{}]; - return nil; - } - completion:^(id _Nullable result, NSError *_Nullable error) { - XCTAssertNil(result); - XCTAssertNotNil(error); - XCTAssertEqual(35, error.code); - [expectation fulfill]; - }]; - [self awaitExpectations]; - XCTAssertEqual(1, (int)count); - FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; - XCTAssertFalse(snapshot.exists); -} - -- (void)testUpdateFieldsWithDotsTransactionally { - FIRDocumentReference *doc = [self documentRef]; - - XCTestExpectation *expectation = - [self expectationWithDescription:@"testUpdateFieldsWithDotsTransactionally"]; - - [doc.firestore - runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { - XCTAssertNil(*error); - [transaction setData:@{@"a.b" : @"old", @"c.d" : @"old"} forDocument:doc]; - [transaction updateData:@{ - [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new" - } - forDocument:doc]; - return nil; - } - completion:^(id result, NSError *error) { - XCTAssertNil(error); - [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { - XCTAssertNil(error); - XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"})); - }]; - [expectation fulfill]; - }]; - [self awaitExpectations]; -} - -- (void)testUpdateNestedFieldsTransactionally { - FIRDocumentReference *doc = [self documentRef]; - - XCTestExpectation *expectation = - [self expectationWithDescription:@"testUpdateNestedFieldsTransactionally"]; - - [doc.firestore - runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { - XCTAssertNil(*error); - [transaction setData:@{ - @"a" : @{@"b" : @"old"}, - @"c" : @{@"d" : @"old"}, - @"e" : @{@"f" : @"old"} - } - forDocument:doc]; - [transaction updateData:@{ - @"a.b" : @"new", - [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new" - } - forDocument:doc]; - return nil; - } - completion:^(id result, NSError *error) { - XCTAssertNil(error); - [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { - XCTAssertNil(error); - XCTAssertEqualObjects(snapshot.data, (@{ - @"a" : @{@"b" : @"new"}, - @"c" : @{@"d" : @"new"}, - @"e" : @{@"f" : @"old"} - })); - }]; - [expectation fulfill]; - }]; - [self awaitExpectations]; -} - -@end diff --git a/Firestore/Example/Tests/Integration/FSTTransactionTests.mm b/Firestore/Example/Tests/Integration/FSTTransactionTests.mm new file mode 100644 index 0000000..21803ea --- /dev/null +++ b/Firestore/Example/Tests/Integration/FSTTransactionTests.mm @@ -0,0 +1,541 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import +#include + +#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" + +@interface FSTTransactionTests : FSTIntegrationTestCase +@end + +@implementation FSTTransactionTests + +// We currently require every document read to also be written. +// TODO(b/34879758): Re-enable this test once we fix it. +- (void)xtestGetDocuments { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"spaces"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{ @"foo" : @1, @"desc" : @"Stuff", @"owner" : @"Jonny" }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNil(result); + // We currently require every document read to also be written. + // TODO(b/34879758): Fix this check once we drop that requirement. + XCTAssertNotNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testDeleteDocument { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(@"bar", snapshot[@"foo"]); + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + [transaction deleteDocument:doc]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertEqualObjects(@YES, result); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + snapshot = [self readDocumentForRef:doc]; + XCTAssertFalse(snapshot.exists); +} + +- (void)testGetNonexistentDocumentThenCreate { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + XCTAssertFalse(snapshot.exists); + [transaction setData:@{@"foo" : @"bar"} forDocument:doc]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertEqualObjects(@YES, result); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertTrue(snapshot.exists); + XCTAssertEqualObjects(@"bar", snapshot[@"foo"]); +} + +- (void)testGetNonexistentDocumentThenFailPatch { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + XCTAssertFalse(snapshot.exists); + [transaction updateData:@{@"foo" : @"bar"} forDocument:doc]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNil(result); + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + // TODO(dimond): This is probably the wrong error code, but it's what we use today. We + // should update the code once the underlying error was fixed. + XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testDeleteDocumentAndPatch { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + XCTAssertTrue(snapshot.exists); + [transaction deleteDocument:doc]; + // Since we deleted the doc, the update will fail + [transaction updateData:@{@"foo" : @"bar"} forDocument:doc]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNil(result); + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + // TODO(dimond): This is probably the wrong error code, but it's what we use today. We + // should update the code once the underlying error was fixed. + XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testDeleteDocumentAndSet { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + XCTAssertTrue(snapshot.exists); + [transaction deleteDocument:doc]; + // TODO(dimond): In theory this should work, but it's complex to make it work, so instead we + // just let the transaction fail and verify it's unsupported for now + [transaction setData:@{@"foo" : @"new-bar"} forDocument:doc]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNil(result); + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + // TODO(dimond): This is probably the wrong error code, but it's what we use today. We + // should update the code once the underlying error was fixed. + XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testWriteDocumentTwice { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) { + [transaction setData:@{@"a" : @"b"} forDocument:doc]; + [transaction setData:@{@"c" : @"d"} forDocument:doc]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertEqualObjects(@YES, result); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(snapshot.data, @{@"c" : @"d"}); +} + +- (void)testSetDocumentWithMerge { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + [transaction setData:@{ @"a" : @"b", @"nested" : @{@"a" : @"b"} } forDocument:doc]; + [transaction setData:@{ + @"c" : @"d", + @"nested" : @{@"c" : @"d"} + } + forDocument:doc + options:[FIRSetOptions merge]]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertEqualObjects(@YES, result); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertEqualObjects( + snapshot.data, ( + @{ @"a" : @"b", + @"c" : @"d", + @"nested" : @{@"a" : @"b", @"c" : @"d"} })); +} + +- (void)testCannotUpdateNonExistentDocument { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + [transaction updateData:@{@"foo" : @"bar"} forDocument:doc]; + return nil; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNotNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertFalse(result.exists); +} + +- (void)testIncrementTransactionally { + // A barrier to make sure every transaction reaches the same spot. + dispatch_semaphore_t writeBarrier = dispatch_semaphore_create(0); + __block volatile int32_t started = 0; + + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{ @"count" : @(5.0) }]; + + // Make 3 transactions that will all increment. + int total = 3; + for (int i = 0; i < total; i++) { + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + int32_t nowStarted = OSAtomicIncrement32(&started); + // Once all of the transactions have read, allow the first write. + if (nowStarted == total) { + dispatch_semaphore_signal(writeBarrier); + } + + dispatch_semaphore_wait(writeBarrier, DISPATCH_TIME_FOREVER); + // Refill the barrier so that the other transactions and retries succeed. + dispatch_semaphore_signal(writeBarrier); + + double newCount = ((NSNumber *)snapshot[@"count"]).doubleValue + 1.0; + [transaction setData:@{ @"count" : @(newCount) } forDocument:doc]; + return @YES; + + } + completion:^(id _Nullable result, NSError *_Nullable error) { + [expectation fulfill]; + }]; + } + + [self awaitExpectations]; + // Now all transaction should be completed, so check the result. + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(@(5.0 + total), snapshot[@"count"]); +} + +- (void)testUpdateTransactionally { + // A barrier to make sure every transaction reaches the same spot. + dispatch_semaphore_t writeBarrier = dispatch_semaphore_create(0); + __block volatile int32_t started = 0; + + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{ @"count" : @(5.0), @"other" : @"yes" }]; + + // Make 3 transactions that will all increment. + int total = 3; + for (int i = 0; i < total; i++) { + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + int32_t nowStarted = OSAtomicIncrement32(&started); + // Once all of the transactions have read, allow the first write. + if (nowStarted == total) { + dispatch_semaphore_signal(writeBarrier); + } + + dispatch_semaphore_wait(writeBarrier, DISPATCH_TIME_FOREVER); + // Refill the barrier so that the other transactions and retries succeed. + dispatch_semaphore_signal(writeBarrier); + + double newCount = ((NSNumber *)snapshot[@"count"]).doubleValue + 1.0; + [transaction updateData:@{ @"count" : @(newCount) } forDocument:doc]; + return @YES; + + } + completion:^(id _Nullable result, NSError *_Nullable error) { + [expectation fulfill]; + }]; + } + + [self awaitExpectations]; + // Now all transaction should be completed, so check the result. + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(@(5.0 + total), snapshot[@"count"]); + XCTAssertEqualObjects(@"yes", snapshot[@"other"]); +} + +// We currently require every document read to also be written. +// TODO(b/34879758): Re-enable this test once we fix it. +- (void)xtestHandleReadingOneDocAndWritingAnother { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc1 = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; + FIRDocumentReference *doc2 = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; + + [self writeDocumentRef:doc1 data:@{ @"count" : @(15.0) }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + // Get the first doc. + [transaction getDocument:doc1 error:error]; + XCTAssertNil(*error); + // Do a write outside of the transaction. The first time the + // transaction is tried, this will bump the version, which + // will cause the write to doc2 to fail. The second time, it + // will be a no-op and not bump the version. + dispatch_semaphore_t writeSemaphore = dispatch_semaphore_create(0); + [doc1 setData:@{ + @"count" : @(1234) + } + completion:^(NSError *_Nullable error) { + dispatch_semaphore_signal(writeSemaphore); + }]; + // We can block on it, because transactions run on a background queue. + dispatch_semaphore_wait(writeSemaphore, DISPATCH_TIME_FOREVER); + // Now try to update the other doc from within the transaction. + // This should fail once, because we read 15 earlier. + [transaction setData:@{ @"count" : @(16) } forDocument:doc2]; + return nil; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + // We currently require every document read to also be written. + // TODO(b/34879758): Add this check back once we drop that. + // NSError *error = nil; + // FIRDocument *snapshot = [transaction getDocument:doc1 error:&error]; + // XCTAssertNil(error); + // XCTAssertEquals(0, tries); + // XCTAssertEqualObjects(@(1234), snapshot[@"count"]); + // snapshot = [transaction getDocument:doc2 error:&error]; + // XCTAssertNil(error); + // XCTAssertEqualObjects(@(16), snapshot[@"count"]); + XCTAssertNotNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testReadingADocTwiceWithDifferentVersions { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{ @"count" : @(15.0) }]; + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + // Get the doc once. + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + XCTAssertEqualObjects(@(15), snapshot[@"count"]); + // Do a write outside of the transaction. + dispatch_semaphore_t writeSemaphore = dispatch_semaphore_create(0); + [doc setData:@{ + @"count" : @(1234) + } + completion:^(NSError *_Nullable error) { + dispatch_semaphore_signal(writeSemaphore); + }]; + // We can block on it, because transactions run on a background queue. + dispatch_semaphore_wait(writeSemaphore, DISPATCH_TIME_FOREVER); + // Get the doc again in the transaction with the new version. + snapshot = [transaction getDocument:doc error:error]; + // The get itself will fail, because we already read an earlier version of this document. + // TODO(klimt): Perhaps we shouldn't fail reads for this, but should wait and fail the + // whole transaction? It's an edge-case anyway, as developers shouldn't be reading the same + // do multiple times. But they need to handle read errors anyway. + XCTAssertNotNil(*error); + return nil; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + [expectation fulfill]; + }]; + [self awaitExpectations]; + + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(@(1234.0), snapshot[@"count"]); +} + +// We currently require every document read to also be written. +// TODO(b/34879758): Add this test back once we fix that. +- (void)xtestCannotHaveAGetWithoutMutations { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"foo"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertTrue(snapshot.exists); + XCTAssertNil(*error); + return nil; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNotNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testSuccessWithNoTransactionOperations { + FIRFirestore *firestore = [self firestore]; + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + return @"yes"; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertEqualObjects(@"yes", result); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testCancellationOnError { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + __block volatile int32_t count = 0; + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + OSAtomicIncrement32(&count); + [transaction setData:@{@"foo" : @"bar"} forDocument:doc]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:35 userInfo:@{}]; + return nil; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNil(result); + XCTAssertNotNil(error); + XCTAssertEqual(35, error.code); + [expectation fulfill]; + }]; + [self awaitExpectations]; + XCTAssertEqual(1, (int)count); + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertFalse(snapshot.exists); +} + +- (void)testUpdateFieldsWithDotsTransactionally { + FIRDocumentReference *doc = [self documentRef]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"testUpdateFieldsWithDotsTransactionally"]; + + [doc.firestore + runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + XCTAssertNil(*error); + [transaction setData:@{@"a.b" : @"old", @"c.d" : @"old"} forDocument:doc]; + [transaction updateData:@{ + [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new" + } + forDocument:doc]; + return nil; + } + completion:^(id result, NSError *error) { + XCTAssertNil(error); + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"})); + }]; + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testUpdateNestedFieldsTransactionally { + FIRDocumentReference *doc = [self documentRef]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"testUpdateNestedFieldsTransactionally"]; + + [doc.firestore + runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + XCTAssertNil(*error); + [transaction setData:@{ + @"a" : @{@"b" : @"old"}, + @"c" : @{@"d" : @"old"}, + @"e" : @{@"f" : @"old"} + } + forDocument:doc]; + [transaction updateData:@{ + @"a.b" : @"new", + [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new" + } + forDocument:doc]; + return nil; + } + completion:^(id result, NSError *error) { + XCTAssertNil(error); + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{ + @"a" : @{@"b" : @"new"}, + @"c" : @{@"d" : @"new"}, + @"e" : @{@"f" : @"old"} + })); + }]; + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +@end diff --git a/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m b/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m deleted file mode 100644 index 53f0202..0000000 --- a/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTEagerGarbageCollector.h" - -#import - -#import "Firestore/Source/Local/FSTReferenceSet.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTEagerGarbageCollectorTests : XCTestCase -@end - -@implementation FSTEagerGarbageCollectorTests - -- (void)testAddOrRemoveReferences { - FSTEagerGarbageCollector *gc = [[FSTEagerGarbageCollector alloc] init]; - FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; - [gc addGarbageSource:referenceSet]; - - FSTDocumentKey *key = FSTTestDocKey(@"foo/bar"); - [referenceSet addReferenceToKey:key forID:1]; - FSTAssertEqualSets([gc collectGarbage], @[]); - XCTAssertFalse([referenceSet isEmpty]); - - [referenceSet removeReferenceToKey:key forID:1]; - FSTAssertEqualSets([gc collectGarbage], @[ key ]); - XCTAssertTrue([referenceSet isEmpty]); -} - -- (void)testRemoveAllReferencesForID { - FSTEagerGarbageCollector *gc = [[FSTEagerGarbageCollector alloc] init]; - FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; - [gc addGarbageSource:referenceSet]; - - FSTDocumentKey *key1 = FSTTestDocKey(@"foo/bar"); - FSTDocumentKey *key2 = FSTTestDocKey(@"foo/baz"); - FSTDocumentKey *key3 = FSTTestDocKey(@"foo/blah"); - [referenceSet addReferenceToKey:key1 forID:1]; - [referenceSet addReferenceToKey:key2 forID:1]; - [referenceSet addReferenceToKey:key3 forID:2]; - XCTAssertFalse([referenceSet isEmpty]); - - [referenceSet removeReferencesForID:1]; - FSTAssertEqualSets([gc collectGarbage], (@[ key1, key2 ])); - XCTAssertFalse([referenceSet isEmpty]); - - [referenceSet removeReferencesForID:2]; - FSTAssertEqualSets([gc collectGarbage], @[ key3 ]); - XCTAssertTrue([referenceSet isEmpty]); -} - -- (void)testTwoReferenceSetsAtTheSameTime { - FSTReferenceSet *remoteTargets = [[FSTReferenceSet alloc] init]; - FSTReferenceSet *localViews = [[FSTReferenceSet alloc] init]; - FSTReferenceSet *mutations = [[FSTReferenceSet alloc] init]; - - FSTEagerGarbageCollector *gc = [[FSTEagerGarbageCollector alloc] init]; - [gc addGarbageSource:remoteTargets]; - [gc addGarbageSource:localViews]; - [gc addGarbageSource:mutations]; - - FSTDocumentKey *key1 = FSTTestDocKey(@"foo/bar"); - [remoteTargets addReferenceToKey:key1 forID:1]; - [localViews addReferenceToKey:key1 forID:1]; - [mutations addReferenceToKey:key1 forID:10]; - - FSTDocumentKey *key2 = FSTTestDocKey(@"foo/baz"); - [mutations addReferenceToKey:key2 forID:10]; - - XCTAssertFalse([remoteTargets isEmpty]); - XCTAssertFalse([localViews isEmpty]); - XCTAssertFalse([mutations isEmpty]); - - [localViews removeReferencesForID:1]; - FSTAssertEqualSets([gc collectGarbage], @[]); - - [remoteTargets removeReferencesForID:1]; - FSTAssertEqualSets([gc collectGarbage], @[]); - - [mutations removeReferenceToKey:key1 forID:10]; - FSTAssertEqualSets([gc collectGarbage], @[ key1 ]); - - [mutations removeReferenceToKey:key2 forID:10]; - FSTAssertEqualSets([gc collectGarbage], @[ key2 ]); - - XCTAssertTrue([remoteTargets isEmpty]); - XCTAssertTrue([localViews isEmpty]); - XCTAssertTrue([mutations isEmpty]); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.mm b/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.mm new file mode 100644 index 0000000..53f0202 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.mm @@ -0,0 +1,111 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTEagerGarbageCollector.h" + +#import + +#import "Firestore/Source/Local/FSTReferenceSet.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTEagerGarbageCollectorTests : XCTestCase +@end + +@implementation FSTEagerGarbageCollectorTests + +- (void)testAddOrRemoveReferences { + FSTEagerGarbageCollector *gc = [[FSTEagerGarbageCollector alloc] init]; + FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; + [gc addGarbageSource:referenceSet]; + + FSTDocumentKey *key = FSTTestDocKey(@"foo/bar"); + [referenceSet addReferenceToKey:key forID:1]; + FSTAssertEqualSets([gc collectGarbage], @[]); + XCTAssertFalse([referenceSet isEmpty]); + + [referenceSet removeReferenceToKey:key forID:1]; + FSTAssertEqualSets([gc collectGarbage], @[ key ]); + XCTAssertTrue([referenceSet isEmpty]); +} + +- (void)testRemoveAllReferencesForID { + FSTEagerGarbageCollector *gc = [[FSTEagerGarbageCollector alloc] init]; + FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; + [gc addGarbageSource:referenceSet]; + + FSTDocumentKey *key1 = FSTTestDocKey(@"foo/bar"); + FSTDocumentKey *key2 = FSTTestDocKey(@"foo/baz"); + FSTDocumentKey *key3 = FSTTestDocKey(@"foo/blah"); + [referenceSet addReferenceToKey:key1 forID:1]; + [referenceSet addReferenceToKey:key2 forID:1]; + [referenceSet addReferenceToKey:key3 forID:2]; + XCTAssertFalse([referenceSet isEmpty]); + + [referenceSet removeReferencesForID:1]; + FSTAssertEqualSets([gc collectGarbage], (@[ key1, key2 ])); + XCTAssertFalse([referenceSet isEmpty]); + + [referenceSet removeReferencesForID:2]; + FSTAssertEqualSets([gc collectGarbage], @[ key3 ]); + XCTAssertTrue([referenceSet isEmpty]); +} + +- (void)testTwoReferenceSetsAtTheSameTime { + FSTReferenceSet *remoteTargets = [[FSTReferenceSet alloc] init]; + FSTReferenceSet *localViews = [[FSTReferenceSet alloc] init]; + FSTReferenceSet *mutations = [[FSTReferenceSet alloc] init]; + + FSTEagerGarbageCollector *gc = [[FSTEagerGarbageCollector alloc] init]; + [gc addGarbageSource:remoteTargets]; + [gc addGarbageSource:localViews]; + [gc addGarbageSource:mutations]; + + FSTDocumentKey *key1 = FSTTestDocKey(@"foo/bar"); + [remoteTargets addReferenceToKey:key1 forID:1]; + [localViews addReferenceToKey:key1 forID:1]; + [mutations addReferenceToKey:key1 forID:10]; + + FSTDocumentKey *key2 = FSTTestDocKey(@"foo/baz"); + [mutations addReferenceToKey:key2 forID:10]; + + XCTAssertFalse([remoteTargets isEmpty]); + XCTAssertFalse([localViews isEmpty]); + XCTAssertFalse([mutations isEmpty]); + + [localViews removeReferencesForID:1]; + FSTAssertEqualSets([gc collectGarbage], @[]); + + [remoteTargets removeReferencesForID:1]; + FSTAssertEqualSets([gc collectGarbage], @[]); + + [mutations removeReferenceToKey:key1 forID:10]; + FSTAssertEqualSets([gc collectGarbage], @[ key1 ]); + + [mutations removeReferenceToKey:key2 forID:10]; + FSTAssertEqualSets([gc collectGarbage], @[ key2 ]); + + XCTAssertTrue([remoteTargets isEmpty]); + XCTAssertTrue([localViews isEmpty]); + XCTAssertTrue([mutations isEmpty]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.m b/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.m deleted file mode 100644 index f71f5c9..0000000 --- a/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.m +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTLocalStore.h" - -#import - -#import "Firestore/Source/Auth/FSTUser.h" -#import "Firestore/Source/Local/FSTLevelDB.h" - -#import "Firestore/Example/Tests/Local/FSTLocalStoreTests.h" -#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -/** - * The tests for FSTLevelDBLocalStore are performed on the FSTLocalStore protocol in - * FSTLocalStoreTests. This class is merely responsible for creating a new FSTPersistence - * implementation on demand. - */ -@interface FSTLevelDBLocalStoreTests : FSTLocalStoreTests -@end - -@implementation FSTLevelDBLocalStoreTests - -- (id)persistence { - return [FSTPersistenceTestHelpers levelDBPersistence]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.mm new file mode 100644 index 0000000..f71f5c9 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.mm @@ -0,0 +1,45 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTLocalStore.h" + +#import + +#import "Firestore/Source/Auth/FSTUser.h" +#import "Firestore/Source/Local/FSTLevelDB.h" + +#import "Firestore/Example/Tests/Local/FSTLocalStoreTests.h" +#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The tests for FSTLevelDBLocalStore are performed on the FSTLocalStore protocol in + * FSTLocalStoreTests. This class is merely responsible for creating a new FSTPersistence + * implementation on demand. + */ +@interface FSTLevelDBLocalStoreTests : FSTLocalStoreTests +@end + +@implementation FSTLevelDBLocalStoreTests + +- (id)persistence { + return [FSTPersistenceTestHelpers levelDBPersistence]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.m b/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.m deleted file mode 100644 index 929ab9e..0000000 --- a/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.m +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTLevelDBQueryCache.h" - -#import "Firestore/Source/Local/FSTLevelDB.h" - -#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" -#import "Firestore/Example/Tests/Local/FSTQueryCacheTests.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTLevelDBQueryCacheTests : FSTQueryCacheTests -@end - -/** - * The tests for FSTLevelDBQueryCache are performed on the FSTQueryCache protocol in - * FSTQueryCacheTests. This class is merely responsible for setting up and tearing down the - * @a queryCache. - */ -@implementation FSTLevelDBQueryCacheTests - -- (void)setUp { - [super setUp]; - - self.persistence = [FSTPersistenceTestHelpers levelDBPersistence]; - self.queryCache = [self.persistence queryCache]; - [self.queryCache start]; -} - -- (void)tearDown { - [self.queryCache shutdown]; - self.persistence = nil; - self.queryCache = nil; - - [super tearDown]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.mm new file mode 100644 index 0000000..929ab9e --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.mm @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTLevelDBQueryCache.h" + +#import "Firestore/Source/Local/FSTLevelDB.h" + +#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" +#import "Firestore/Example/Tests/Local/FSTQueryCacheTests.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTLevelDBQueryCacheTests : FSTQueryCacheTests +@end + +/** + * The tests for FSTLevelDBQueryCache are performed on the FSTQueryCache protocol in + * FSTQueryCacheTests. This class is merely responsible for setting up and tearing down the + * @a queryCache. + */ +@implementation FSTLevelDBQueryCacheTests + +- (void)setUp { + [super setUp]; + + self.persistence = [FSTPersistenceTestHelpers levelDBPersistence]; + self.queryCache = [self.persistence queryCache]; + [self.queryCache start]; +} + +- (void)tearDown { + [self.queryCache shutdown]; + self.persistence = nil; + self.queryCache = nil; + + [super tearDown]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLocalSerializerTests.m b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.m deleted file mode 100644 index 95b9b11..0000000 --- a/Firestore/Example/Tests/Local/FSTLocalSerializerTests.m +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTLocalSerializer.h" - -#import - -#import "Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h" -#import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" -#import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h" -#import "Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.h" -#import "Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.h" -#import "Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h" -#import "Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.h" -#import "Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.h" -#import "Firestore/Protos/objc/google/type/Latlng.pbobjc.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Core/FSTTimestamp.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTDatabaseID.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Remote/FSTSerializerBeta.h" - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTSerializerBeta (Test) -- (GCFSValue *)encodedNull; -- (GCFSValue *)encodedBool:(BOOL)value; -- (GCFSValue *)encodedDouble:(double)value; -- (GCFSValue *)encodedInteger:(int64_t)value; -- (GCFSValue *)encodedString:(NSString *)value; -@end - -@interface FSTLocalSerializerTests : XCTestCase - -@property(nonatomic, strong) FSTLocalSerializer *serializer; -@property(nonatomic, strong) FSTSerializerBeta *remoteSerializer; - -@end - -@implementation FSTLocalSerializerTests - -- (void)setUp { - FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; - self.remoteSerializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseID]; - self.serializer = [[FSTLocalSerializer alloc] initWithRemoteSerializer:self.remoteSerializer]; -} - -- (void)testEncodesMutationBatch { - FSTMutation *set = FSTTestSetMutation(@"foo/bar", @{ @"a" : @"b", @"num" : @1 }); - FSTMutation *patch = [[FSTPatchMutation alloc] - initWithKey:FSTTestDocKey(@"bar/baz") - fieldMask:[[FSTFieldMask alloc] initWithFields:@[ FSTTestFieldPath(@"a") ]] - value:FSTTestObjectValue( - @{ @"a" : @"b", - @"num" : @1 }) - precondition:[FSTPrecondition preconditionWithExists:YES]]; - FSTMutation *del = FSTTestDeleteMutation(@"baz/quux"); - FSTTimestamp *writeTime = [FSTTimestamp timestamp]; - FSTMutationBatch *model = [[FSTMutationBatch alloc] initWithBatchID:42 - localWriteTime:writeTime - mutations:@[ set, patch, del ]]; - - GCFSWrite *setProto = [GCFSWrite message]; - setProto.update.name = @"projects/p/databases/d/documents/foo/bar"; - [setProto.update.fields addEntriesFromDictionary:@{ - @"a" : [self.remoteSerializer encodedString:@"b"], - @"num" : [self.remoteSerializer encodedInteger:1] - }]; - - GCFSWrite *patchProto = [GCFSWrite message]; - patchProto.update.name = @"projects/p/databases/d/documents/bar/baz"; - [patchProto.update.fields addEntriesFromDictionary:@{ - @"a" : [self.remoteSerializer encodedString:@"b"], - @"num" : [self.remoteSerializer encodedInteger:1] - }]; - [patchProto.updateMask.fieldPathsArray addObjectsFromArray:@[ @"a" ]]; - patchProto.currentDocument.exists = YES; - - GCFSWrite *delProto = [GCFSWrite message]; - delProto.delete_p = @"projects/p/databases/d/documents/baz/quux"; - - GPBTimestamp *writeTimeProto = [GPBTimestamp message]; - writeTimeProto.seconds = writeTime.seconds; - writeTimeProto.nanos = writeTime.nanos; - - FSTPBWriteBatch *batchProto = [FSTPBWriteBatch message]; - batchProto.batchId = 42; - [batchProto.writesArray addObjectsFromArray:@[ setProto, patchProto, delProto ]]; - batchProto.localWriteTime = writeTimeProto; - - XCTAssertEqualObjects([self.serializer encodedMutationBatch:model], batchProto); - FSTMutationBatch *decoded = [self.serializer decodedMutationBatch:batchProto]; - XCTAssertEqual(decoded.batchID, model.batchID); - XCTAssertEqualObjects(decoded.localWriteTime, model.localWriteTime); - XCTAssertEqualObjects(decoded.mutations, model.mutations); - XCTAssertEqualObjects([decoded keys], [model keys]); -} - -- (void)testEncodesDocumentAsMaybeDocument { - FSTDocument *doc = FSTTestDoc(@"some/path", 42, @{@"foo" : @"bar"}, NO); - - FSTPBMaybeDocument *maybeDocProto = [FSTPBMaybeDocument message]; - maybeDocProto.document = [GCFSDocument message]; - maybeDocProto.document.name = @"projects/p/databases/d/documents/some/path"; - [maybeDocProto.document.fields addEntriesFromDictionary:@{ - @"foo" : [self.remoteSerializer encodedString:@"bar"], - }]; - maybeDocProto.document.updateTime.seconds = 0; - maybeDocProto.document.updateTime.nanos = 42000; - - XCTAssertEqualObjects([self.serializer encodedMaybeDocument:doc], maybeDocProto); - FSTMaybeDocument *decoded = [self.serializer decodedMaybeDocument:maybeDocProto]; - XCTAssertEqualObjects(decoded, doc); -} - -- (void)testEncodesDeletedDocumentAsMaybeDocument { - FSTDeletedDocument *deletedDoc = FSTTestDeletedDoc(@"some/path", 42); - - FSTPBMaybeDocument *maybeDocProto = [FSTPBMaybeDocument message]; - maybeDocProto.noDocument = [FSTPBNoDocument message]; - maybeDocProto.noDocument.name = @"projects/p/databases/d/documents/some/path"; - maybeDocProto.noDocument.readTime.seconds = 0; - maybeDocProto.noDocument.readTime.nanos = 42000; - - XCTAssertEqualObjects([self.serializer encodedMaybeDocument:deletedDoc], maybeDocProto); - FSTMaybeDocument *decoded = [self.serializer decodedMaybeDocument:maybeDocProto]; - XCTAssertEqualObjects(decoded, deletedDoc); -} - -- (void)testEncodesQueryData { - FSTQuery *query = FSTTestQuery(@"room"); - FSTTargetID targetID = 42; - FSTSnapshotVersion *version = FSTTestVersion(1039); - NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1039); - - FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query - targetID:targetID - listenSequenceNumber:10 - purpose:FSTQueryPurposeListen - snapshotVersion:version - resumeToken:resumeToken]; - - // Let the RPC serializer test various permutations of query serialization. - GCFSTarget_QueryTarget *queryTarget = [self.remoteSerializer encodedQueryTarget:query]; - - FSTPBTarget *expected = [FSTPBTarget message]; - expected.targetId = targetID; - expected.lastListenSequenceNumber = 10; - expected.snapshotVersion.nanos = 1039000; - expected.resumeToken = [resumeToken copy]; - expected.query.parent = queryTarget.parent; - expected.query.structuredQuery = queryTarget.structuredQuery; - - XCTAssertEqualObjects([self.serializer encodedQueryData:queryData], expected); - FSTQueryData *decoded = [self.serializer decodedQueryData:expected]; - XCTAssertEqualObjects(decoded, queryData); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm new file mode 100644 index 0000000..95b9b11 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm @@ -0,0 +1,183 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTLocalSerializer.h" + +#import + +#import "Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h" +#import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" +#import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h" +#import "Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.h" +#import "Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.h" +#import "Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h" +#import "Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.h" +#import "Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.h" +#import "Firestore/Protos/objc/google/type/Latlng.pbobjc.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Core/FSTTimestamp.h" +#import "Firestore/Source/Local/FSTQueryData.h" +#import "Firestore/Source/Model/FSTDatabaseID.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Model/FSTMutationBatch.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Remote/FSTSerializerBeta.h" + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTSerializerBeta (Test) +- (GCFSValue *)encodedNull; +- (GCFSValue *)encodedBool:(BOOL)value; +- (GCFSValue *)encodedDouble:(double)value; +- (GCFSValue *)encodedInteger:(int64_t)value; +- (GCFSValue *)encodedString:(NSString *)value; +@end + +@interface FSTLocalSerializerTests : XCTestCase + +@property(nonatomic, strong) FSTLocalSerializer *serializer; +@property(nonatomic, strong) FSTSerializerBeta *remoteSerializer; + +@end + +@implementation FSTLocalSerializerTests + +- (void)setUp { + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; + self.remoteSerializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseID]; + self.serializer = [[FSTLocalSerializer alloc] initWithRemoteSerializer:self.remoteSerializer]; +} + +- (void)testEncodesMutationBatch { + FSTMutation *set = FSTTestSetMutation(@"foo/bar", @{ @"a" : @"b", @"num" : @1 }); + FSTMutation *patch = [[FSTPatchMutation alloc] + initWithKey:FSTTestDocKey(@"bar/baz") + fieldMask:[[FSTFieldMask alloc] initWithFields:@[ FSTTestFieldPath(@"a") ]] + value:FSTTestObjectValue( + @{ @"a" : @"b", + @"num" : @1 }) + precondition:[FSTPrecondition preconditionWithExists:YES]]; + FSTMutation *del = FSTTestDeleteMutation(@"baz/quux"); + FSTTimestamp *writeTime = [FSTTimestamp timestamp]; + FSTMutationBatch *model = [[FSTMutationBatch alloc] initWithBatchID:42 + localWriteTime:writeTime + mutations:@[ set, patch, del ]]; + + GCFSWrite *setProto = [GCFSWrite message]; + setProto.update.name = @"projects/p/databases/d/documents/foo/bar"; + [setProto.update.fields addEntriesFromDictionary:@{ + @"a" : [self.remoteSerializer encodedString:@"b"], + @"num" : [self.remoteSerializer encodedInteger:1] + }]; + + GCFSWrite *patchProto = [GCFSWrite message]; + patchProto.update.name = @"projects/p/databases/d/documents/bar/baz"; + [patchProto.update.fields addEntriesFromDictionary:@{ + @"a" : [self.remoteSerializer encodedString:@"b"], + @"num" : [self.remoteSerializer encodedInteger:1] + }]; + [patchProto.updateMask.fieldPathsArray addObjectsFromArray:@[ @"a" ]]; + patchProto.currentDocument.exists = YES; + + GCFSWrite *delProto = [GCFSWrite message]; + delProto.delete_p = @"projects/p/databases/d/documents/baz/quux"; + + GPBTimestamp *writeTimeProto = [GPBTimestamp message]; + writeTimeProto.seconds = writeTime.seconds; + writeTimeProto.nanos = writeTime.nanos; + + FSTPBWriteBatch *batchProto = [FSTPBWriteBatch message]; + batchProto.batchId = 42; + [batchProto.writesArray addObjectsFromArray:@[ setProto, patchProto, delProto ]]; + batchProto.localWriteTime = writeTimeProto; + + XCTAssertEqualObjects([self.serializer encodedMutationBatch:model], batchProto); + FSTMutationBatch *decoded = [self.serializer decodedMutationBatch:batchProto]; + XCTAssertEqual(decoded.batchID, model.batchID); + XCTAssertEqualObjects(decoded.localWriteTime, model.localWriteTime); + XCTAssertEqualObjects(decoded.mutations, model.mutations); + XCTAssertEqualObjects([decoded keys], [model keys]); +} + +- (void)testEncodesDocumentAsMaybeDocument { + FSTDocument *doc = FSTTestDoc(@"some/path", 42, @{@"foo" : @"bar"}, NO); + + FSTPBMaybeDocument *maybeDocProto = [FSTPBMaybeDocument message]; + maybeDocProto.document = [GCFSDocument message]; + maybeDocProto.document.name = @"projects/p/databases/d/documents/some/path"; + [maybeDocProto.document.fields addEntriesFromDictionary:@{ + @"foo" : [self.remoteSerializer encodedString:@"bar"], + }]; + maybeDocProto.document.updateTime.seconds = 0; + maybeDocProto.document.updateTime.nanos = 42000; + + XCTAssertEqualObjects([self.serializer encodedMaybeDocument:doc], maybeDocProto); + FSTMaybeDocument *decoded = [self.serializer decodedMaybeDocument:maybeDocProto]; + XCTAssertEqualObjects(decoded, doc); +} + +- (void)testEncodesDeletedDocumentAsMaybeDocument { + FSTDeletedDocument *deletedDoc = FSTTestDeletedDoc(@"some/path", 42); + + FSTPBMaybeDocument *maybeDocProto = [FSTPBMaybeDocument message]; + maybeDocProto.noDocument = [FSTPBNoDocument message]; + maybeDocProto.noDocument.name = @"projects/p/databases/d/documents/some/path"; + maybeDocProto.noDocument.readTime.seconds = 0; + maybeDocProto.noDocument.readTime.nanos = 42000; + + XCTAssertEqualObjects([self.serializer encodedMaybeDocument:deletedDoc], maybeDocProto); + FSTMaybeDocument *decoded = [self.serializer decodedMaybeDocument:maybeDocProto]; + XCTAssertEqualObjects(decoded, deletedDoc); +} + +- (void)testEncodesQueryData { + FSTQuery *query = FSTTestQuery(@"room"); + FSTTargetID targetID = 42; + FSTSnapshotVersion *version = FSTTestVersion(1039); + NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1039); + + FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query + targetID:targetID + listenSequenceNumber:10 + purpose:FSTQueryPurposeListen + snapshotVersion:version + resumeToken:resumeToken]; + + // Let the RPC serializer test various permutations of query serialization. + GCFSTarget_QueryTarget *queryTarget = [self.remoteSerializer encodedQueryTarget:query]; + + FSTPBTarget *expected = [FSTPBTarget message]; + expected.targetId = targetID; + expected.lastListenSequenceNumber = 10; + expected.snapshotVersion.nanos = 1039000; + expected.resumeToken = [resumeToken copy]; + expected.query.parent = queryTarget.parent; + expected.query.structuredQuery = queryTarget.structuredQuery; + + XCTAssertEqualObjects([self.serializer encodedQueryData:queryData], expected); + FSTQueryData *decoded = [self.serializer decodedQueryData:expected]; + XCTAssertEqualObjects(decoded, queryData); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.m b/Firestore/Example/Tests/Local/FSTLocalStoreTests.m deleted file mode 100644 index 45d1815..0000000 --- a/Firestore/Example/Tests/Local/FSTLocalStoreTests.m +++ /dev/null @@ -1,794 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTLocalStore.h" - -#import - -#import "Firestore/Source/Auth/FSTUser.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTTimestamp.h" -#import "Firestore/Source/Local/FSTEagerGarbageCollector.h" -#import "Firestore/Source/Local/FSTLocalWriteResult.h" -#import "Firestore/Source/Local/FSTNoOpGarbageCollector.h" -#import "Firestore/Source/Local/FSTPersistence.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" -#import "Firestore/Source/Remote/FSTWatchChange.h" -#import "Firestore/Source/Util/FSTClasses.h" - -#import "Firestore/Example/Tests/Local/FSTLocalStoreTests.h" -#import "Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h" -#import "Firestore/Example/Tests/Util/FSTHelpers.h" -#import "Firestore/third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.h" -#import "Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.h" - -NS_ASSUME_NONNULL_BEGIN - -/** Creates a document version dictionary mapping the document in @a mutation to @a version. */ -FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, - FSTTestSnapshotVersion version) { - FSTDocumentVersionDictionary *result = [FSTDocumentVersionDictionary documentVersionDictionary]; - result = [result dictionaryBySettingObject:FSTTestVersion(version) forKey:mutation.key]; - return result; -} - -@interface FSTLocalStoreTests () - -@property(nonatomic, strong, readwrite) id localStorePersistence; -@property(nonatomic, strong, readwrite) FSTLocalStore *localStore; - -@property(nonatomic, strong, readonly) NSMutableArray *batches; -@property(nonatomic, strong, readwrite, nullable) FSTMaybeDocumentDictionary *lastChanges; -@property(nonatomic, assign, readwrite) FSTTargetID lastTargetID; - -@end - -@implementation FSTLocalStoreTests - -- (void)setUp { - [super setUp]; - - if ([self isTestBaseClass]) { - return; - } - - id persistence = [self persistence]; - self.localStorePersistence = persistence; - id garbageCollector = [[FSTEagerGarbageCollector alloc] init]; - self.localStore = [[FSTLocalStore alloc] initWithPersistence:persistence - garbageCollector:garbageCollector - initialUser:[FSTUser unauthenticatedUser]]; - [self.localStore start]; - - _batches = [NSMutableArray array]; - _lastChanges = nil; - _lastTargetID = 0; -} - -- (void)tearDown { - [self.localStore shutdown]; - [self.localStorePersistence shutdown]; - - [super tearDown]; -} - -- (id)persistence { - @throw FSTAbstractMethodException(); // NOLINT -} - -/** - * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for - * FSTLocalStoreTests since it is incomplete without the implementations supplied by its - * subclasses. - */ -- (BOOL)isTestBaseClass { - return [self class] == [FSTLocalStoreTests class]; -} - -/** Restarts the local store using the FSTNoOpGarbageCollector instead of the default. */ -- (void)restartWithNoopGarbageCollector { - [self.localStore shutdown]; - - id garbageCollector = [[FSTNoOpGarbageCollector alloc] init]; - self.localStore = [[FSTLocalStore alloc] initWithPersistence:self.localStorePersistence - garbageCollector:garbageCollector - initialUser:[FSTUser unauthenticatedUser]]; - [self.localStore start]; -} - -- (void)writeMutation:(FSTMutation *)mutation { - [self writeMutations:@[ mutation ]]; -} - -- (void)writeMutations:(NSArray *)mutations { - FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:mutations]; - XCTAssertNotNil(result); - [self.batches addObject:[[FSTMutationBatch alloc] initWithBatchID:result.batchID - localWriteTime:[FSTTimestamp timestamp] - mutations:mutations]]; - self.lastChanges = result.changes; -} - -- (void)applyRemoteEvent:(FSTRemoteEvent *)event { - self.lastChanges = [self.localStore applyRemoteEvent:event]; -} - -- (void)notifyLocalViewChanges:(FSTLocalViewChanges *)changes { - [self.localStore notifyLocalViewChanges:@[ changes ]]; -} - -- (void)acknowledgeMutationWithVersion:(FSTTestSnapshotVersion)documentVersion { - FSTMutationBatch *batch = [self.batches firstObject]; - [self.batches removeObjectAtIndex:0]; - XCTAssertEqual(batch.mutations.count, 1, @"Acknowledging more than one mutation not supported."); - FSTSnapshotVersion *version = FSTTestVersion(documentVersion); - FSTMutationResult *mutationResult = - [[FSTMutationResult alloc] initWithVersion:version transformResults:nil]; - FSTMutationBatchResult *result = [FSTMutationBatchResult resultWithBatch:batch - commitVersion:version - mutationResults:@[ mutationResult ] - streamToken:nil]; - self.lastChanges = [self.localStore acknowledgeBatchWithResult:result]; -} - -- (void)rejectMutation { - FSTMutationBatch *batch = [self.batches firstObject]; - [self.batches removeObjectAtIndex:0]; - self.lastChanges = [self.localStore rejectBatchID:batch.batchID]; -} - -- (void)allocateQuery:(FSTQuery *)query { - FSTQueryData *queryData = [self.localStore allocateQuery:query]; - self.lastTargetID = queryData.targetID; -} - -- (void)collectGarbage { - [self.localStore collectGarbage]; -} - -/** Asserts that the last target ID is the given number. */ -#define FSTAssertTargetID(targetID) \ - do { \ - XCTAssertEqual(self.lastTargetID, targetID); \ - } while (0) - -/** Asserts that a the lastChanges contain the docs in the given array. */ -#define FSTAssertChanged(documents) \ - XCTAssertNotNil(self.lastChanges); \ - do { \ - FSTMaybeDocumentDictionary *actual = self.lastChanges; \ - NSArray *expected = (documents); \ - XCTAssertEqual(actual.count, expected.count); \ - NSEnumerator *enumerator = expected.objectEnumerator; \ - [actual enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey * key, FSTMaybeDocument * value, \ - BOOL * stop) { \ - XCTAssertEqualObjects(value, [enumerator nextObject]); \ - }]; \ - self.lastChanges = nil; \ - } while (0) - -/** Asserts that the given keys were removed. */ -#define FSTAssertRemoved(keyPaths) \ - XCTAssertNotNil(self.lastChanges); \ - do { \ - FSTMaybeDocumentDictionary *actual = self.lastChanges; \ - XCTAssertEqual(actual.count, keyPaths.count); \ - NSEnumerator *keyPathEnumerator = keyPaths.objectEnumerator; \ - [actual enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey * actualKey, \ - FSTMaybeDocument * value, BOOL * stop) { \ - FSTDocumentKey *expectedKey = FSTTestDocKey([keyPathEnumerator nextObject]); \ - XCTAssertEqualObjects(actualKey, expectedKey); \ - XCTAssertTrue([value isKindOfClass:[FSTDeletedDocument class]]); \ - }]; \ - self.lastChanges = nil; \ - } while (0) - -/** Asserts that the given local store contains the given document. */ -#define FSTAssertContains(document) \ - do { \ - FSTMaybeDocument *expected = (document); \ - FSTMaybeDocument *actual = [self.localStore readDocument:expected.key]; \ - XCTAssertEqualObjects(actual, expected); \ - } while (0) - -/** Asserts that the given local store does not contain the given document. */ -#define FSTAssertNotContains(keyPathString) \ - do { \ - FSTDocumentKey *key = FSTTestDocKey(keyPathString); \ - FSTMaybeDocument *actual = [self.localStore readDocument:key]; \ - XCTAssertNil(actual); \ - } while (0) - -- (void)testMutationBatchKeys { - if ([self isTestBaseClass]) return; - - FSTMutation *set1 = FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}); - FSTMutation *set2 = FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"}); - FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:1 - localWriteTime:[FSTTimestamp timestamp] - mutations:@[ set1, set2 ]]; - FSTDocumentKeySet *keys = [batch keys]; - XCTAssertEqual(keys.count, 2); -} - -- (void)testHandlesSetMutation { - if ([self isTestBaseClass]) return; - - [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); - - [self acknowledgeMutationWithVersion:0]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO)); -} - -- (void)testHandlesSetMutationThenDocument { - if ([self isTestBaseClass]) return; - - [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent( - FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES)); -} - -- (void)testHandlesAckThenRejectThenRemoteEvent { - if ([self isTestBaseClass]) return; - - // Start a query that requires acks to be held. - FSTQuery *query = FSTTestQuery(@"foo"); - [self allocateQuery:query]; - - [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); - - // The last seen version is zero, so this ack must be held. - [self acknowledgeMutationWithVersion:1]; - FSTAssertChanged(@[]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); - - [self writeMutation:FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"})]; - FSTAssertChanged(@[ FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES) ]); - FSTAssertContains(FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES)); - - [self rejectMutation]; - FSTAssertRemoved(@[ @"bar/baz" ]); - FSTAssertNotContains(@"bar/baz"); - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent( - FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO)); - FSTAssertNotContains(@"bar/baz"); -} - -- (void)testHandlesDeletedDocumentThenSetMutationThenAck { - if ([self isTestBaseClass]) return; - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])]; - FSTAssertRemoved(@[ @"foo/bar" ]); - FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 2)); - - [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); - - [self acknowledgeMutationWithVersion:3]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO)); -} - -- (void)testHandlesSetMutationThenDeletedDocument { - if ([self isTestBaseClass]) return; - - [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); -} - -- (void)testHandlesDocumentThenSetMutationThenAckThenDocument { - if ([self isTestBaseClass]) return; - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"it" : @"base"}, NO), - @[ @1 ], @[])]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"it" : @"base"}, NO) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"it" : @"base"}, NO)); - - [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES)); - - [self acknowledgeMutationWithVersion:3]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO)); - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent( - FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO)); -} - -- (void)testHandlesPatchWithoutPriorDocument { - if ([self isTestBaseClass]) return; - - [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; - FSTAssertRemoved(@[ @"foo/bar" ]); - FSTAssertNotContains(@"foo/bar"); - - [self acknowledgeMutationWithVersion:1]; - FSTAssertRemoved(@[ @"foo/bar" ]); - FSTAssertNotContains(@"foo/bar"); -} - -- (void)testHandlesPatchMutationThenDocumentThenAck { - if ([self isTestBaseClass]) return; - - [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; - FSTAssertRemoved(@[ @"foo/bar" ]); - FSTAssertNotContains(@"foo/bar"); - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), - @[ @1 ], @[])]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, YES) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, YES)); - - [self acknowledgeMutationWithVersion:2]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, NO) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, NO)); -} - -- (void)testHandlesPatchMutationThenAckThenDocument { - if ([self isTestBaseClass]) return; - - [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; - FSTAssertRemoved(@[ @"foo/bar" ]); - FSTAssertNotContains(@"foo/bar"); - - [self acknowledgeMutationWithVersion:1]; - FSTAssertRemoved(@[ @"foo/bar" ]); - FSTAssertNotContains(@"foo/bar"); - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), - @[ @1 ], @[])]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO)); -} - -- (void)testHandlesDeleteMutationThenAck { - if ([self isTestBaseClass]) return; - - [self writeMutation:FSTTestDeleteMutation(@"foo/bar")]; - FSTAssertRemoved(@[ @"foo/bar" ]); - FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); - - [self acknowledgeMutationWithVersion:1]; - FSTAssertRemoved(@[ @"foo/bar" ]); - FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); -} - -- (void)testHandlesDocumentThenDeleteMutationThenAck { - if ([self isTestBaseClass]) return; - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), - @[ @1 ], @[])]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO)); - - [self writeMutation:FSTTestDeleteMutation(@"foo/bar")]; - FSTAssertRemoved(@[ @"foo/bar" ]); - FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); - - [self acknowledgeMutationWithVersion:2]; - FSTAssertRemoved(@[ @"foo/bar" ]); - FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); -} - -- (void)testHandlesDeleteMutationThenDocumentThenAck { - if ([self isTestBaseClass]) return; - - [self writeMutation:FSTTestDeleteMutation(@"foo/bar")]; - FSTAssertRemoved(@[ @"foo/bar" ]); - FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), - @[ @1 ], @[])]; - FSTAssertRemoved(@[ @"foo/bar" ]); - FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); - - [self acknowledgeMutationWithVersion:2]; - FSTAssertRemoved(@[ @"foo/bar" ]); - FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); -} - -- (void)testHandlesDocumentThenDeletedDocumentThenDocument { - if ([self isTestBaseClass]) return; - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), - @[ @1 ], @[])]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO)); - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])]; - FSTAssertRemoved(@[ @"foo/bar" ]); - FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 2)); - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent( - FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO)); -} - -- (void)testHandlesSetMutationThenPatchMutationThenDocumentThenAckThenAck { - if ([self isTestBaseClass]) return; - - [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"})]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, YES) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, YES)); - - [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), - @[ @1 ], @[])]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES)); - - [self acknowledgeMutationWithVersion:2]; // delete mutation - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES)); - - [self acknowledgeMutationWithVersion:3]; // patch mutation - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO)); -} - -- (void)testHandlesSetMutationAndPatchMutationTogether { - if ([self isTestBaseClass]) return; - - [self writeMutations:@[ - FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}), - FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil) - ]]; - - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); -} - -- (void)testHandlesSetMutationThenPatchMutationThenReject { - if ([self isTestBaseClass]) return; - - [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"})]; - [self acknowledgeMutationWithVersion:1]; - FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO)); - - [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; - FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); - - [self rejectMutation]; - FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO) ]); - FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO)); -} - -- (void)testHandlesSetMutationsAndPatchMutationOfJustOneTogether { - if ([self isTestBaseClass]) return; - - [self writeMutations:@[ - FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}), - FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"}), - FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil) - ]]; - - FSTAssertChanged((@[ - FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES), - FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) - ])); - FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); - FSTAssertContains(FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES)); -} - -- (void)testHandlesDeleteMutationThenPatchMutationThenAckThenAck { - if ([self isTestBaseClass]) return; - - [self writeMutation:FSTTestDeleteMutation(@"foo/bar")]; - FSTAssertRemoved(@[ @"foo/bar" ]); - FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); - - [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; - FSTAssertRemoved(@[ @"foo/bar" ]); - FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); - - [self acknowledgeMutationWithVersion:2]; // delete mutation - FSTAssertRemoved(@[ @"foo/bar" ]); - FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); - - [self acknowledgeMutationWithVersion:3]; // patch mutation - FSTAssertRemoved(@[ @"foo/bar" ]); - FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); -} - -- (void)testCollectsGarbageAfterChangeBatchWithNoTargetIDs { - if ([self isTestBaseClass]) return; - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])]; - FSTAssertRemoved(@[ @"foo/bar" ]); - - [self collectGarbage]; - FSTAssertNotContains(@"foo/bar"); - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO), - @[ @1 ], @[])]; - [self collectGarbage]; - FSTAssertNotContains(@"foo/bar"); -} - -- (void)testCollectsGarbageAfterChangeBatch { - if ([self isTestBaseClass]) return; - - FSTQuery *query = FSTTestQuery(@"foo"); - [self allocateQuery:query]; - FSTAssertTargetID(2); - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO), - @[ @2 ], @[])]; - [self collectGarbage]; - FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO)); - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"baz"}, NO), - @[], @[ @2 ])]; - [self collectGarbage]; - - FSTAssertNotContains(@"foo/bar"); -} - -- (void)testCollectsGarbageAfterAcknowledgedMutation { - if ([self isTestBaseClass]) return; - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO), - @[ @1 ], @[])]; - [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; - [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})]; - [self writeMutation:FSTTestDeleteMutation(@"foo/baz")]; - [self collectGarbage]; - FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); - FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES)); - FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); - - [self acknowledgeMutationWithVersion:3]; - [self collectGarbage]; - FSTAssertNotContains(@"foo/bar"); - FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES)); - FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); - - [self acknowledgeMutationWithVersion:4]; - [self collectGarbage]; - FSTAssertNotContains(@"foo/bar"); - FSTAssertNotContains(@"foo/bah"); - FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); - - [self acknowledgeMutationWithVersion:5]; - [self collectGarbage]; - FSTAssertNotContains(@"foo/bar"); - FSTAssertNotContains(@"foo/bah"); - FSTAssertNotContains(@"foo/baz"); -} - -- (void)testCollectsGarbageAfterRejectedMutation { - if ([self isTestBaseClass]) return; - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO), - @[ @1 ], @[])]; - [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; - [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})]; - [self writeMutation:FSTTestDeleteMutation(@"foo/baz")]; - [self collectGarbage]; - FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); - FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES)); - FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); - - [self rejectMutation]; // patch mutation - [self collectGarbage]; - FSTAssertNotContains(@"foo/bar"); - FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES)); - FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); - - [self rejectMutation]; // set mutation - [self collectGarbage]; - FSTAssertNotContains(@"foo/bar"); - FSTAssertNotContains(@"foo/bah"); - FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); - - [self rejectMutation]; // delete mutation - [self collectGarbage]; - FSTAssertNotContains(@"foo/bar"); - FSTAssertNotContains(@"foo/bah"); - FSTAssertNotContains(@"foo/baz"); -} - -- (void)testPinsDocumentsInTheLocalView { - if ([self isTestBaseClass]) return; - - FSTQuery *query = FSTTestQuery(@"foo"); - [self allocateQuery:query]; - FSTAssertTargetID(2); - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO), - @[ @2 ], @[])]; - [self writeMutation:FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"})]; - [self collectGarbage]; - FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO)); - FSTAssertContains(FSTTestDoc(@"foo/baz", 0, @{@"foo" : @"baz"}, YES)); - - [self notifyLocalViewChanges:FSTTestViewChanges(query, @[ @"foo/bar", @"foo/baz" ], @[])]; - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO), - @[], @[ @2 ])]; - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/baz", 2, @{@"foo" : @"baz"}, NO), - @[ @1 ], @[])]; - [self acknowledgeMutationWithVersion:2]; - [self collectGarbage]; - FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO)); - FSTAssertContains(FSTTestDoc(@"foo/baz", 2, @{@"foo" : @"baz"}, NO)); - - [self notifyLocalViewChanges:FSTTestViewChanges(query, @[], @[ @"foo/bar", @"foo/baz" ])]; - [self collectGarbage]; - - FSTAssertNotContains(@"foo/bar"); - FSTAssertNotContains(@"foo/baz"); -} - -- (void)testThrowsAwayDocumentsWithUnknownTargetIDsImmediately { - if ([self isTestBaseClass]) return; - - FSTTargetID targetID = 321; - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{}, NO), - @[ @(targetID) ], @[])]; - FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{}, NO)); - - [self collectGarbage]; - FSTAssertNotContains(@"foo/bar"); -} - -- (void)testCanExecuteDocumentQueries { - if ([self isTestBaseClass]) return; - - [self.localStore locallyWriteMutations:@[ - FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}), - FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}), - FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}) - ]]; - FSTQuery *query = FSTTestQuery(@"foo/bar"); - FSTDocumentDictionary *docs = [self.localStore executeQuery:query]; - XCTAssertEqualObjects([docs values], @[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); -} - -- (void)testCanExecuteCollectionQueries { - if ([self isTestBaseClass]) return; - - [self.localStore locallyWriteMutations:@[ - FSTTestSetMutation(@"fo/bar", @{@"fo" : @"bar"}), - FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}), - FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}), - FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}), - FSTTestSetMutation(@"fooo/blah", @{@"fooo" : @"blah"}) - ]]; - FSTQuery *query = FSTTestQuery(@"foo"); - FSTDocumentDictionary *docs = [self.localStore executeQuery:query]; - XCTAssertEqualObjects([docs values], (@[ - FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES), - FSTTestDoc(@"foo/baz", 0, @{@"foo" : @"baz"}, YES) - ])); -} - -- (void)testCanExecuteMixedCollectionQueries { - if ([self isTestBaseClass]) return; - - FSTQuery *query = FSTTestQuery(@"foo"); - [self allocateQuery:query]; - FSTAssertTargetID(2); - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/baz", 10, @{@"a" : @"b"}, NO), - @[ @2 ], @[])]; - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 20, @{@"a" : @"b"}, NO), - @[ @2 ], @[])]; - - [self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]]; - - FSTDocumentDictionary *docs = [self.localStore executeQuery:query]; - XCTAssertEqualObjects([docs values], (@[ - FSTTestDoc(@"foo/bar", 20, @{@"a" : @"b"}, NO), - FSTTestDoc(@"foo/baz", 10, @{@"a" : @"b"}, NO), - FSTTestDoc(@"foo/bonk", 0, @{@"a" : @"b"}, YES) - ])); -} - -- (void)testPersistsResumeTokens { - if ([self isTestBaseClass]) return; - - // This test only works in the absence of the FSTEagerGarbageCollector. - [self restartWithNoopGarbageCollector]; - - FSTQuery *query = FSTTestQuery(@"foo/bar"); - FSTQueryData *queryData = [self.localStore allocateQuery:query]; - FSTBoxedTargetID *targetID = @(queryData.targetID); - NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1000); - - FSTWatchChange *watchChange = - [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent - targetIDs:@[ targetID ] - resumeToken:resumeToken]; - NSMutableDictionary *listens = - [NSMutableDictionary dictionary]; - listens[targetID] = queryData; - NSMutableDictionary *pendingResponses = - [NSMutableDictionary dictionary]; - FSTWatchChangeAggregator *aggregator = - [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:FSTTestVersion(1000) - listenTargets:listens - pendingTargetResponses:pendingResponses]; - [aggregator addWatchChanges:@[ watchChange ]]; - FSTRemoteEvent *remoteEvent = [aggregator remoteEvent]; - [self applyRemoteEvent:remoteEvent]; - - // Stop listening so that the query should become inactive (but persistent) - [self.localStore releaseQuery:query]; - - // Should come back with the same resume token - FSTQueryData *queryData2 = [self.localStore allocateQuery:query]; - XCTAssertEqualObjects(queryData2.resumeToken, resumeToken); -} - -- (void)testRemoteDocumentKeysForTarget { - if ([self isTestBaseClass]) return; - [self restartWithNoopGarbageCollector]; - - FSTQuery *query = FSTTestQuery(@"foo"); - [self allocateQuery:query]; - FSTAssertTargetID(2); - - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/baz", 10, @{@"a" : @"b"}, NO), - @[ @2 ], @[])]; - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 20, @{@"a" : @"b"}, NO), - @[ @2 ], @[])]; - - [self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]]; - - FSTDocumentKeySet *keys = [self.localStore remoteDocumentKeysForTarget:2]; - FSTAssertEqualSets(keys, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/baz") ])); - - [self restartWithNoopGarbageCollector]; - - keys = [self.localStore remoteDocumentKeysForTarget:2]; - FSTAssertEqualSets(keys, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/baz") ])); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm new file mode 100644 index 0000000..45d1815 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm @@ -0,0 +1,794 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTLocalStore.h" + +#import + +#import "Firestore/Source/Auth/FSTUser.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTTimestamp.h" +#import "Firestore/Source/Local/FSTEagerGarbageCollector.h" +#import "Firestore/Source/Local/FSTLocalWriteResult.h" +#import "Firestore/Source/Local/FSTNoOpGarbageCollector.h" +#import "Firestore/Source/Local/FSTPersistence.h" +#import "Firestore/Source/Local/FSTQueryData.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Model/FSTMutationBatch.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Remote/FSTRemoteEvent.h" +#import "Firestore/Source/Remote/FSTWatchChange.h" +#import "Firestore/Source/Util/FSTClasses.h" + +#import "Firestore/Example/Tests/Local/FSTLocalStoreTests.h" +#import "Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h" +#import "Firestore/Example/Tests/Util/FSTHelpers.h" +#import "Firestore/third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.h" +#import "Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.h" + +NS_ASSUME_NONNULL_BEGIN + +/** Creates a document version dictionary mapping the document in @a mutation to @a version. */ +FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, + FSTTestSnapshotVersion version) { + FSTDocumentVersionDictionary *result = [FSTDocumentVersionDictionary documentVersionDictionary]; + result = [result dictionaryBySettingObject:FSTTestVersion(version) forKey:mutation.key]; + return result; +} + +@interface FSTLocalStoreTests () + +@property(nonatomic, strong, readwrite) id localStorePersistence; +@property(nonatomic, strong, readwrite) FSTLocalStore *localStore; + +@property(nonatomic, strong, readonly) NSMutableArray *batches; +@property(nonatomic, strong, readwrite, nullable) FSTMaybeDocumentDictionary *lastChanges; +@property(nonatomic, assign, readwrite) FSTTargetID lastTargetID; + +@end + +@implementation FSTLocalStoreTests + +- (void)setUp { + [super setUp]; + + if ([self isTestBaseClass]) { + return; + } + + id persistence = [self persistence]; + self.localStorePersistence = persistence; + id garbageCollector = [[FSTEagerGarbageCollector alloc] init]; + self.localStore = [[FSTLocalStore alloc] initWithPersistence:persistence + garbageCollector:garbageCollector + initialUser:[FSTUser unauthenticatedUser]]; + [self.localStore start]; + + _batches = [NSMutableArray array]; + _lastChanges = nil; + _lastTargetID = 0; +} + +- (void)tearDown { + [self.localStore shutdown]; + [self.localStorePersistence shutdown]; + + [super tearDown]; +} + +- (id)persistence { + @throw FSTAbstractMethodException(); // NOLINT +} + +/** + * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for + * FSTLocalStoreTests since it is incomplete without the implementations supplied by its + * subclasses. + */ +- (BOOL)isTestBaseClass { + return [self class] == [FSTLocalStoreTests class]; +} + +/** Restarts the local store using the FSTNoOpGarbageCollector instead of the default. */ +- (void)restartWithNoopGarbageCollector { + [self.localStore shutdown]; + + id garbageCollector = [[FSTNoOpGarbageCollector alloc] init]; + self.localStore = [[FSTLocalStore alloc] initWithPersistence:self.localStorePersistence + garbageCollector:garbageCollector + initialUser:[FSTUser unauthenticatedUser]]; + [self.localStore start]; +} + +- (void)writeMutation:(FSTMutation *)mutation { + [self writeMutations:@[ mutation ]]; +} + +- (void)writeMutations:(NSArray *)mutations { + FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:mutations]; + XCTAssertNotNil(result); + [self.batches addObject:[[FSTMutationBatch alloc] initWithBatchID:result.batchID + localWriteTime:[FSTTimestamp timestamp] + mutations:mutations]]; + self.lastChanges = result.changes; +} + +- (void)applyRemoteEvent:(FSTRemoteEvent *)event { + self.lastChanges = [self.localStore applyRemoteEvent:event]; +} + +- (void)notifyLocalViewChanges:(FSTLocalViewChanges *)changes { + [self.localStore notifyLocalViewChanges:@[ changes ]]; +} + +- (void)acknowledgeMutationWithVersion:(FSTTestSnapshotVersion)documentVersion { + FSTMutationBatch *batch = [self.batches firstObject]; + [self.batches removeObjectAtIndex:0]; + XCTAssertEqual(batch.mutations.count, 1, @"Acknowledging more than one mutation not supported."); + FSTSnapshotVersion *version = FSTTestVersion(documentVersion); + FSTMutationResult *mutationResult = + [[FSTMutationResult alloc] initWithVersion:version transformResults:nil]; + FSTMutationBatchResult *result = [FSTMutationBatchResult resultWithBatch:batch + commitVersion:version + mutationResults:@[ mutationResult ] + streamToken:nil]; + self.lastChanges = [self.localStore acknowledgeBatchWithResult:result]; +} + +- (void)rejectMutation { + FSTMutationBatch *batch = [self.batches firstObject]; + [self.batches removeObjectAtIndex:0]; + self.lastChanges = [self.localStore rejectBatchID:batch.batchID]; +} + +- (void)allocateQuery:(FSTQuery *)query { + FSTQueryData *queryData = [self.localStore allocateQuery:query]; + self.lastTargetID = queryData.targetID; +} + +- (void)collectGarbage { + [self.localStore collectGarbage]; +} + +/** Asserts that the last target ID is the given number. */ +#define FSTAssertTargetID(targetID) \ + do { \ + XCTAssertEqual(self.lastTargetID, targetID); \ + } while (0) + +/** Asserts that a the lastChanges contain the docs in the given array. */ +#define FSTAssertChanged(documents) \ + XCTAssertNotNil(self.lastChanges); \ + do { \ + FSTMaybeDocumentDictionary *actual = self.lastChanges; \ + NSArray *expected = (documents); \ + XCTAssertEqual(actual.count, expected.count); \ + NSEnumerator *enumerator = expected.objectEnumerator; \ + [actual enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey * key, FSTMaybeDocument * value, \ + BOOL * stop) { \ + XCTAssertEqualObjects(value, [enumerator nextObject]); \ + }]; \ + self.lastChanges = nil; \ + } while (0) + +/** Asserts that the given keys were removed. */ +#define FSTAssertRemoved(keyPaths) \ + XCTAssertNotNil(self.lastChanges); \ + do { \ + FSTMaybeDocumentDictionary *actual = self.lastChanges; \ + XCTAssertEqual(actual.count, keyPaths.count); \ + NSEnumerator *keyPathEnumerator = keyPaths.objectEnumerator; \ + [actual enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey * actualKey, \ + FSTMaybeDocument * value, BOOL * stop) { \ + FSTDocumentKey *expectedKey = FSTTestDocKey([keyPathEnumerator nextObject]); \ + XCTAssertEqualObjects(actualKey, expectedKey); \ + XCTAssertTrue([value isKindOfClass:[FSTDeletedDocument class]]); \ + }]; \ + self.lastChanges = nil; \ + } while (0) + +/** Asserts that the given local store contains the given document. */ +#define FSTAssertContains(document) \ + do { \ + FSTMaybeDocument *expected = (document); \ + FSTMaybeDocument *actual = [self.localStore readDocument:expected.key]; \ + XCTAssertEqualObjects(actual, expected); \ + } while (0) + +/** Asserts that the given local store does not contain the given document. */ +#define FSTAssertNotContains(keyPathString) \ + do { \ + FSTDocumentKey *key = FSTTestDocKey(keyPathString); \ + FSTMaybeDocument *actual = [self.localStore readDocument:key]; \ + XCTAssertNil(actual); \ + } while (0) + +- (void)testMutationBatchKeys { + if ([self isTestBaseClass]) return; + + FSTMutation *set1 = FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}); + FSTMutation *set2 = FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"}); + FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:1 + localWriteTime:[FSTTimestamp timestamp] + mutations:@[ set1, set2 ]]; + FSTDocumentKeySet *keys = [batch keys]; + XCTAssertEqual(keys.count, 2); +} + +- (void)testHandlesSetMutation { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + [self acknowledgeMutationWithVersion:0]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO)); +} + +- (void)testHandlesSetMutationThenDocument { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent( + FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES)); +} + +- (void)testHandlesAckThenRejectThenRemoteEvent { + if ([self isTestBaseClass]) return; + + // Start a query that requires acks to be held. + FSTQuery *query = FSTTestQuery(@"foo"); + [self allocateQuery:query]; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + // The last seen version is zero, so this ack must be held. + [self acknowledgeMutationWithVersion:1]; + FSTAssertChanged(@[]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + [self writeMutation:FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"})]; + FSTAssertChanged(@[ FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES)); + + [self rejectMutation]; + FSTAssertRemoved(@[ @"bar/baz" ]); + FSTAssertNotContains(@"bar/baz"); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent( + FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO)); + FSTAssertNotContains(@"bar/baz"); +} + +- (void)testHandlesDeletedDocumentThenSetMutationThenAck { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 2)); + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + [self acknowledgeMutationWithVersion:3]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO)); +} + +- (void)testHandlesSetMutationThenDeletedDocument { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); +} + +- (void)testHandlesDocumentThenSetMutationThenAckThenDocument { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"it" : @"base"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"it" : @"base"}, NO)); + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES)); + + [self acknowledgeMutationWithVersion:3]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent( + FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO)); +} + +- (void)testHandlesPatchWithoutPriorDocument { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertNotContains(@"foo/bar"); + + [self acknowledgeMutationWithVersion:1]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertNotContains(@"foo/bar"); +} + +- (void)testHandlesPatchMutationThenDocumentThenAck { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertNotContains(@"foo/bar"); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, YES)); + + [self acknowledgeMutationWithVersion:2]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, NO)); +} + +- (void)testHandlesPatchMutationThenAckThenDocument { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertNotContains(@"foo/bar"); + + [self acknowledgeMutationWithVersion:1]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertNotContains(@"foo/bar"); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO)); +} + +- (void)testHandlesDeleteMutationThenAck { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestDeleteMutation(@"foo/bar")]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self acknowledgeMutationWithVersion:1]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); +} + +- (void)testHandlesDocumentThenDeleteMutationThenAck { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO)); + + [self writeMutation:FSTTestDeleteMutation(@"foo/bar")]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self acknowledgeMutationWithVersion:2]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); +} + +- (void)testHandlesDeleteMutationThenDocumentThenAck { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestDeleteMutation(@"foo/bar")]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self acknowledgeMutationWithVersion:2]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); +} + +- (void)testHandlesDocumentThenDeletedDocumentThenDocument { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 2)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent( + FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO)); +} + +- (void)testHandlesSetMutationThenPatchMutationThenDocumentThenAckThenAck { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, YES)); + + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES)); + + [self acknowledgeMutationWithVersion:2]; // delete mutation + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES)); + + [self acknowledgeMutationWithVersion:3]; // patch mutation + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO)); +} + +- (void)testHandlesSetMutationAndPatchMutationTogether { + if ([self isTestBaseClass]) return; + + [self writeMutations:@[ + FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}), + FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil) + ]]; + + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); +} + +- (void)testHandlesSetMutationThenPatchMutationThenReject { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"})]; + [self acknowledgeMutationWithVersion:1]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO)); + + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + [self rejectMutation]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO)); +} + +- (void)testHandlesSetMutationsAndPatchMutationOfJustOneTogether { + if ([self isTestBaseClass]) return; + + [self writeMutations:@[ + FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}), + FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"}), + FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil) + ]]; + + FSTAssertChanged((@[ + FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES), + FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) + ])); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + FSTAssertContains(FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES)); +} + +- (void)testHandlesDeleteMutationThenPatchMutationThenAckThenAck { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestDeleteMutation(@"foo/bar")]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self acknowledgeMutationWithVersion:2]; // delete mutation + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self acknowledgeMutationWithVersion:3]; // patch mutation + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); +} + +- (void)testCollectsGarbageAfterChangeBatchWithNoTargetIDs { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])]; + FSTAssertRemoved(@[ @"foo/bar" ]); + + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO), + @[ @1 ], @[])]; + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); +} + +- (void)testCollectsGarbageAfterChangeBatch { + if ([self isTestBaseClass]) return; + + FSTQuery *query = FSTTestQuery(@"foo"); + [self allocateQuery:query]; + FSTAssertTargetID(2); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO), + @[ @2 ], @[])]; + [self collectGarbage]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"baz"}, NO), + @[], @[ @2 ])]; + [self collectGarbage]; + + FSTAssertNotContains(@"foo/bar"); +} + +- (void)testCollectsGarbageAfterAcknowledgedMutation { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO), + @[ @1 ], @[])]; + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})]; + [self writeMutation:FSTTestDeleteMutation(@"foo/baz")]; + [self collectGarbage]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES)); + FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); + + [self acknowledgeMutationWithVersion:3]; + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES)); + FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); + + [self acknowledgeMutationWithVersion:4]; + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + FSTAssertNotContains(@"foo/bah"); + FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); + + [self acknowledgeMutationWithVersion:5]; + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + FSTAssertNotContains(@"foo/bah"); + FSTAssertNotContains(@"foo/baz"); +} + +- (void)testCollectsGarbageAfterRejectedMutation { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO), + @[ @1 ], @[])]; + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})]; + [self writeMutation:FSTTestDeleteMutation(@"foo/baz")]; + [self collectGarbage]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES)); + FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); + + [self rejectMutation]; // patch mutation + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES)); + FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); + + [self rejectMutation]; // set mutation + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + FSTAssertNotContains(@"foo/bah"); + FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); + + [self rejectMutation]; // delete mutation + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + FSTAssertNotContains(@"foo/bah"); + FSTAssertNotContains(@"foo/baz"); +} + +- (void)testPinsDocumentsInTheLocalView { + if ([self isTestBaseClass]) return; + + FSTQuery *query = FSTTestQuery(@"foo"); + [self allocateQuery:query]; + FSTAssertTargetID(2); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO), + @[ @2 ], @[])]; + [self writeMutation:FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"})]; + [self collectGarbage]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO)); + FSTAssertContains(FSTTestDoc(@"foo/baz", 0, @{@"foo" : @"baz"}, YES)); + + [self notifyLocalViewChanges:FSTTestViewChanges(query, @[ @"foo/bar", @"foo/baz" ], @[])]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO), + @[], @[ @2 ])]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/baz", 2, @{@"foo" : @"baz"}, NO), + @[ @1 ], @[])]; + [self acknowledgeMutationWithVersion:2]; + [self collectGarbage]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO)); + FSTAssertContains(FSTTestDoc(@"foo/baz", 2, @{@"foo" : @"baz"}, NO)); + + [self notifyLocalViewChanges:FSTTestViewChanges(query, @[], @[ @"foo/bar", @"foo/baz" ])]; + [self collectGarbage]; + + FSTAssertNotContains(@"foo/bar"); + FSTAssertNotContains(@"foo/baz"); +} + +- (void)testThrowsAwayDocumentsWithUnknownTargetIDsImmediately { + if ([self isTestBaseClass]) return; + + FSTTargetID targetID = 321; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{}, NO), + @[ @(targetID) ], @[])]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{}, NO)); + + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); +} + +- (void)testCanExecuteDocumentQueries { + if ([self isTestBaseClass]) return; + + [self.localStore locallyWriteMutations:@[ + FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}), + FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}), + FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}) + ]]; + FSTQuery *query = FSTTestQuery(@"foo/bar"); + FSTDocumentDictionary *docs = [self.localStore executeQuery:query]; + XCTAssertEqualObjects([docs values], @[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); +} + +- (void)testCanExecuteCollectionQueries { + if ([self isTestBaseClass]) return; + + [self.localStore locallyWriteMutations:@[ + FSTTestSetMutation(@"fo/bar", @{@"fo" : @"bar"}), + FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}), + FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}), + FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}), + FSTTestSetMutation(@"fooo/blah", @{@"fooo" : @"blah"}) + ]]; + FSTQuery *query = FSTTestQuery(@"foo"); + FSTDocumentDictionary *docs = [self.localStore executeQuery:query]; + XCTAssertEqualObjects([docs values], (@[ + FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES), + FSTTestDoc(@"foo/baz", 0, @{@"foo" : @"baz"}, YES) + ])); +} + +- (void)testCanExecuteMixedCollectionQueries { + if ([self isTestBaseClass]) return; + + FSTQuery *query = FSTTestQuery(@"foo"); + [self allocateQuery:query]; + FSTAssertTargetID(2); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/baz", 10, @{@"a" : @"b"}, NO), + @[ @2 ], @[])]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 20, @{@"a" : @"b"}, NO), + @[ @2 ], @[])]; + + [self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]]; + + FSTDocumentDictionary *docs = [self.localStore executeQuery:query]; + XCTAssertEqualObjects([docs values], (@[ + FSTTestDoc(@"foo/bar", 20, @{@"a" : @"b"}, NO), + FSTTestDoc(@"foo/baz", 10, @{@"a" : @"b"}, NO), + FSTTestDoc(@"foo/bonk", 0, @{@"a" : @"b"}, YES) + ])); +} + +- (void)testPersistsResumeTokens { + if ([self isTestBaseClass]) return; + + // This test only works in the absence of the FSTEagerGarbageCollector. + [self restartWithNoopGarbageCollector]; + + FSTQuery *query = FSTTestQuery(@"foo/bar"); + FSTQueryData *queryData = [self.localStore allocateQuery:query]; + FSTBoxedTargetID *targetID = @(queryData.targetID); + NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1000); + + FSTWatchChange *watchChange = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ targetID ] + resumeToken:resumeToken]; + NSMutableDictionary *listens = + [NSMutableDictionary dictionary]; + listens[targetID] = queryData; + NSMutableDictionary *pendingResponses = + [NSMutableDictionary dictionary]; + FSTWatchChangeAggregator *aggregator = + [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:FSTTestVersion(1000) + listenTargets:listens + pendingTargetResponses:pendingResponses]; + [aggregator addWatchChanges:@[ watchChange ]]; + FSTRemoteEvent *remoteEvent = [aggregator remoteEvent]; + [self applyRemoteEvent:remoteEvent]; + + // Stop listening so that the query should become inactive (but persistent) + [self.localStore releaseQuery:query]; + + // Should come back with the same resume token + FSTQueryData *queryData2 = [self.localStore allocateQuery:query]; + XCTAssertEqualObjects(queryData2.resumeToken, resumeToken); +} + +- (void)testRemoteDocumentKeysForTarget { + if ([self isTestBaseClass]) return; + [self restartWithNoopGarbageCollector]; + + FSTQuery *query = FSTTestQuery(@"foo"); + [self allocateQuery:query]; + FSTAssertTargetID(2); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/baz", 10, @{@"a" : @"b"}, NO), + @[ @2 ], @[])]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 20, @{@"a" : @"b"}, NO), + @[ @2 ], @[])]; + + [self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]]; + + FSTDocumentKeySet *keys = [self.localStore remoteDocumentKeysForTarget:2]; + FSTAssertEqualSets(keys, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/baz") ])); + + [self restartWithNoopGarbageCollector]; + + keys = [self.localStore remoteDocumentKeysForTarget:2]; + FSTAssertEqualSets(keys, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/baz") ])); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.m b/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.m deleted file mode 100644 index b78239e..0000000 --- a/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.m +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTLocalStore.h" - -#import - -#import "Firestore/Source/Local/FSTMemoryPersistence.h" - -#import "Firestore/Example/Tests/Local/FSTLocalStoreTests.h" -#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -/** - * This tests the FSTLocalStore with an FSTMemoryPersistence persistence implementation. The tests - * are in FSTLocalStoreTests and this class is merely responsible for creating a new FSTPersistence - * implementation on demand. - */ -@interface FSTMemoryLocalStoreTests : FSTLocalStoreTests -@end - -@implementation FSTMemoryLocalStoreTests - -- (id)persistence { - return [FSTPersistenceTestHelpers memoryPersistence]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.mm b/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.mm new file mode 100644 index 0000000..b78239e --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.mm @@ -0,0 +1,44 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTLocalStore.h" + +#import + +#import "Firestore/Source/Local/FSTMemoryPersistence.h" + +#import "Firestore/Example/Tests/Local/FSTLocalStoreTests.h" +#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * This tests the FSTLocalStore with an FSTMemoryPersistence persistence implementation. The tests + * are in FSTLocalStoreTests and this class is merely responsible for creating a new FSTPersistence + * implementation on demand. + */ +@interface FSTMemoryLocalStoreTests : FSTLocalStoreTests +@end + +@implementation FSTMemoryLocalStoreTests + +- (id)persistence { + return [FSTPersistenceTestHelpers memoryPersistence]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.m b/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.m deleted file mode 100644 index ab7afee..0000000 --- a/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.m +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTMemoryMutationQueue.h" - -#import "Firestore/Source/Auth/FSTUser.h" -#import "Firestore/Source/Local/FSTMemoryPersistence.h" - -#import "Firestore/Example/Tests/Local/FSTMutationQueueTests.h" -#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" - -@interface FSTMemoryMutationQueueTests : FSTMutationQueueTests -@end - -/** - * The tests for FSTMemoryMutationQueue are performed on the FSTMutationQueue protocol in - * FSTMutationQueueTests. This class is merely responsible for setting up the @a mutationQueue. - */ -@implementation FSTMemoryMutationQueueTests - -- (void)setUp { - [super setUp]; - - self.persistence = [FSTPersistenceTestHelpers memoryPersistence]; - self.mutationQueue = - [self.persistence mutationQueueForUser:[[FSTUser alloc] initWithUID:@"user"]]; -} - -@end diff --git a/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm b/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm new file mode 100644 index 0000000..ab7afee --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm @@ -0,0 +1,42 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTMemoryMutationQueue.h" + +#import "Firestore/Source/Auth/FSTUser.h" +#import "Firestore/Source/Local/FSTMemoryPersistence.h" + +#import "Firestore/Example/Tests/Local/FSTMutationQueueTests.h" +#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" + +@interface FSTMemoryMutationQueueTests : FSTMutationQueueTests +@end + +/** + * The tests for FSTMemoryMutationQueue are performed on the FSTMutationQueue protocol in + * FSTMutationQueueTests. This class is merely responsible for setting up the @a mutationQueue. + */ +@implementation FSTMemoryMutationQueueTests + +- (void)setUp { + [super setUp]; + + self.persistence = [FSTPersistenceTestHelpers memoryPersistence]; + self.mutationQueue = + [self.persistence mutationQueueForUser:[[FSTUser alloc] initWithUID:@"user"]]; +} + +@end diff --git a/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.m b/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.m deleted file mode 100644 index fb7df6b..0000000 --- a/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.m +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTMemoryQueryCache.h" - -#import "Firestore/Source/Local/FSTMemoryPersistence.h" - -#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" -#import "Firestore/Example/Tests/Local/FSTQueryCacheTests.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTMemoryQueryCacheTests : FSTQueryCacheTests -@end - -/** - * The tests for FSTMemoryQueryCache are performed on the FSTQueryCache protocol in - * FSTQueryCacheTests. This class is merely responsible for setting up and tearing down the - * @a queryCache. - */ -@implementation FSTMemoryQueryCacheTests - -- (void)setUp { - [super setUp]; - - self.persistence = [FSTPersistenceTestHelpers memoryPersistence]; - self.queryCache = [self.persistence queryCache]; - [self.queryCache start]; -} - -- (void)tearDown { - [self.queryCache shutdown]; - self.persistence = nil; - self.queryCache = nil; - - [super tearDown]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.mm b/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.mm new file mode 100644 index 0000000..fb7df6b --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.mm @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTMemoryQueryCache.h" + +#import "Firestore/Source/Local/FSTMemoryPersistence.h" + +#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" +#import "Firestore/Example/Tests/Local/FSTQueryCacheTests.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMemoryQueryCacheTests : FSTQueryCacheTests +@end + +/** + * The tests for FSTMemoryQueryCache are performed on the FSTQueryCache protocol in + * FSTQueryCacheTests. This class is merely responsible for setting up and tearing down the + * @a queryCache. + */ +@implementation FSTMemoryQueryCacheTests + +- (void)setUp { + [super setUp]; + + self.persistence = [FSTPersistenceTestHelpers memoryPersistence]; + self.queryCache = [self.persistence queryCache]; + [self.queryCache start]; +} + +- (void)tearDown { + [self.queryCache shutdown]; + self.persistence = nil; + self.queryCache = nil; + + [super tearDown]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.m b/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.m deleted file mode 100644 index 162eef0..0000000 --- a/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.m +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTMemoryRemoteDocumentCache.h" - -#import "Firestore/Source/Local/FSTMemoryPersistence.h" - -#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" -#import "Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h" - -@interface FSTMemoryRemoteDocumentCacheTests : FSTRemoteDocumentCacheTests -@end - -/** - * The tests for FSTMemoryRemoteDocumentCache are performed on the FSTRemoteDocumentCache - * protocol in FSTRemoteDocumentCacheTests. This class is merely responsible for setting up and - * tearing down the @a remoteDocumentCache. - */ -@implementation FSTMemoryRemoteDocumentCacheTests - -- (void)setUp { - [super setUp]; - - self.persistence = [FSTPersistenceTestHelpers memoryPersistence]; - self.remoteDocumentCache = [self.persistence remoteDocumentCache]; -} - -- (void)tearDown { - [self.remoteDocumentCache shutdown]; - self.persistence = nil; - self.remoteDocumentCache = nil; - - [super tearDown]; -} - -@end diff --git a/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.mm b/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.mm new file mode 100644 index 0000000..162eef0 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.mm @@ -0,0 +1,49 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTMemoryRemoteDocumentCache.h" + +#import "Firestore/Source/Local/FSTMemoryPersistence.h" + +#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" +#import "Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h" + +@interface FSTMemoryRemoteDocumentCacheTests : FSTRemoteDocumentCacheTests +@end + +/** + * The tests for FSTMemoryRemoteDocumentCache are performed on the FSTRemoteDocumentCache + * protocol in FSTRemoteDocumentCacheTests. This class is merely responsible for setting up and + * tearing down the @a remoteDocumentCache. + */ +@implementation FSTMemoryRemoteDocumentCacheTests + +- (void)setUp { + [super setUp]; + + self.persistence = [FSTPersistenceTestHelpers memoryPersistence]; + self.remoteDocumentCache = [self.persistence remoteDocumentCache]; +} + +- (void)tearDown { + [self.remoteDocumentCache shutdown]; + self.persistence = nil; + self.remoteDocumentCache = nil; + + [super tearDown]; +} + +@end diff --git a/Firestore/Example/Tests/Local/FSTMutationQueueTests.m b/Firestore/Example/Tests/Local/FSTMutationQueueTests.m deleted file mode 100644 index 020a0a7..0000000 --- a/Firestore/Example/Tests/Local/FSTMutationQueueTests.m +++ /dev/null @@ -1,511 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Example/Tests/Local/FSTMutationQueueTests.h" - -#import "Firestore/Source/Auth/FSTUser.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTTimestamp.h" -#import "Firestore/Source/Local/FSTEagerGarbageCollector.h" -#import "Firestore/Source/Local/FSTMutationQueue.h" -#import "Firestore/Source/Local/FSTPersistence.h" -#import "Firestore/Source/Local/FSTWriteGroup.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTMutationQueueTests - -- (void)tearDown { - [self.mutationQueue shutdown]; - [self.persistence shutdown]; - [super tearDown]; -} - -/** - * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for - * FSTMutationQueueTests since it is incomplete without the implementations supplied by its - * subclasses. - */ -- (BOOL)isTestBaseClass { - return [self class] == [FSTMutationQueueTests class]; -} - -- (void)testCountBatches { - if ([self isTestBaseClass]) return; - - XCTAssertEqual(0, [self batchCount]); - XCTAssertTrue([self.mutationQueue isEmpty]); - - FSTMutationBatch *batch1 = [self addMutationBatch]; - XCTAssertEqual(1, [self batchCount]); - XCTAssertFalse([self.mutationQueue isEmpty]); - - FSTMutationBatch *batch2 = [self addMutationBatch]; - XCTAssertEqual(2, [self batchCount]); - - [self removeMutationBatches:@[ batch2 ]]; - XCTAssertEqual(1, [self batchCount]); - - [self removeMutationBatches:@[ batch1 ]]; - XCTAssertEqual(0, [self batchCount]); - XCTAssertTrue([self.mutationQueue isEmpty]); -} - -- (void)testAcknowledgeBatchID { - if ([self isTestBaseClass]) return; - - // Initial state of an empty queue - XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown); - - // Adding mutation batches should not change the highest acked batchID. - FSTMutationBatch *batch1 = [self addMutationBatch]; - FSTMutationBatch *batch2 = [self addMutationBatch]; - FSTMutationBatch *batch3 = [self addMutationBatch]; - XCTAssertGreaterThan(batch1.batchID, kFSTBatchIDUnknown); - XCTAssertGreaterThan(batch2.batchID, batch1.batchID); - XCTAssertGreaterThan(batch3.batchID, batch2.batchID); - - XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown); - - [self acknowledgeBatch:batch1]; - [self acknowledgeBatch:batch2]; - XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); - - [self removeMutationBatches:@[ batch1 ]]; - XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); - - [self removeMutationBatches:@[ batch2 ]]; - XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); - - // Batch 3 never acknowledged. - [self removeMutationBatches:@[ batch3 ]]; - XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); -} - -- (void)testAcknowledgeThenRemove { - if ([self isTestBaseClass]) return; - - FSTMutationBatch *batch1 = [self addMutationBatch]; - - FSTWriteGroup *group = [self.persistence startGroupWithAction:NSStringFromSelector(_cmd)]; - [self.mutationQueue acknowledgeBatch:batch1 streamToken:nil group:group]; - [self.mutationQueue removeMutationBatches:@[ batch1 ] group:group]; - [self.persistence commitGroup:group]; - - XCTAssertEqual([self batchCount], 0); - XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch1.batchID); -} - -- (void)testHighestAcknowledgedBatchIDNeverExceedsNextBatchID { - if ([self isTestBaseClass]) return; - - FSTMutationBatch *batch1 = [self addMutationBatch]; - FSTMutationBatch *batch2 = [self addMutationBatch]; - [self acknowledgeBatch:batch1]; - [self acknowledgeBatch:batch2]; - XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); - - [self removeMutationBatches:@[ batch1, batch2 ]]; - XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); - - // Restart the queue so that nextBatchID will be reset. - [self.mutationQueue shutdown]; - self.mutationQueue = - [self.persistence mutationQueueForUser:[[FSTUser alloc] initWithUID:@"user"]]; - - FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Start MutationQueue"]; - [self.mutationQueue startWithGroup:group]; - [self.persistence commitGroup:group]; - - // Verify that on restart with an empty queue, nextBatchID falls to a lower value. - XCTAssertLessThan(self.mutationQueue.nextBatchID, batch2.batchID); - - // As a result highestAcknowledgedBatchID must also reset lower. - XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown); - - // The mutation queue will reset the next batchID after all mutations are removed so adding - // another mutation will cause a collision. - FSTMutationBatch *newBatch = [self addMutationBatch]; - XCTAssertEqual(newBatch.batchID, batch1.batchID); - - // Restart the queue with one unacknowledged batch in it. - group = [self.persistence startGroupWithAction:@"Start MutationQueue"]; - [self.mutationQueue startWithGroup:group]; - [self.persistence commitGroup:group]; - - XCTAssertEqual([self.mutationQueue nextBatchID], newBatch.batchID + 1); - - // highestAcknowledgedBatchID must still be kFSTBatchIDUnknown. - XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown); -} - -- (void)testLookupMutationBatch { - if ([self isTestBaseClass]) return; - - // Searching on an empty queue should not find a non-existent batch - FSTMutationBatch *notFound = [self.mutationQueue lookupMutationBatch:42]; - XCTAssertNil(notFound); - - NSMutableArray *batches = [self createBatches:10]; - NSArray *removed = [self makeHoles:@[ @2, @6, @7 ] inBatches:batches]; - - // After removing, a batch should not be found - for (NSUInteger i = 0; i < removed.count; i++) { - notFound = [self.mutationQueue lookupMutationBatch:removed[i].batchID]; - XCTAssertNil(notFound); - } - - // Remaining entries should still be found - for (FSTMutationBatch *batch in batches) { - FSTMutationBatch *found = [self.mutationQueue lookupMutationBatch:batch.batchID]; - XCTAssertEqual(found.batchID, batch.batchID); - } - - // Even on a nonempty queue searching should not find a non-existent batch - notFound = [self.mutationQueue lookupMutationBatch:42]; - XCTAssertNil(notFound); -} - -- (void)testNextMutationBatchAfterBatchID { - if ([self isTestBaseClass]) return; - - NSMutableArray *batches = [self createBatches:10]; - - // This is an array of successors assuming the removals below will happen: - NSArray *afters = @[ batches[3], batches[8], batches[8] ]; - NSArray *removed = [self makeHoles:@[ @2, @6, @7 ] inBatches:batches]; - - for (NSUInteger i = 0; i < batches.count - 1; i++) { - FSTMutationBatch *current = batches[i]; - FSTMutationBatch *next = batches[i + 1]; - FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:current.batchID]; - XCTAssertEqual(found.batchID, next.batchID); - } - - for (NSUInteger i = 0; i < removed.count; i++) { - FSTMutationBatch *current = removed[i]; - FSTMutationBatch *next = afters[i]; - FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:current.batchID]; - XCTAssertEqual(found.batchID, next.batchID); - } - - FSTMutationBatch *first = batches[0]; - FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:first.batchID - 42]; - XCTAssertEqual(found.batchID, first.batchID); - - FSTMutationBatch *last = batches[batches.count - 1]; - FSTMutationBatch *notFound = [self.mutationQueue nextMutationBatchAfterBatchID:last.batchID]; - XCTAssertNil(notFound); -} - -- (void)testAllMutationBatchesThroughBatchID { - if ([self isTestBaseClass]) return; - - NSMutableArray *batches = [self createBatches:10]; - [self makeHoles:@[ @2, @6, @7 ] inBatches:batches]; - - NSArray *found, *expected; - - found = [self.mutationQueue allMutationBatchesThroughBatchID:batches[0].batchID - 1]; - XCTAssertEqualObjects(found, (@[])); - - for (NSUInteger i = 0; i < batches.count; i++) { - found = [self.mutationQueue allMutationBatchesThroughBatchID:batches[i].batchID]; - expected = [batches subarrayWithRange:NSMakeRange(0, i + 1)]; - XCTAssertEqualObjects(found, expected, @"for index %lu", (unsigned long)i); - } -} - -- (void)testAllMutationBatchesAffectingDocumentKey { - if ([self isTestBaseClass]) return; - - NSArray *mutations = @[ - FSTTestSetMutation(@"fob/bar", - @{ @"a" : @1 }), - FSTTestSetMutation(@"foo/bar", - @{ @"a" : @1 }), - FSTTestPatchMutation(@"foo/bar", - @{ @"b" : @1 }, nil), - FSTTestSetMutation(@"foo/bar/suffix/key", - @{ @"a" : @1 }), - FSTTestSetMutation(@"foo/baz", - @{ @"a" : @1 }), - FSTTestSetMutation(@"food/bar", - @{ @"a" : @1 }) - ]; - - // Store all the mutations. - NSMutableArray *batches = [NSMutableArray array]; - FSTWriteGroup *group = [self.persistence startGroupWithAction:@"New mutation batch"]; - for (FSTMutation *mutation in mutations) { - FSTMutationBatch *batch = - [self.mutationQueue addMutationBatchWithWriteTime:[FSTTimestamp timestamp] - mutations:@[ mutation ] - group:group]; - [batches addObject:batch]; - } - [self.persistence commitGroup:group]; - - NSArray *expected = @[ batches[1], batches[2] ]; - NSArray *matches = - [self.mutationQueue allMutationBatchesAffectingDocumentKey:FSTTestDocKey(@"foo/bar")]; - - XCTAssertEqualObjects(matches, expected); -} - -- (void)testAllMutationBatchesAffectingQuery { - if ([self isTestBaseClass]) return; - - NSArray *mutations = @[ - FSTTestSetMutation(@"fob/bar", - @{ @"a" : @1 }), - FSTTestSetMutation(@"foo/bar", - @{ @"a" : @1 }), - FSTTestPatchMutation(@"foo/bar", - @{ @"b" : @1 }, nil), - FSTTestSetMutation(@"foo/bar/suffix/key", - @{ @"a" : @1 }), - FSTTestSetMutation(@"foo/baz", - @{ @"a" : @1 }), - FSTTestSetMutation(@"food/bar", - @{ @"a" : @1 }) - ]; - - // Store all the mutations. - NSMutableArray *batches = [NSMutableArray array]; - FSTWriteGroup *group = [self.persistence startGroupWithAction:@"New mutation batch"]; - for (FSTMutation *mutation in mutations) { - FSTMutationBatch *batch = - [self.mutationQueue addMutationBatchWithWriteTime:[FSTTimestamp timestamp] - mutations:@[ mutation ] - group:group]; - [batches addObject:batch]; - } - [self.persistence commitGroup:group]; - - NSArray *expected = @[ batches[1], batches[2], batches[4] ]; - FSTQuery *query = FSTTestQuery(@"foo"); - NSArray *matches = - [self.mutationQueue allMutationBatchesAffectingQuery:query]; - - XCTAssertEqualObjects(matches, expected); -} - -- (void)testRemoveMutationBatches { - if ([self isTestBaseClass]) return; - - NSMutableArray *batches = [self createBatches:10]; - FSTMutationBatch *last = batches[batches.count - 1]; - - [self removeMutationBatches:@[ batches[0] ]]; - [batches removeObjectAtIndex:0]; - XCTAssertEqual([self batchCount], 9); - - NSArray *found; - - found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; - XCTAssertEqualObjects(found, batches); - XCTAssertEqual(found.count, 9); - - [self removeMutationBatches:@[ batches[0], batches[1], batches[2] ]]; - [batches removeObjectsInRange:NSMakeRange(0, 3)]; - XCTAssertEqual([self batchCount], 6); - - found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; - XCTAssertEqualObjects(found, batches); - XCTAssertEqual(found.count, 6); - - [self removeMutationBatches:@[ batches[batches.count - 1] ]]; - [batches removeObjectAtIndex:batches.count - 1]; - XCTAssertEqual([self batchCount], 5); - - found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; - XCTAssertEqualObjects(found, batches); - XCTAssertEqual(found.count, 5); - - [self removeMutationBatches:@[ batches[3] ]]; - [batches removeObjectAtIndex:3]; - XCTAssertEqual([self batchCount], 4); - - [self removeMutationBatches:@[ batches[1] ]]; - [batches removeObjectAtIndex:1]; - XCTAssertEqual([self batchCount], 3); - - found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; - XCTAssertEqualObjects(found, batches); - XCTAssertEqual(found.count, 3); - XCTAssertFalse([self.mutationQueue isEmpty]); - - [self removeMutationBatches:batches]; - found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; - XCTAssertEqualObjects(found, @[]); - XCTAssertEqual(found.count, 0); - XCTAssertTrue([self.mutationQueue isEmpty]); -} - -- (void)testRemoveMutationBatchesEmitsGarbageEvents { - if ([self isTestBaseClass]) return; - - FSTEagerGarbageCollector *garbageCollector = [[FSTEagerGarbageCollector alloc] init]; - [garbageCollector addGarbageSource:self.mutationQueue]; - - NSMutableArray *batches = [NSMutableArray array]; - [batches addObjectsFromArray:@[ - [self addMutationBatchWithKey:@"foo/bar"], - [self addMutationBatchWithKey:@"foo/ba"], - [self addMutationBatchWithKey:@"foo/bar2"], - [self addMutationBatchWithKey:@"foo/bar"], - [self addMutationBatchWithKey:@"foo/bar/suffix/baz"], - [self addMutationBatchWithKey:@"bar/baz"], - ]]; - - [self removeMutationBatches:@[ batches[0] ]]; - NSSet *garbage = [garbageCollector collectGarbage]; - FSTAssertEqualSets(garbage, @[]); - - [self removeMutationBatches:@[ batches[1] ]]; - garbage = [garbageCollector collectGarbage]; - FSTAssertEqualSets(garbage, @[ FSTTestDocKey(@"foo/ba") ]); - - [self removeMutationBatches:@[ batches[5] ]]; - garbage = [garbageCollector collectGarbage]; - FSTAssertEqualSets(garbage, @[ FSTTestDocKey(@"bar/baz") ]); - - [self removeMutationBatches:@[ batches[2], batches[3] ]]; - garbage = [garbageCollector collectGarbage]; - FSTAssertEqualSets(garbage, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/bar2") ])); - - [batches addObject:[self addMutationBatchWithKey:@"foo/bar/suffix/baz"]]; - garbage = [garbageCollector collectGarbage]; - FSTAssertEqualSets(garbage, @[]); - - [self removeMutationBatches:@[ batches[4], batches[6] ]]; - garbage = [garbageCollector collectGarbage]; - FSTAssertEqualSets(garbage, @[ FSTTestDocKey(@"foo/bar/suffix/baz") ]); -} - -- (void)testStreamToken { - if ([self isTestBaseClass]) return; - - NSData *streamToken1 = [@"token1" dataUsingEncoding:NSUTF8StringEncoding]; - NSData *streamToken2 = [@"token2" dataUsingEncoding:NSUTF8StringEncoding]; - - FSTWriteGroup *group = [self.persistence startGroupWithAction:@"initial stream token"]; - [self.mutationQueue setLastStreamToken:streamToken1 group:group]; - [self.persistence commitGroup:group]; - - FSTMutationBatch *batch1 = [self addMutationBatch]; - [self addMutationBatch]; - - XCTAssertEqualObjects([self.mutationQueue lastStreamToken], streamToken1); - - group = [self.persistence startGroupWithAction:@"acknowledgeBatchID"]; - [self.mutationQueue acknowledgeBatch:batch1 streamToken:streamToken2 group:group]; - [self.persistence commitGroup:group]; - - XCTAssertEqual(self.mutationQueue.highestAcknowledgedBatchID, batch1.batchID); - XCTAssertEqualObjects([self.mutationQueue lastStreamToken], streamToken2); -} - -/** Creates a new FSTMutationBatch with the next batch ID and a set of dummy mutations. */ -- (FSTMutationBatch *)addMutationBatch { - return [self addMutationBatchWithKey:@"foo/bar"]; -} - -/** - * Creates a new FSTMutationBatch with the given key, the next batch ID and a set of dummy - * mutations. - */ -- (FSTMutationBatch *)addMutationBatchWithKey:(NSString *)key { - FSTSetMutation *mutation = FSTTestSetMutation(key, @{ @"a" : @1 }); - - FSTWriteGroup *group = [self.persistence startGroupWithAction:@"New mutation batch"]; - FSTMutationBatch *batch = - [self.mutationQueue addMutationBatchWithWriteTime:[FSTTimestamp timestamp] - mutations:@[ mutation ] - group:group]; - [self.persistence commitGroup:group]; - return batch; -} - -/** - * Creates an array of batches containing @a number dummy FSTMutationBatches. Each has a different - * batchID. - */ -- (NSMutableArray *)createBatches:(int)number { - NSMutableArray *batches = [NSMutableArray array]; - - for (int i = 0; i < number; i++) { - FSTMutationBatch *batch = [self addMutationBatch]; - [batches addObject:batch]; - } - - return batches; -} - -/** - * Calls -acknowledgeBatch:streamToken:group: on the mutation queue in a new group and commits the - * the group. - */ -- (void)acknowledgeBatch:(FSTMutationBatch *)batch { - FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Ack batchID"]; - [self.mutationQueue acknowledgeBatch:batch streamToken:nil group:group]; - [self.persistence commitGroup:group]; -} - -/** - * Calls -removeMutationBatches:group: on the mutation queue in a new group and commits the group. - */ -- (void)removeMutationBatches:(NSArray *)batches { - FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Remove mutation batch"]; - [self.mutationQueue removeMutationBatches:batches group:group]; - [self.persistence commitGroup:group]; -} - -/** Returns the number of mutation batches in the mutation queue. */ -- (NSUInteger)batchCount { - return [self.mutationQueue allMutationBatches].count; -} - -/** - * Removes entries from from the given @a batches and returns them. - * - * @param holes An array of indexes in the batches array; in increasing order. Indexes are relative - * to the original state of the batches array, not any intermediate state that might occur. - * @param batches The array to mutate, removing entries from it. - * @return A new array containing all the entries that were removed from @a batches. - */ -- (NSArray *)makeHoles:(NSArray *)holes - inBatches:(NSMutableArray *)batches { - NSMutableArray *removed = [NSMutableArray array]; - for (NSUInteger i = 0; i < holes.count; i++) { - NSUInteger index = holes[i].unsignedIntegerValue - i; - FSTMutationBatch *batch = batches[index]; - [self removeMutationBatches:@[ batch ]]; - - [batches removeObjectAtIndex:index]; - [removed addObject:batch]; - } - return removed; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTMutationQueueTests.mm b/Firestore/Example/Tests/Local/FSTMutationQueueTests.mm new file mode 100644 index 0000000..020a0a7 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTMutationQueueTests.mm @@ -0,0 +1,511 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Example/Tests/Local/FSTMutationQueueTests.h" + +#import "Firestore/Source/Auth/FSTUser.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTTimestamp.h" +#import "Firestore/Source/Local/FSTEagerGarbageCollector.h" +#import "Firestore/Source/Local/FSTMutationQueue.h" +#import "Firestore/Source/Local/FSTPersistence.h" +#import "Firestore/Source/Local/FSTWriteGroup.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Model/FSTMutationBatch.h" + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTMutationQueueTests + +- (void)tearDown { + [self.mutationQueue shutdown]; + [self.persistence shutdown]; + [super tearDown]; +} + +/** + * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for + * FSTMutationQueueTests since it is incomplete without the implementations supplied by its + * subclasses. + */ +- (BOOL)isTestBaseClass { + return [self class] == [FSTMutationQueueTests class]; +} + +- (void)testCountBatches { + if ([self isTestBaseClass]) return; + + XCTAssertEqual(0, [self batchCount]); + XCTAssertTrue([self.mutationQueue isEmpty]); + + FSTMutationBatch *batch1 = [self addMutationBatch]; + XCTAssertEqual(1, [self batchCount]); + XCTAssertFalse([self.mutationQueue isEmpty]); + + FSTMutationBatch *batch2 = [self addMutationBatch]; + XCTAssertEqual(2, [self batchCount]); + + [self removeMutationBatches:@[ batch2 ]]; + XCTAssertEqual(1, [self batchCount]); + + [self removeMutationBatches:@[ batch1 ]]; + XCTAssertEqual(0, [self batchCount]); + XCTAssertTrue([self.mutationQueue isEmpty]); +} + +- (void)testAcknowledgeBatchID { + if ([self isTestBaseClass]) return; + + // Initial state of an empty queue + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown); + + // Adding mutation batches should not change the highest acked batchID. + FSTMutationBatch *batch1 = [self addMutationBatch]; + FSTMutationBatch *batch2 = [self addMutationBatch]; + FSTMutationBatch *batch3 = [self addMutationBatch]; + XCTAssertGreaterThan(batch1.batchID, kFSTBatchIDUnknown); + XCTAssertGreaterThan(batch2.batchID, batch1.batchID); + XCTAssertGreaterThan(batch3.batchID, batch2.batchID); + + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown); + + [self acknowledgeBatch:batch1]; + [self acknowledgeBatch:batch2]; + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); + + [self removeMutationBatches:@[ batch1 ]]; + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); + + [self removeMutationBatches:@[ batch2 ]]; + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); + + // Batch 3 never acknowledged. + [self removeMutationBatches:@[ batch3 ]]; + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); +} + +- (void)testAcknowledgeThenRemove { + if ([self isTestBaseClass]) return; + + FSTMutationBatch *batch1 = [self addMutationBatch]; + + FSTWriteGroup *group = [self.persistence startGroupWithAction:NSStringFromSelector(_cmd)]; + [self.mutationQueue acknowledgeBatch:batch1 streamToken:nil group:group]; + [self.mutationQueue removeMutationBatches:@[ batch1 ] group:group]; + [self.persistence commitGroup:group]; + + XCTAssertEqual([self batchCount], 0); + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch1.batchID); +} + +- (void)testHighestAcknowledgedBatchIDNeverExceedsNextBatchID { + if ([self isTestBaseClass]) return; + + FSTMutationBatch *batch1 = [self addMutationBatch]; + FSTMutationBatch *batch2 = [self addMutationBatch]; + [self acknowledgeBatch:batch1]; + [self acknowledgeBatch:batch2]; + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); + + [self removeMutationBatches:@[ batch1, batch2 ]]; + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); + + // Restart the queue so that nextBatchID will be reset. + [self.mutationQueue shutdown]; + self.mutationQueue = + [self.persistence mutationQueueForUser:[[FSTUser alloc] initWithUID:@"user"]]; + + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Start MutationQueue"]; + [self.mutationQueue startWithGroup:group]; + [self.persistence commitGroup:group]; + + // Verify that on restart with an empty queue, nextBatchID falls to a lower value. + XCTAssertLessThan(self.mutationQueue.nextBatchID, batch2.batchID); + + // As a result highestAcknowledgedBatchID must also reset lower. + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown); + + // The mutation queue will reset the next batchID after all mutations are removed so adding + // another mutation will cause a collision. + FSTMutationBatch *newBatch = [self addMutationBatch]; + XCTAssertEqual(newBatch.batchID, batch1.batchID); + + // Restart the queue with one unacknowledged batch in it. + group = [self.persistence startGroupWithAction:@"Start MutationQueue"]; + [self.mutationQueue startWithGroup:group]; + [self.persistence commitGroup:group]; + + XCTAssertEqual([self.mutationQueue nextBatchID], newBatch.batchID + 1); + + // highestAcknowledgedBatchID must still be kFSTBatchIDUnknown. + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown); +} + +- (void)testLookupMutationBatch { + if ([self isTestBaseClass]) return; + + // Searching on an empty queue should not find a non-existent batch + FSTMutationBatch *notFound = [self.mutationQueue lookupMutationBatch:42]; + XCTAssertNil(notFound); + + NSMutableArray *batches = [self createBatches:10]; + NSArray *removed = [self makeHoles:@[ @2, @6, @7 ] inBatches:batches]; + + // After removing, a batch should not be found + for (NSUInteger i = 0; i < removed.count; i++) { + notFound = [self.mutationQueue lookupMutationBatch:removed[i].batchID]; + XCTAssertNil(notFound); + } + + // Remaining entries should still be found + for (FSTMutationBatch *batch in batches) { + FSTMutationBatch *found = [self.mutationQueue lookupMutationBatch:batch.batchID]; + XCTAssertEqual(found.batchID, batch.batchID); + } + + // Even on a nonempty queue searching should not find a non-existent batch + notFound = [self.mutationQueue lookupMutationBatch:42]; + XCTAssertNil(notFound); +} + +- (void)testNextMutationBatchAfterBatchID { + if ([self isTestBaseClass]) return; + + NSMutableArray *batches = [self createBatches:10]; + + // This is an array of successors assuming the removals below will happen: + NSArray *afters = @[ batches[3], batches[8], batches[8] ]; + NSArray *removed = [self makeHoles:@[ @2, @6, @7 ] inBatches:batches]; + + for (NSUInteger i = 0; i < batches.count - 1; i++) { + FSTMutationBatch *current = batches[i]; + FSTMutationBatch *next = batches[i + 1]; + FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:current.batchID]; + XCTAssertEqual(found.batchID, next.batchID); + } + + for (NSUInteger i = 0; i < removed.count; i++) { + FSTMutationBatch *current = removed[i]; + FSTMutationBatch *next = afters[i]; + FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:current.batchID]; + XCTAssertEqual(found.batchID, next.batchID); + } + + FSTMutationBatch *first = batches[0]; + FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:first.batchID - 42]; + XCTAssertEqual(found.batchID, first.batchID); + + FSTMutationBatch *last = batches[batches.count - 1]; + FSTMutationBatch *notFound = [self.mutationQueue nextMutationBatchAfterBatchID:last.batchID]; + XCTAssertNil(notFound); +} + +- (void)testAllMutationBatchesThroughBatchID { + if ([self isTestBaseClass]) return; + + NSMutableArray *batches = [self createBatches:10]; + [self makeHoles:@[ @2, @6, @7 ] inBatches:batches]; + + NSArray *found, *expected; + + found = [self.mutationQueue allMutationBatchesThroughBatchID:batches[0].batchID - 1]; + XCTAssertEqualObjects(found, (@[])); + + for (NSUInteger i = 0; i < batches.count; i++) { + found = [self.mutationQueue allMutationBatchesThroughBatchID:batches[i].batchID]; + expected = [batches subarrayWithRange:NSMakeRange(0, i + 1)]; + XCTAssertEqualObjects(found, expected, @"for index %lu", (unsigned long)i); + } +} + +- (void)testAllMutationBatchesAffectingDocumentKey { + if ([self isTestBaseClass]) return; + + NSArray *mutations = @[ + FSTTestSetMutation(@"fob/bar", + @{ @"a" : @1 }), + FSTTestSetMutation(@"foo/bar", + @{ @"a" : @1 }), + FSTTestPatchMutation(@"foo/bar", + @{ @"b" : @1 }, nil), + FSTTestSetMutation(@"foo/bar/suffix/key", + @{ @"a" : @1 }), + FSTTestSetMutation(@"foo/baz", + @{ @"a" : @1 }), + FSTTestSetMutation(@"food/bar", + @{ @"a" : @1 }) + ]; + + // Store all the mutations. + NSMutableArray *batches = [NSMutableArray array]; + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"New mutation batch"]; + for (FSTMutation *mutation in mutations) { + FSTMutationBatch *batch = + [self.mutationQueue addMutationBatchWithWriteTime:[FSTTimestamp timestamp] + mutations:@[ mutation ] + group:group]; + [batches addObject:batch]; + } + [self.persistence commitGroup:group]; + + NSArray *expected = @[ batches[1], batches[2] ]; + NSArray *matches = + [self.mutationQueue allMutationBatchesAffectingDocumentKey:FSTTestDocKey(@"foo/bar")]; + + XCTAssertEqualObjects(matches, expected); +} + +- (void)testAllMutationBatchesAffectingQuery { + if ([self isTestBaseClass]) return; + + NSArray *mutations = @[ + FSTTestSetMutation(@"fob/bar", + @{ @"a" : @1 }), + FSTTestSetMutation(@"foo/bar", + @{ @"a" : @1 }), + FSTTestPatchMutation(@"foo/bar", + @{ @"b" : @1 }, nil), + FSTTestSetMutation(@"foo/bar/suffix/key", + @{ @"a" : @1 }), + FSTTestSetMutation(@"foo/baz", + @{ @"a" : @1 }), + FSTTestSetMutation(@"food/bar", + @{ @"a" : @1 }) + ]; + + // Store all the mutations. + NSMutableArray *batches = [NSMutableArray array]; + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"New mutation batch"]; + for (FSTMutation *mutation in mutations) { + FSTMutationBatch *batch = + [self.mutationQueue addMutationBatchWithWriteTime:[FSTTimestamp timestamp] + mutations:@[ mutation ] + group:group]; + [batches addObject:batch]; + } + [self.persistence commitGroup:group]; + + NSArray *expected = @[ batches[1], batches[2], batches[4] ]; + FSTQuery *query = FSTTestQuery(@"foo"); + NSArray *matches = + [self.mutationQueue allMutationBatchesAffectingQuery:query]; + + XCTAssertEqualObjects(matches, expected); +} + +- (void)testRemoveMutationBatches { + if ([self isTestBaseClass]) return; + + NSMutableArray *batches = [self createBatches:10]; + FSTMutationBatch *last = batches[batches.count - 1]; + + [self removeMutationBatches:@[ batches[0] ]]; + [batches removeObjectAtIndex:0]; + XCTAssertEqual([self batchCount], 9); + + NSArray *found; + + found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; + XCTAssertEqualObjects(found, batches); + XCTAssertEqual(found.count, 9); + + [self removeMutationBatches:@[ batches[0], batches[1], batches[2] ]]; + [batches removeObjectsInRange:NSMakeRange(0, 3)]; + XCTAssertEqual([self batchCount], 6); + + found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; + XCTAssertEqualObjects(found, batches); + XCTAssertEqual(found.count, 6); + + [self removeMutationBatches:@[ batches[batches.count - 1] ]]; + [batches removeObjectAtIndex:batches.count - 1]; + XCTAssertEqual([self batchCount], 5); + + found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; + XCTAssertEqualObjects(found, batches); + XCTAssertEqual(found.count, 5); + + [self removeMutationBatches:@[ batches[3] ]]; + [batches removeObjectAtIndex:3]; + XCTAssertEqual([self batchCount], 4); + + [self removeMutationBatches:@[ batches[1] ]]; + [batches removeObjectAtIndex:1]; + XCTAssertEqual([self batchCount], 3); + + found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; + XCTAssertEqualObjects(found, batches); + XCTAssertEqual(found.count, 3); + XCTAssertFalse([self.mutationQueue isEmpty]); + + [self removeMutationBatches:batches]; + found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; + XCTAssertEqualObjects(found, @[]); + XCTAssertEqual(found.count, 0); + XCTAssertTrue([self.mutationQueue isEmpty]); +} + +- (void)testRemoveMutationBatchesEmitsGarbageEvents { + if ([self isTestBaseClass]) return; + + FSTEagerGarbageCollector *garbageCollector = [[FSTEagerGarbageCollector alloc] init]; + [garbageCollector addGarbageSource:self.mutationQueue]; + + NSMutableArray *batches = [NSMutableArray array]; + [batches addObjectsFromArray:@[ + [self addMutationBatchWithKey:@"foo/bar"], + [self addMutationBatchWithKey:@"foo/ba"], + [self addMutationBatchWithKey:@"foo/bar2"], + [self addMutationBatchWithKey:@"foo/bar"], + [self addMutationBatchWithKey:@"foo/bar/suffix/baz"], + [self addMutationBatchWithKey:@"bar/baz"], + ]]; + + [self removeMutationBatches:@[ batches[0] ]]; + NSSet *garbage = [garbageCollector collectGarbage]; + FSTAssertEqualSets(garbage, @[]); + + [self removeMutationBatches:@[ batches[1] ]]; + garbage = [garbageCollector collectGarbage]; + FSTAssertEqualSets(garbage, @[ FSTTestDocKey(@"foo/ba") ]); + + [self removeMutationBatches:@[ batches[5] ]]; + garbage = [garbageCollector collectGarbage]; + FSTAssertEqualSets(garbage, @[ FSTTestDocKey(@"bar/baz") ]); + + [self removeMutationBatches:@[ batches[2], batches[3] ]]; + garbage = [garbageCollector collectGarbage]; + FSTAssertEqualSets(garbage, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/bar2") ])); + + [batches addObject:[self addMutationBatchWithKey:@"foo/bar/suffix/baz"]]; + garbage = [garbageCollector collectGarbage]; + FSTAssertEqualSets(garbage, @[]); + + [self removeMutationBatches:@[ batches[4], batches[6] ]]; + garbage = [garbageCollector collectGarbage]; + FSTAssertEqualSets(garbage, @[ FSTTestDocKey(@"foo/bar/suffix/baz") ]); +} + +- (void)testStreamToken { + if ([self isTestBaseClass]) return; + + NSData *streamToken1 = [@"token1" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *streamToken2 = [@"token2" dataUsingEncoding:NSUTF8StringEncoding]; + + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"initial stream token"]; + [self.mutationQueue setLastStreamToken:streamToken1 group:group]; + [self.persistence commitGroup:group]; + + FSTMutationBatch *batch1 = [self addMutationBatch]; + [self addMutationBatch]; + + XCTAssertEqualObjects([self.mutationQueue lastStreamToken], streamToken1); + + group = [self.persistence startGroupWithAction:@"acknowledgeBatchID"]; + [self.mutationQueue acknowledgeBatch:batch1 streamToken:streamToken2 group:group]; + [self.persistence commitGroup:group]; + + XCTAssertEqual(self.mutationQueue.highestAcknowledgedBatchID, batch1.batchID); + XCTAssertEqualObjects([self.mutationQueue lastStreamToken], streamToken2); +} + +/** Creates a new FSTMutationBatch with the next batch ID and a set of dummy mutations. */ +- (FSTMutationBatch *)addMutationBatch { + return [self addMutationBatchWithKey:@"foo/bar"]; +} + +/** + * Creates a new FSTMutationBatch with the given key, the next batch ID and a set of dummy + * mutations. + */ +- (FSTMutationBatch *)addMutationBatchWithKey:(NSString *)key { + FSTSetMutation *mutation = FSTTestSetMutation(key, @{ @"a" : @1 }); + + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"New mutation batch"]; + FSTMutationBatch *batch = + [self.mutationQueue addMutationBatchWithWriteTime:[FSTTimestamp timestamp] + mutations:@[ mutation ] + group:group]; + [self.persistence commitGroup:group]; + return batch; +} + +/** + * Creates an array of batches containing @a number dummy FSTMutationBatches. Each has a different + * batchID. + */ +- (NSMutableArray *)createBatches:(int)number { + NSMutableArray *batches = [NSMutableArray array]; + + for (int i = 0; i < number; i++) { + FSTMutationBatch *batch = [self addMutationBatch]; + [batches addObject:batch]; + } + + return batches; +} + +/** + * Calls -acknowledgeBatch:streamToken:group: on the mutation queue in a new group and commits the + * the group. + */ +- (void)acknowledgeBatch:(FSTMutationBatch *)batch { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Ack batchID"]; + [self.mutationQueue acknowledgeBatch:batch streamToken:nil group:group]; + [self.persistence commitGroup:group]; +} + +/** + * Calls -removeMutationBatches:group: on the mutation queue in a new group and commits the group. + */ +- (void)removeMutationBatches:(NSArray *)batches { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Remove mutation batch"]; + [self.mutationQueue removeMutationBatches:batches group:group]; + [self.persistence commitGroup:group]; +} + +/** Returns the number of mutation batches in the mutation queue. */ +- (NSUInteger)batchCount { + return [self.mutationQueue allMutationBatches].count; +} + +/** + * Removes entries from from the given @a batches and returns them. + * + * @param holes An array of indexes in the batches array; in increasing order. Indexes are relative + * to the original state of the batches array, not any intermediate state that might occur. + * @param batches The array to mutate, removing entries from it. + * @return A new array containing all the entries that were removed from @a batches. + */ +- (NSArray *)makeHoles:(NSArray *)holes + inBatches:(NSMutableArray *)batches { + NSMutableArray *removed = [NSMutableArray array]; + for (NSUInteger i = 0; i < holes.count; i++) { + NSUInteger index = holes[i].unsignedIntegerValue - i; + FSTMutationBatch *batch = batches[index]; + [self removeMutationBatches:@[ batch ]]; + + [batches removeObjectAtIndex:index]; + [removed addObject:batch]; + } + return removed; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.m b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.m deleted file mode 100644 index e9e129d..0000000 --- a/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.m +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" - -#import "Firestore/Source/Local/FSTLevelDB.h" -#import "Firestore/Source/Local/FSTLocalSerializer.h" -#import "Firestore/Source/Local/FSTMemoryPersistence.h" -#import "Firestore/Source/Model/FSTDatabaseID.h" -#import "Firestore/Source/Remote/FSTSerializerBeta.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTPersistenceTestHelpers - -+ (NSString *)levelDBDir { - NSError *error; - NSFileManager *files = [NSFileManager defaultManager]; - NSString *dir = - [NSTemporaryDirectory() stringByAppendingPathComponent:@"FSTPersistenceTestHelpers"]; - if ([files fileExistsAtPath:dir]) { - // Delete the directory first to ensure isolation between runs. - BOOL success = [files removeItemAtPath:dir error:&error]; - if (!success) { - [NSException raise:NSInternalInconsistencyException - format:@"Failed to clean up leveldb path %@: %@", dir, error]; - } - } - return dir; -} - -+ (FSTLevelDB *)levelDBPersistence { - NSString *dir = [self levelDBDir]; - - FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; - FSTSerializerBeta *remoteSerializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseID]; - FSTLocalSerializer *serializer = - [[FSTLocalSerializer alloc] initWithRemoteSerializer:remoteSerializer]; - FSTLevelDB *db = [[FSTLevelDB alloc] initWithDirectory:dir serializer:serializer]; - NSError *error; - BOOL success = [db start:&error]; - if (!success) { - [NSException raise:NSInternalInconsistencyException - format:@"Failed to create leveldb path %@: %@", dir, error]; - } - - return db; -} - -+ (FSTMemoryPersistence *)memoryPersistence { - NSError *error; - FSTMemoryPersistence *persistence = [FSTMemoryPersistence persistence]; - BOOL success = [persistence start:&error]; - if (!success) { - [NSException raise:NSInternalInconsistencyException - format:@"Failed to start memory persistence: %@", error]; - } - - return persistence; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.mm b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.mm new file mode 100644 index 0000000..e9e129d --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.mm @@ -0,0 +1,77 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" + +#import "Firestore/Source/Local/FSTLevelDB.h" +#import "Firestore/Source/Local/FSTLocalSerializer.h" +#import "Firestore/Source/Local/FSTMemoryPersistence.h" +#import "Firestore/Source/Model/FSTDatabaseID.h" +#import "Firestore/Source/Remote/FSTSerializerBeta.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTPersistenceTestHelpers + ++ (NSString *)levelDBDir { + NSError *error; + NSFileManager *files = [NSFileManager defaultManager]; + NSString *dir = + [NSTemporaryDirectory() stringByAppendingPathComponent:@"FSTPersistenceTestHelpers"]; + if ([files fileExistsAtPath:dir]) { + // Delete the directory first to ensure isolation between runs. + BOOL success = [files removeItemAtPath:dir error:&error]; + if (!success) { + [NSException raise:NSInternalInconsistencyException + format:@"Failed to clean up leveldb path %@: %@", dir, error]; + } + } + return dir; +} + ++ (FSTLevelDB *)levelDBPersistence { + NSString *dir = [self levelDBDir]; + + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; + FSTSerializerBeta *remoteSerializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseID]; + FSTLocalSerializer *serializer = + [[FSTLocalSerializer alloc] initWithRemoteSerializer:remoteSerializer]; + FSTLevelDB *db = [[FSTLevelDB alloc] initWithDirectory:dir serializer:serializer]; + NSError *error; + BOOL success = [db start:&error]; + if (!success) { + [NSException raise:NSInternalInconsistencyException + format:@"Failed to create leveldb path %@: %@", dir, error]; + } + + return db; +} + ++ (FSTMemoryPersistence *)memoryPersistence { + NSError *error; + FSTMemoryPersistence *persistence = [FSTMemoryPersistence persistence]; + BOOL success = [persistence start:&error]; + if (!success) { + [NSException raise:NSInternalInconsistencyException + format:@"Failed to start memory persistence: %@", error]; + } + + return persistence; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTQueryCacheTests.m b/Firestore/Example/Tests/Local/FSTQueryCacheTests.m deleted file mode 100644 index 0c6a2a4..0000000 --- a/Firestore/Example/Tests/Local/FSTQueryCacheTests.m +++ /dev/null @@ -1,433 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Example/Tests/Local/FSTQueryCacheTests.h" - -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Local/FSTEagerGarbageCollector.h" -#import "Firestore/Source/Local/FSTPersistence.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Local/FSTWriteGroup.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" -#import "Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTQueryCacheTests { - FSTQuery *_queryRooms; - FSTListenSequenceNumber _previousSequenceNumber; - FSTTargetID _previousTargetID; - FSTTestSnapshotVersion _previousSnapshotVersion; -} - -- (void)setUp { - [super setUp]; - - _queryRooms = FSTTestQuery(@"rooms"); - _previousSequenceNumber = 1000; - _previousTargetID = 500; - _previousSnapshotVersion = 100; -} - -/** - * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for - * FSTSpecTests since it is incomplete without the implementations supplied by its subclasses. - */ -- (BOOL)isTestBaseClass { - return [self class] == [FSTQueryCacheTests class]; -} - -- (void)testReadQueryNotInCache { - if ([self isTestBaseClass]) return; - - XCTAssertNil([self.queryCache queryDataForQuery:_queryRooms]); -} - -- (void)testSetAndReadAQuery { - if ([self isTestBaseClass]) return; - - FSTQueryData *queryData = [self queryDataWithQuery:_queryRooms]; - [self addQueryData:queryData]; - - FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms]; - XCTAssertEqualObjects(result.query, queryData.query); - XCTAssertEqual(result.targetID, queryData.targetID); - XCTAssertEqualObjects(result.resumeToken, queryData.resumeToken); -} - -- (void)testCanonicalIDCollision { - if ([self isTestBaseClass]) return; - - // Type information is currently lost in our canonicalID implementations so this currently an - // easy way to force colliding canonicalIDs - FSTQuery *q1 = [FSTTestQuery(@"a") queryByAddingFilter:FSTTestFilter(@"foo", @"==", @(1))]; - FSTQuery *q2 = [FSTTestQuery(@"a") queryByAddingFilter:FSTTestFilter(@"foo", @"==", @"1")]; - XCTAssertEqualObjects(q1.canonicalID, q2.canonicalID); - - FSTQueryData *data1 = [self queryDataWithQuery:q1]; - [self addQueryData:data1]; - - // Using the other query should not return the query cache entry despite equal canonicalIDs. - XCTAssertNil([self.queryCache queryDataForQuery:q2]); - XCTAssertEqualObjects([self.queryCache queryDataForQuery:q1], data1); - - FSTQueryData *data2 = [self queryDataWithQuery:q2]; - [self addQueryData:data2]; - - XCTAssertEqualObjects([self.queryCache queryDataForQuery:q1], data1); - XCTAssertEqualObjects([self.queryCache queryDataForQuery:q2], data2); - - [self removeQueryData:data1]; - XCTAssertNil([self.queryCache queryDataForQuery:q1]); - XCTAssertEqualObjects([self.queryCache queryDataForQuery:q2], data2); - - [self removeQueryData:data2]; - XCTAssertNil([self.queryCache queryDataForQuery:q1]); - XCTAssertNil([self.queryCache queryDataForQuery:q2]); -} - -- (void)testSetQueryToNewValue { - if ([self isTestBaseClass]) return; - - FSTQueryData *queryData1 = - [self queryDataWithQuery:_queryRooms targetID:1 listenSequenceNumber:10 version:1]; - [self addQueryData:queryData1]; - - FSTQueryData *queryData2 = - [self queryDataWithQuery:_queryRooms targetID:1 listenSequenceNumber:10 version:2]; - [self addQueryData:queryData2]; - - FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms]; - XCTAssertNotEqualObjects(queryData2.resumeToken, queryData1.resumeToken); - XCTAssertNotEqualObjects(queryData2.snapshotVersion, queryData1.snapshotVersion); - XCTAssertEqualObjects(result.resumeToken, queryData2.resumeToken); - XCTAssertEqualObjects(result.snapshotVersion, queryData2.snapshotVersion); -} - -- (void)testRemoveQuery { - if ([self isTestBaseClass]) return; - - FSTQueryData *queryData1 = [self queryDataWithQuery:_queryRooms]; - [self addQueryData:queryData1]; - - [self removeQueryData:queryData1]; - - FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms]; - XCTAssertNil(result); -} - -- (void)testRemoveNonExistentQuery { - if ([self isTestBaseClass]) return; - - FSTQueryData *queryData = [self queryDataWithQuery:_queryRooms]; - - // no-op, but make sure it doesn't throw. - XCTAssertNoThrow([self removeQueryData:queryData]); -} - -- (void)testRemoveQueryRemovesMatchingKeysToo { - if ([self isTestBaseClass]) return; - - FSTQueryData *rooms = [self queryDataWithQuery:_queryRooms]; - [self addQueryData:rooms]; - - FSTDocumentKey *key1 = FSTTestDocKey(@"rooms/foo"); - FSTDocumentKey *key2 = FSTTestDocKey(@"rooms/bar"); - [self addMatchingKey:key1 forTargetID:rooms.targetID]; - [self addMatchingKey:key2 forTargetID:rooms.targetID]; - - XCTAssertTrue([self.queryCache containsKey:key1]); - XCTAssertTrue([self.queryCache containsKey:key2]); - - [self removeQueryData:rooms]; - XCTAssertFalse([self.queryCache containsKey:key1]); - XCTAssertFalse([self.queryCache containsKey:key2]); -} - -- (void)testAddOrRemoveMatchingKeys { - if ([self isTestBaseClass]) return; - - FSTDocumentKey *key = FSTTestDocKey(@"foo/bar"); - - XCTAssertFalse([self.queryCache containsKey:key]); - - [self addMatchingKey:key forTargetID:1]; - XCTAssertTrue([self.queryCache containsKey:key]); - - [self addMatchingKey:key forTargetID:2]; - XCTAssertTrue([self.queryCache containsKey:key]); - - [self removeMatchingKey:key forTargetID:1]; - XCTAssertTrue([self.queryCache containsKey:key]); - - [self removeMatchingKey:key forTargetID:2]; - XCTAssertFalse([self.queryCache containsKey:key]); -} - -- (void)testRemoveMatchingKeysForTargetID { - if ([self isTestBaseClass]) return; - - FSTDocumentKey *key1 = FSTTestDocKey(@"foo/bar"); - FSTDocumentKey *key2 = FSTTestDocKey(@"foo/baz"); - FSTDocumentKey *key3 = FSTTestDocKey(@"foo/blah"); - - [self addMatchingKey:key1 forTargetID:1]; - [self addMatchingKey:key2 forTargetID:1]; - [self addMatchingKey:key3 forTargetID:2]; - XCTAssertTrue([self.queryCache containsKey:key1]); - XCTAssertTrue([self.queryCache containsKey:key2]); - XCTAssertTrue([self.queryCache containsKey:key3]); - - [self removeMatchingKeysForTargetID:1]; - XCTAssertFalse([self.queryCache containsKey:key1]); - XCTAssertFalse([self.queryCache containsKey:key2]); - XCTAssertTrue([self.queryCache containsKey:key3]); - - [self removeMatchingKeysForTargetID:2]; - XCTAssertFalse([self.queryCache containsKey:key1]); - XCTAssertFalse([self.queryCache containsKey:key2]); - XCTAssertFalse([self.queryCache containsKey:key3]); -} - -- (void)testRemoveEmitsGarbageEvents { - if ([self isTestBaseClass]) return; - - FSTEagerGarbageCollector *garbageCollector = [[FSTEagerGarbageCollector alloc] init]; - [garbageCollector addGarbageSource:self.queryCache]; - FSTAssertEqualSets([garbageCollector collectGarbage], @[]); - - FSTQueryData *rooms = [self queryDataWithQuery:FSTTestQuery(@"rooms")]; - FSTDocumentKey *room1 = FSTTestDocKey(@"rooms/bar"); - FSTDocumentKey *room2 = FSTTestDocKey(@"rooms/foo"); - [self addQueryData:rooms]; - [self addMatchingKey:room1 forTargetID:rooms.targetID]; - [self addMatchingKey:room2 forTargetID:rooms.targetID]; - - FSTQueryData *halls = [self queryDataWithQuery:FSTTestQuery(@"halls")]; - FSTDocumentKey *hall1 = FSTTestDocKey(@"halls/bar"); - FSTDocumentKey *hall2 = FSTTestDocKey(@"halls/foo"); - [self addQueryData:halls]; - [self addMatchingKey:hall1 forTargetID:halls.targetID]; - [self addMatchingKey:hall2 forTargetID:halls.targetID]; - - FSTAssertEqualSets([garbageCollector collectGarbage], @[]); - - [self removeMatchingKey:room1 forTargetID:rooms.targetID]; - FSTAssertEqualSets([garbageCollector collectGarbage], @[ room1 ]); - - [self removeQueryData:rooms]; - FSTAssertEqualSets([garbageCollector collectGarbage], @[ room2 ]); - - [self removeMatchingKeysForTargetID:halls.targetID]; - FSTAssertEqualSets([garbageCollector collectGarbage], (@[ hall1, hall2 ])); -} - -- (void)testMatchingKeysForTargetID { - if ([self isTestBaseClass]) return; - - FSTDocumentKey *key1 = FSTTestDocKey(@"foo/bar"); - FSTDocumentKey *key2 = FSTTestDocKey(@"foo/baz"); - FSTDocumentKey *key3 = FSTTestDocKey(@"foo/blah"); - - [self addMatchingKey:key1 forTargetID:1]; - [self addMatchingKey:key2 forTargetID:1]; - [self addMatchingKey:key3 forTargetID:2]; - - FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:1], (@[ key1, key2 ])); - FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:2], @[ key3 ]); - - [self addMatchingKey:key1 forTargetID:2]; - FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:1], (@[ key1, key2 ])); - FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:2], (@[ key1, key3 ])); -} - -- (void)testHighestListenSequenceNumber { - if ([self isTestBaseClass]) return; - - FSTQueryData *query1 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"rooms") - targetID:1 - listenSequenceNumber:10 - purpose:FSTQueryPurposeListen]; - [self addQueryData:query1]; - FSTQueryData *query2 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"halls") - targetID:2 - listenSequenceNumber:20 - purpose:FSTQueryPurposeListen]; - [self addQueryData:query2]; - XCTAssertEqual([self.queryCache highestListenSequenceNumber], 20); - - // TargetIDs never come down. - [self removeQueryData:query2]; - XCTAssertEqual([self.queryCache highestListenSequenceNumber], 20); - - // A query with an empty result set still counts. - FSTQueryData *query3 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"garages") - targetID:42 - listenSequenceNumber:100 - purpose:FSTQueryPurposeListen]; - [self addQueryData:query3]; - XCTAssertEqual([self.queryCache highestListenSequenceNumber], 100); - - [self removeQueryData:query1]; - XCTAssertEqual([self.queryCache highestListenSequenceNumber], 100); - - [self removeQueryData:query3]; - XCTAssertEqual([self.queryCache highestListenSequenceNumber], 100); - - // Verify that the highestTargetID even survives restarts. - [self.queryCache shutdown]; - self.queryCache = [self.persistence queryCache]; - [self.queryCache start]; - XCTAssertEqual([self.queryCache highestListenSequenceNumber], 100); -} - -- (void)testHighestTargetID { - if ([self isTestBaseClass]) return; - - XCTAssertEqual([self.queryCache highestTargetID], 0); - - FSTQueryData *query1 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"rooms") - targetID:1 - listenSequenceNumber:10 - purpose:FSTQueryPurposeListen]; - FSTDocumentKey *key1 = FSTTestDocKey(@"rooms/bar"); - FSTDocumentKey *key2 = FSTTestDocKey(@"rooms/foo"); - [self addQueryData:query1]; - [self addMatchingKey:key1 forTargetID:1]; - [self addMatchingKey:key2 forTargetID:1]; - - FSTQueryData *query2 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"halls") - targetID:2 - listenSequenceNumber:20 - purpose:FSTQueryPurposeListen]; - FSTDocumentKey *key3 = FSTTestDocKey(@"halls/foo"); - [self addQueryData:query2]; - [self addMatchingKey:key3 forTargetID:2]; - XCTAssertEqual([self.queryCache highestTargetID], 2); - - // TargetIDs never come down. - [self removeQueryData:query2]; - XCTAssertEqual([self.queryCache highestTargetID], 2); - - // A query with an empty result set still counts. - FSTQueryData *query3 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"garages") - targetID:42 - listenSequenceNumber:100 - purpose:FSTQueryPurposeListen]; - [self addQueryData:query3]; - XCTAssertEqual([self.queryCache highestTargetID], 42); - - [self removeQueryData:query1]; - XCTAssertEqual([self.queryCache highestTargetID], 42); - - [self removeQueryData:query3]; - XCTAssertEqual([self.queryCache highestTargetID], 42); - - // Verify that the highestTargetID even survives restarts. - [self.queryCache shutdown]; - self.queryCache = [self.persistence queryCache]; - [self.queryCache start]; - XCTAssertEqual([self.queryCache highestTargetID], 42); -} - -- (void)testLastRemoteSnapshotVersion { - if ([self isTestBaseClass]) return; - - XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], - [FSTSnapshotVersion noVersion]); - - // Can set the snapshot version. - FSTWriteGroup *group = [self.persistence startGroupWithAction:@"setLastRemoteSnapshotVersion"]; - [self.queryCache setLastRemoteSnapshotVersion:FSTTestVersion(42) group:group]; - [self.persistence commitGroup:group]; - XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], FSTTestVersion(42)); - - // Snapshot version persists restarts. - self.queryCache = [self.persistence queryCache]; - [self.queryCache start]; - XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], FSTTestVersion(42)); -} - -#pragma mark - Helpers - -/** - * Creates a new FSTQueryData object from the given parameters, synthesizing a resume token from - * the snapshot version. - */ -- (FSTQueryData *)queryDataWithQuery:(FSTQuery *)query { - return [self queryDataWithQuery:query - targetID:++_previousTargetID - listenSequenceNumber:++_previousSequenceNumber - version:++_previousSnapshotVersion]; -} - -- (FSTQueryData *)queryDataWithQuery:(FSTQuery *)query - targetID:(FSTTargetID)targetID - listenSequenceNumber:(FSTListenSequenceNumber)sequenceNumber - version:(FSTTestSnapshotVersion)version { - NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(version); - return [[FSTQueryData alloc] initWithQuery:query - targetID:targetID - listenSequenceNumber:sequenceNumber - purpose:FSTQueryPurposeListen - snapshotVersion:FSTTestVersion(version) - resumeToken:resumeToken]; -} - -/** Adds the given query data to the queryCache under test, committing immediately. */ -- (void)addQueryData:(FSTQueryData *)queryData { - FSTWriteGroup *group = [self.persistence startGroupWithAction:@"addQueryData"]; - [self.queryCache addQueryData:queryData group:group]; - [self.persistence commitGroup:group]; -} - -/** Removes the given query data from the queryCache under test, committing immediately. */ -- (void)removeQueryData:(FSTQueryData *)queryData { - FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeQueryData"]; - [self.queryCache removeQueryData:queryData group:group]; - [self.persistence commitGroup:group]; -} - -- (void)addMatchingKey:(FSTDocumentKey *)key forTargetID:(FSTTargetID)targetID { - FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet]; - keys = [keys setByAddingObject:key]; - - FSTWriteGroup *group = [self.persistence startGroupWithAction:@"addMatchingKeys"]; - [self.queryCache addMatchingKeys:keys forTargetID:targetID group:group]; - [self.persistence commitGroup:group]; -} - -- (void)removeMatchingKey:(FSTDocumentKey *)key forTargetID:(FSTTargetID)targetID { - FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet]; - keys = [keys setByAddingObject:key]; - - FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeMatchingKeys"]; - [self.queryCache removeMatchingKeys:keys forTargetID:targetID group:group]; - [self.persistence commitGroup:group]; -} - -- (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID { - FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeMatchingKeysForTargetID"]; - [self.queryCache removeMatchingKeysForTargetID:targetID group:group]; - [self.persistence commitGroup:group]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTQueryCacheTests.mm b/Firestore/Example/Tests/Local/FSTQueryCacheTests.mm new file mode 100644 index 0000000..0c6a2a4 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTQueryCacheTests.mm @@ -0,0 +1,433 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Example/Tests/Local/FSTQueryCacheTests.h" + +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Local/FSTEagerGarbageCollector.h" +#import "Firestore/Source/Local/FSTPersistence.h" +#import "Firestore/Source/Local/FSTQueryData.h" +#import "Firestore/Source/Local/FSTWriteGroup.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" +#import "Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTQueryCacheTests { + FSTQuery *_queryRooms; + FSTListenSequenceNumber _previousSequenceNumber; + FSTTargetID _previousTargetID; + FSTTestSnapshotVersion _previousSnapshotVersion; +} + +- (void)setUp { + [super setUp]; + + _queryRooms = FSTTestQuery(@"rooms"); + _previousSequenceNumber = 1000; + _previousTargetID = 500; + _previousSnapshotVersion = 100; +} + +/** + * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for + * FSTSpecTests since it is incomplete without the implementations supplied by its subclasses. + */ +- (BOOL)isTestBaseClass { + return [self class] == [FSTQueryCacheTests class]; +} + +- (void)testReadQueryNotInCache { + if ([self isTestBaseClass]) return; + + XCTAssertNil([self.queryCache queryDataForQuery:_queryRooms]); +} + +- (void)testSetAndReadAQuery { + if ([self isTestBaseClass]) return; + + FSTQueryData *queryData = [self queryDataWithQuery:_queryRooms]; + [self addQueryData:queryData]; + + FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms]; + XCTAssertEqualObjects(result.query, queryData.query); + XCTAssertEqual(result.targetID, queryData.targetID); + XCTAssertEqualObjects(result.resumeToken, queryData.resumeToken); +} + +- (void)testCanonicalIDCollision { + if ([self isTestBaseClass]) return; + + // Type information is currently lost in our canonicalID implementations so this currently an + // easy way to force colliding canonicalIDs + FSTQuery *q1 = [FSTTestQuery(@"a") queryByAddingFilter:FSTTestFilter(@"foo", @"==", @(1))]; + FSTQuery *q2 = [FSTTestQuery(@"a") queryByAddingFilter:FSTTestFilter(@"foo", @"==", @"1")]; + XCTAssertEqualObjects(q1.canonicalID, q2.canonicalID); + + FSTQueryData *data1 = [self queryDataWithQuery:q1]; + [self addQueryData:data1]; + + // Using the other query should not return the query cache entry despite equal canonicalIDs. + XCTAssertNil([self.queryCache queryDataForQuery:q2]); + XCTAssertEqualObjects([self.queryCache queryDataForQuery:q1], data1); + + FSTQueryData *data2 = [self queryDataWithQuery:q2]; + [self addQueryData:data2]; + + XCTAssertEqualObjects([self.queryCache queryDataForQuery:q1], data1); + XCTAssertEqualObjects([self.queryCache queryDataForQuery:q2], data2); + + [self removeQueryData:data1]; + XCTAssertNil([self.queryCache queryDataForQuery:q1]); + XCTAssertEqualObjects([self.queryCache queryDataForQuery:q2], data2); + + [self removeQueryData:data2]; + XCTAssertNil([self.queryCache queryDataForQuery:q1]); + XCTAssertNil([self.queryCache queryDataForQuery:q2]); +} + +- (void)testSetQueryToNewValue { + if ([self isTestBaseClass]) return; + + FSTQueryData *queryData1 = + [self queryDataWithQuery:_queryRooms targetID:1 listenSequenceNumber:10 version:1]; + [self addQueryData:queryData1]; + + FSTQueryData *queryData2 = + [self queryDataWithQuery:_queryRooms targetID:1 listenSequenceNumber:10 version:2]; + [self addQueryData:queryData2]; + + FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms]; + XCTAssertNotEqualObjects(queryData2.resumeToken, queryData1.resumeToken); + XCTAssertNotEqualObjects(queryData2.snapshotVersion, queryData1.snapshotVersion); + XCTAssertEqualObjects(result.resumeToken, queryData2.resumeToken); + XCTAssertEqualObjects(result.snapshotVersion, queryData2.snapshotVersion); +} + +- (void)testRemoveQuery { + if ([self isTestBaseClass]) return; + + FSTQueryData *queryData1 = [self queryDataWithQuery:_queryRooms]; + [self addQueryData:queryData1]; + + [self removeQueryData:queryData1]; + + FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms]; + XCTAssertNil(result); +} + +- (void)testRemoveNonExistentQuery { + if ([self isTestBaseClass]) return; + + FSTQueryData *queryData = [self queryDataWithQuery:_queryRooms]; + + // no-op, but make sure it doesn't throw. + XCTAssertNoThrow([self removeQueryData:queryData]); +} + +- (void)testRemoveQueryRemovesMatchingKeysToo { + if ([self isTestBaseClass]) return; + + FSTQueryData *rooms = [self queryDataWithQuery:_queryRooms]; + [self addQueryData:rooms]; + + FSTDocumentKey *key1 = FSTTestDocKey(@"rooms/foo"); + FSTDocumentKey *key2 = FSTTestDocKey(@"rooms/bar"); + [self addMatchingKey:key1 forTargetID:rooms.targetID]; + [self addMatchingKey:key2 forTargetID:rooms.targetID]; + + XCTAssertTrue([self.queryCache containsKey:key1]); + XCTAssertTrue([self.queryCache containsKey:key2]); + + [self removeQueryData:rooms]; + XCTAssertFalse([self.queryCache containsKey:key1]); + XCTAssertFalse([self.queryCache containsKey:key2]); +} + +- (void)testAddOrRemoveMatchingKeys { + if ([self isTestBaseClass]) return; + + FSTDocumentKey *key = FSTTestDocKey(@"foo/bar"); + + XCTAssertFalse([self.queryCache containsKey:key]); + + [self addMatchingKey:key forTargetID:1]; + XCTAssertTrue([self.queryCache containsKey:key]); + + [self addMatchingKey:key forTargetID:2]; + XCTAssertTrue([self.queryCache containsKey:key]); + + [self removeMatchingKey:key forTargetID:1]; + XCTAssertTrue([self.queryCache containsKey:key]); + + [self removeMatchingKey:key forTargetID:2]; + XCTAssertFalse([self.queryCache containsKey:key]); +} + +- (void)testRemoveMatchingKeysForTargetID { + if ([self isTestBaseClass]) return; + + FSTDocumentKey *key1 = FSTTestDocKey(@"foo/bar"); + FSTDocumentKey *key2 = FSTTestDocKey(@"foo/baz"); + FSTDocumentKey *key3 = FSTTestDocKey(@"foo/blah"); + + [self addMatchingKey:key1 forTargetID:1]; + [self addMatchingKey:key2 forTargetID:1]; + [self addMatchingKey:key3 forTargetID:2]; + XCTAssertTrue([self.queryCache containsKey:key1]); + XCTAssertTrue([self.queryCache containsKey:key2]); + XCTAssertTrue([self.queryCache containsKey:key3]); + + [self removeMatchingKeysForTargetID:1]; + XCTAssertFalse([self.queryCache containsKey:key1]); + XCTAssertFalse([self.queryCache containsKey:key2]); + XCTAssertTrue([self.queryCache containsKey:key3]); + + [self removeMatchingKeysForTargetID:2]; + XCTAssertFalse([self.queryCache containsKey:key1]); + XCTAssertFalse([self.queryCache containsKey:key2]); + XCTAssertFalse([self.queryCache containsKey:key3]); +} + +- (void)testRemoveEmitsGarbageEvents { + if ([self isTestBaseClass]) return; + + FSTEagerGarbageCollector *garbageCollector = [[FSTEagerGarbageCollector alloc] init]; + [garbageCollector addGarbageSource:self.queryCache]; + FSTAssertEqualSets([garbageCollector collectGarbage], @[]); + + FSTQueryData *rooms = [self queryDataWithQuery:FSTTestQuery(@"rooms")]; + FSTDocumentKey *room1 = FSTTestDocKey(@"rooms/bar"); + FSTDocumentKey *room2 = FSTTestDocKey(@"rooms/foo"); + [self addQueryData:rooms]; + [self addMatchingKey:room1 forTargetID:rooms.targetID]; + [self addMatchingKey:room2 forTargetID:rooms.targetID]; + + FSTQueryData *halls = [self queryDataWithQuery:FSTTestQuery(@"halls")]; + FSTDocumentKey *hall1 = FSTTestDocKey(@"halls/bar"); + FSTDocumentKey *hall2 = FSTTestDocKey(@"halls/foo"); + [self addQueryData:halls]; + [self addMatchingKey:hall1 forTargetID:halls.targetID]; + [self addMatchingKey:hall2 forTargetID:halls.targetID]; + + FSTAssertEqualSets([garbageCollector collectGarbage], @[]); + + [self removeMatchingKey:room1 forTargetID:rooms.targetID]; + FSTAssertEqualSets([garbageCollector collectGarbage], @[ room1 ]); + + [self removeQueryData:rooms]; + FSTAssertEqualSets([garbageCollector collectGarbage], @[ room2 ]); + + [self removeMatchingKeysForTargetID:halls.targetID]; + FSTAssertEqualSets([garbageCollector collectGarbage], (@[ hall1, hall2 ])); +} + +- (void)testMatchingKeysForTargetID { + if ([self isTestBaseClass]) return; + + FSTDocumentKey *key1 = FSTTestDocKey(@"foo/bar"); + FSTDocumentKey *key2 = FSTTestDocKey(@"foo/baz"); + FSTDocumentKey *key3 = FSTTestDocKey(@"foo/blah"); + + [self addMatchingKey:key1 forTargetID:1]; + [self addMatchingKey:key2 forTargetID:1]; + [self addMatchingKey:key3 forTargetID:2]; + + FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:1], (@[ key1, key2 ])); + FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:2], @[ key3 ]); + + [self addMatchingKey:key1 forTargetID:2]; + FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:1], (@[ key1, key2 ])); + FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:2], (@[ key1, key3 ])); +} + +- (void)testHighestListenSequenceNumber { + if ([self isTestBaseClass]) return; + + FSTQueryData *query1 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"rooms") + targetID:1 + listenSequenceNumber:10 + purpose:FSTQueryPurposeListen]; + [self addQueryData:query1]; + FSTQueryData *query2 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"halls") + targetID:2 + listenSequenceNumber:20 + purpose:FSTQueryPurposeListen]; + [self addQueryData:query2]; + XCTAssertEqual([self.queryCache highestListenSequenceNumber], 20); + + // TargetIDs never come down. + [self removeQueryData:query2]; + XCTAssertEqual([self.queryCache highestListenSequenceNumber], 20); + + // A query with an empty result set still counts. + FSTQueryData *query3 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"garages") + targetID:42 + listenSequenceNumber:100 + purpose:FSTQueryPurposeListen]; + [self addQueryData:query3]; + XCTAssertEqual([self.queryCache highestListenSequenceNumber], 100); + + [self removeQueryData:query1]; + XCTAssertEqual([self.queryCache highestListenSequenceNumber], 100); + + [self removeQueryData:query3]; + XCTAssertEqual([self.queryCache highestListenSequenceNumber], 100); + + // Verify that the highestTargetID even survives restarts. + [self.queryCache shutdown]; + self.queryCache = [self.persistence queryCache]; + [self.queryCache start]; + XCTAssertEqual([self.queryCache highestListenSequenceNumber], 100); +} + +- (void)testHighestTargetID { + if ([self isTestBaseClass]) return; + + XCTAssertEqual([self.queryCache highestTargetID], 0); + + FSTQueryData *query1 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"rooms") + targetID:1 + listenSequenceNumber:10 + purpose:FSTQueryPurposeListen]; + FSTDocumentKey *key1 = FSTTestDocKey(@"rooms/bar"); + FSTDocumentKey *key2 = FSTTestDocKey(@"rooms/foo"); + [self addQueryData:query1]; + [self addMatchingKey:key1 forTargetID:1]; + [self addMatchingKey:key2 forTargetID:1]; + + FSTQueryData *query2 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"halls") + targetID:2 + listenSequenceNumber:20 + purpose:FSTQueryPurposeListen]; + FSTDocumentKey *key3 = FSTTestDocKey(@"halls/foo"); + [self addQueryData:query2]; + [self addMatchingKey:key3 forTargetID:2]; + XCTAssertEqual([self.queryCache highestTargetID], 2); + + // TargetIDs never come down. + [self removeQueryData:query2]; + XCTAssertEqual([self.queryCache highestTargetID], 2); + + // A query with an empty result set still counts. + FSTQueryData *query3 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"garages") + targetID:42 + listenSequenceNumber:100 + purpose:FSTQueryPurposeListen]; + [self addQueryData:query3]; + XCTAssertEqual([self.queryCache highestTargetID], 42); + + [self removeQueryData:query1]; + XCTAssertEqual([self.queryCache highestTargetID], 42); + + [self removeQueryData:query3]; + XCTAssertEqual([self.queryCache highestTargetID], 42); + + // Verify that the highestTargetID even survives restarts. + [self.queryCache shutdown]; + self.queryCache = [self.persistence queryCache]; + [self.queryCache start]; + XCTAssertEqual([self.queryCache highestTargetID], 42); +} + +- (void)testLastRemoteSnapshotVersion { + if ([self isTestBaseClass]) return; + + XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], + [FSTSnapshotVersion noVersion]); + + // Can set the snapshot version. + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"setLastRemoteSnapshotVersion"]; + [self.queryCache setLastRemoteSnapshotVersion:FSTTestVersion(42) group:group]; + [self.persistence commitGroup:group]; + XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], FSTTestVersion(42)); + + // Snapshot version persists restarts. + self.queryCache = [self.persistence queryCache]; + [self.queryCache start]; + XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], FSTTestVersion(42)); +} + +#pragma mark - Helpers + +/** + * Creates a new FSTQueryData object from the given parameters, synthesizing a resume token from + * the snapshot version. + */ +- (FSTQueryData *)queryDataWithQuery:(FSTQuery *)query { + return [self queryDataWithQuery:query + targetID:++_previousTargetID + listenSequenceNumber:++_previousSequenceNumber + version:++_previousSnapshotVersion]; +} + +- (FSTQueryData *)queryDataWithQuery:(FSTQuery *)query + targetID:(FSTTargetID)targetID + listenSequenceNumber:(FSTListenSequenceNumber)sequenceNumber + version:(FSTTestSnapshotVersion)version { + NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(version); + return [[FSTQueryData alloc] initWithQuery:query + targetID:targetID + listenSequenceNumber:sequenceNumber + purpose:FSTQueryPurposeListen + snapshotVersion:FSTTestVersion(version) + resumeToken:resumeToken]; +} + +/** Adds the given query data to the queryCache under test, committing immediately. */ +- (void)addQueryData:(FSTQueryData *)queryData { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"addQueryData"]; + [self.queryCache addQueryData:queryData group:group]; + [self.persistence commitGroup:group]; +} + +/** Removes the given query data from the queryCache under test, committing immediately. */ +- (void)removeQueryData:(FSTQueryData *)queryData { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeQueryData"]; + [self.queryCache removeQueryData:queryData group:group]; + [self.persistence commitGroup:group]; +} + +- (void)addMatchingKey:(FSTDocumentKey *)key forTargetID:(FSTTargetID)targetID { + FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet]; + keys = [keys setByAddingObject:key]; + + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"addMatchingKeys"]; + [self.queryCache addMatchingKeys:keys forTargetID:targetID group:group]; + [self.persistence commitGroup:group]; +} + +- (void)removeMatchingKey:(FSTDocumentKey *)key forTargetID:(FSTTargetID)targetID { + FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet]; + keys = [keys setByAddingObject:key]; + + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeMatchingKeys"]; + [self.queryCache removeMatchingKeys:keys forTargetID:targetID group:group]; + [self.persistence commitGroup:group]; +} + +- (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeMatchingKeysForTargetID"]; + [self.queryCache removeMatchingKeysForTargetID:targetID group:group]; + [self.persistence commitGroup:group]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTReferenceSetTests.m b/Firestore/Example/Tests/Local/FSTReferenceSetTests.m deleted file mode 100644 index 802117a..0000000 --- a/Firestore/Example/Tests/Local/FSTReferenceSetTests.m +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTReferenceSet.h" - -#import - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTReferenceSetTests : XCTestCase -@end - -@implementation FSTReferenceSetTests - -- (void)testAddOrRemoveReferences { - FSTDocumentKey *key = FSTTestDocKey(@"foo/bar"); - - FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; - XCTAssertTrue([referenceSet isEmpty]); - XCTAssertFalse([referenceSet containsKey:key]); - - [referenceSet addReferenceToKey:key forID:1]; - XCTAssertTrue([referenceSet containsKey:key]); - XCTAssertFalse([referenceSet isEmpty]); - - [referenceSet addReferenceToKey:key forID:2]; - XCTAssertTrue([referenceSet containsKey:key]); - - [referenceSet removeReferenceToKey:key forID:1]; - XCTAssertTrue([referenceSet containsKey:key]); - - [referenceSet removeReferenceToKey:key forID:3]; - XCTAssertTrue([referenceSet containsKey:key]); - - [referenceSet removeReferenceToKey:key forID:2]; - XCTAssertFalse([referenceSet containsKey:key]); - XCTAssertTrue([referenceSet isEmpty]); -} - -- (void)testRemoveAllReferencesForTargetID { - FSTDocumentKey *key1 = FSTTestDocKey(@"foo/bar"); - FSTDocumentKey *key2 = FSTTestDocKey(@"foo/baz"); - FSTDocumentKey *key3 = FSTTestDocKey(@"foo/blah"); - FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; - - [referenceSet addReferenceToKey:key1 forID:1]; - [referenceSet addReferenceToKey:key2 forID:1]; - [referenceSet addReferenceToKey:key3 forID:2]; - XCTAssertFalse([referenceSet isEmpty]); - XCTAssertTrue([referenceSet containsKey:key1]); - XCTAssertTrue([referenceSet containsKey:key2]); - XCTAssertTrue([referenceSet containsKey:key3]); - - [referenceSet removeReferencesForID:1]; - XCTAssertFalse([referenceSet isEmpty]); - XCTAssertFalse([referenceSet containsKey:key1]); - XCTAssertFalse([referenceSet containsKey:key2]); - XCTAssertTrue([referenceSet containsKey:key3]); - - [referenceSet removeReferencesForID:2]; - XCTAssertTrue([referenceSet isEmpty]); - XCTAssertFalse([referenceSet containsKey:key1]); - XCTAssertFalse([referenceSet containsKey:key2]); - XCTAssertFalse([referenceSet containsKey:key3]); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTReferenceSetTests.mm b/Firestore/Example/Tests/Local/FSTReferenceSetTests.mm new file mode 100644 index 0000000..802117a --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTReferenceSetTests.mm @@ -0,0 +1,85 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTReferenceSet.h" + +#import + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTReferenceSetTests : XCTestCase +@end + +@implementation FSTReferenceSetTests + +- (void)testAddOrRemoveReferences { + FSTDocumentKey *key = FSTTestDocKey(@"foo/bar"); + + FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; + XCTAssertTrue([referenceSet isEmpty]); + XCTAssertFalse([referenceSet containsKey:key]); + + [referenceSet addReferenceToKey:key forID:1]; + XCTAssertTrue([referenceSet containsKey:key]); + XCTAssertFalse([referenceSet isEmpty]); + + [referenceSet addReferenceToKey:key forID:2]; + XCTAssertTrue([referenceSet containsKey:key]); + + [referenceSet removeReferenceToKey:key forID:1]; + XCTAssertTrue([referenceSet containsKey:key]); + + [referenceSet removeReferenceToKey:key forID:3]; + XCTAssertTrue([referenceSet containsKey:key]); + + [referenceSet removeReferenceToKey:key forID:2]; + XCTAssertFalse([referenceSet containsKey:key]); + XCTAssertTrue([referenceSet isEmpty]); +} + +- (void)testRemoveAllReferencesForTargetID { + FSTDocumentKey *key1 = FSTTestDocKey(@"foo/bar"); + FSTDocumentKey *key2 = FSTTestDocKey(@"foo/baz"); + FSTDocumentKey *key3 = FSTTestDocKey(@"foo/blah"); + FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; + + [referenceSet addReferenceToKey:key1 forID:1]; + [referenceSet addReferenceToKey:key2 forID:1]; + [referenceSet addReferenceToKey:key3 forID:2]; + XCTAssertFalse([referenceSet isEmpty]); + XCTAssertTrue([referenceSet containsKey:key1]); + XCTAssertTrue([referenceSet containsKey:key2]); + XCTAssertTrue([referenceSet containsKey:key3]); + + [referenceSet removeReferencesForID:1]; + XCTAssertFalse([referenceSet isEmpty]); + XCTAssertFalse([referenceSet containsKey:key1]); + XCTAssertFalse([referenceSet containsKey:key2]); + XCTAssertTrue([referenceSet containsKey:key3]); + + [referenceSet removeReferencesForID:2]; + XCTAssertTrue([referenceSet isEmpty]); + XCTAssertFalse([referenceSet containsKey:key1]); + XCTAssertFalse([referenceSet containsKey:key2]); + XCTAssertFalse([referenceSet containsKey:key3]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m deleted file mode 100644 index d240604..0000000 --- a/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h" - -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Local/FSTPersistence.h" -#import "Firestore/Source/Local/FSTWriteGroup.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -static NSString *const kDocPath = @"a/b"; -static NSString *const kLongDocPath = @"a/b/c/d/e/f"; -static const int kVersion = 42; - -@implementation FSTRemoteDocumentCacheTests { - NSDictionary *_kDocData; -} - -- (void)setUp { - [super setUp]; - - // essentially a constant, but can't be a compile-time one. - _kDocData = @{ @"a" : @1, @"b" : @2 }; -} - -- (void)testReadDocumentNotInCache { - if (!self.remoteDocumentCache) return; - - XCTAssertNil([self readEntryAtPath:kDocPath]); -} - -// Helper for next two tests. -- (void)setAndReadADocumentAtPath:(NSString *)path { - FSTDocument *written = [self setTestDocumentAtPath:path]; - FSTMaybeDocument *read = [self readEntryAtPath:path]; - XCTAssertEqualObjects(read, written); -} - -- (void)testSetAndReadADocument { - if (!self.remoteDocumentCache) return; - - [self setAndReadADocumentAtPath:kDocPath]; -} - -- (void)testSetAndReadADocumentAtDeepPath { - if (!self.remoteDocumentCache) return; - - [self setAndReadADocumentAtPath:kLongDocPath]; -} - -- (void)testSetAndReadDeletedDocument { - if (!self.remoteDocumentCache) return; - - FSTDeletedDocument *deletedDoc = FSTTestDeletedDoc(kDocPath, kVersion); - [self addEntry:deletedDoc]; - - XCTAssertEqualObjects([self readEntryAtPath:kDocPath], deletedDoc); -} - -- (void)testSetDocumentToNewValue { - if (!self.remoteDocumentCache) return; - - [self setTestDocumentAtPath:kDocPath]; - FSTDocument *newDoc = FSTTestDoc(kDocPath, kVersion, @{ @"data" : @2 }, NO); - [self addEntry:newDoc]; - XCTAssertEqualObjects([self readEntryAtPath:kDocPath], newDoc); -} - -- (void)testRemoveDocument { - if (!self.remoteDocumentCache) return; - - [self setTestDocumentAtPath:kDocPath]; - [self removeEntryAtPath:kDocPath]; - - XCTAssertNil([self readEntryAtPath:kDocPath]); -} - -- (void)testRemoveNonExistentDocument { - if (!self.remoteDocumentCache) return; - - // no-op, but make sure it doesn't throw. - XCTAssertNoThrow([self removeEntryAtPath:kDocPath]); -} - -// TODO(mikelehen): Write more elaborate tests once we have more elaborate implementations. -- (void)testDocumentsMatchingQuery { - if (!self.remoteDocumentCache) return; - - // TODO(rsgowman): This just verifies that we do a prefix scan against the - // query path. We'll need more tests once we add index support. - [self setTestDocumentAtPath:@"a/1"]; - [self setTestDocumentAtPath:@"b/1"]; - [self setTestDocumentAtPath:@"b/2"]; - [self setTestDocumentAtPath:@"c/1"]; - - FSTQuery *query = FSTTestQuery(@"b"); - FSTDocumentDictionary *results = [self.remoteDocumentCache documentsMatchingQuery:query]; - NSArray *expected = - @[ FSTTestDoc(@"b/1", kVersion, _kDocData, NO), FSTTestDoc(@"b/2", kVersion, _kDocData, NO) ]; - XCTAssertEqual([results count], [expected count]); - for (FSTDocument *doc in expected) { - XCTAssertEqualObjects([results objectForKey:doc.key], doc); - } -} - -#pragma mark - Helpers - -- (FSTDocument *)setTestDocumentAtPath:(NSString *)path { - FSTDocument *doc = FSTTestDoc(path, kVersion, _kDocData, NO); - [self addEntry:doc]; - return doc; -} - -- (void)addEntry:(FSTMaybeDocument *)maybeDoc { - FSTWriteGroup *group = [self.persistence startGroupWithAction:@"addEntry"]; - [self.remoteDocumentCache addEntry:maybeDoc group:group]; - [self.persistence commitGroup:group]; -} - -- (FSTMaybeDocument *_Nullable)readEntryAtPath:(NSString *)path { - return [self.remoteDocumentCache entryForKey:FSTTestDocKey(path)]; -} - -- (void)removeEntryAtPath:(NSString *)path { - FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeEntryAtPath"]; - [self.remoteDocumentCache removeEntryForKey:FSTTestDocKey(path) group:group]; - [self.persistence commitGroup:group]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.mm b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.mm new file mode 100644 index 0000000..d240604 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.mm @@ -0,0 +1,151 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h" + +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Local/FSTPersistence.h" +#import "Firestore/Source/Local/FSTWriteGroup.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const kDocPath = @"a/b"; +static NSString *const kLongDocPath = @"a/b/c/d/e/f"; +static const int kVersion = 42; + +@implementation FSTRemoteDocumentCacheTests { + NSDictionary *_kDocData; +} + +- (void)setUp { + [super setUp]; + + // essentially a constant, but can't be a compile-time one. + _kDocData = @{ @"a" : @1, @"b" : @2 }; +} + +- (void)testReadDocumentNotInCache { + if (!self.remoteDocumentCache) return; + + XCTAssertNil([self readEntryAtPath:kDocPath]); +} + +// Helper for next two tests. +- (void)setAndReadADocumentAtPath:(NSString *)path { + FSTDocument *written = [self setTestDocumentAtPath:path]; + FSTMaybeDocument *read = [self readEntryAtPath:path]; + XCTAssertEqualObjects(read, written); +} + +- (void)testSetAndReadADocument { + if (!self.remoteDocumentCache) return; + + [self setAndReadADocumentAtPath:kDocPath]; +} + +- (void)testSetAndReadADocumentAtDeepPath { + if (!self.remoteDocumentCache) return; + + [self setAndReadADocumentAtPath:kLongDocPath]; +} + +- (void)testSetAndReadDeletedDocument { + if (!self.remoteDocumentCache) return; + + FSTDeletedDocument *deletedDoc = FSTTestDeletedDoc(kDocPath, kVersion); + [self addEntry:deletedDoc]; + + XCTAssertEqualObjects([self readEntryAtPath:kDocPath], deletedDoc); +} + +- (void)testSetDocumentToNewValue { + if (!self.remoteDocumentCache) return; + + [self setTestDocumentAtPath:kDocPath]; + FSTDocument *newDoc = FSTTestDoc(kDocPath, kVersion, @{ @"data" : @2 }, NO); + [self addEntry:newDoc]; + XCTAssertEqualObjects([self readEntryAtPath:kDocPath], newDoc); +} + +- (void)testRemoveDocument { + if (!self.remoteDocumentCache) return; + + [self setTestDocumentAtPath:kDocPath]; + [self removeEntryAtPath:kDocPath]; + + XCTAssertNil([self readEntryAtPath:kDocPath]); +} + +- (void)testRemoveNonExistentDocument { + if (!self.remoteDocumentCache) return; + + // no-op, but make sure it doesn't throw. + XCTAssertNoThrow([self removeEntryAtPath:kDocPath]); +} + +// TODO(mikelehen): Write more elaborate tests once we have more elaborate implementations. +- (void)testDocumentsMatchingQuery { + if (!self.remoteDocumentCache) return; + + // TODO(rsgowman): This just verifies that we do a prefix scan against the + // query path. We'll need more tests once we add index support. + [self setTestDocumentAtPath:@"a/1"]; + [self setTestDocumentAtPath:@"b/1"]; + [self setTestDocumentAtPath:@"b/2"]; + [self setTestDocumentAtPath:@"c/1"]; + + FSTQuery *query = FSTTestQuery(@"b"); + FSTDocumentDictionary *results = [self.remoteDocumentCache documentsMatchingQuery:query]; + NSArray *expected = + @[ FSTTestDoc(@"b/1", kVersion, _kDocData, NO), FSTTestDoc(@"b/2", kVersion, _kDocData, NO) ]; + XCTAssertEqual([results count], [expected count]); + for (FSTDocument *doc in expected) { + XCTAssertEqualObjects([results objectForKey:doc.key], doc); + } +} + +#pragma mark - Helpers + +- (FSTDocument *)setTestDocumentAtPath:(NSString *)path { + FSTDocument *doc = FSTTestDoc(path, kVersion, _kDocData, NO); + [self addEntry:doc]; + return doc; +} + +- (void)addEntry:(FSTMaybeDocument *)maybeDoc { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"addEntry"]; + [self.remoteDocumentCache addEntry:maybeDoc group:group]; + [self.persistence commitGroup:group]; +} + +- (FSTMaybeDocument *_Nullable)readEntryAtPath:(NSString *)path { + return [self.remoteDocumentCache entryForKey:FSTTestDocKey(path)]; +} + +- (void)removeEntryAtPath:(NSString *)path { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeEntryAtPath"]; + [self.remoteDocumentCache removeEntryForKey:FSTTestDocKey(path) group:group]; + [self.persistence commitGroup:group]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.m b/Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.m deleted file mode 100644 index 1970779..0000000 --- a/Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.m +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.h" - -#import - -#import "Firestore/Source/Local/FSTLevelDB.h" -#import "Firestore/Source/Local/FSTRemoteDocumentCache.h" -#import "Firestore/Source/Model/FSTDocument.h" - -#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTRemoteDocumentChangeBufferTests : XCTestCase -@end - -@implementation FSTRemoteDocumentChangeBufferTests { - FSTLevelDB *_db; - id _remoteDocumentCache; - FSTRemoteDocumentChangeBuffer *_remoteDocumentBuffer; - - FSTMaybeDocument *_kInitialADoc; - FSTMaybeDocument *_kInitialBDoc; -} - -- (void)setUp { - [super setUp]; - - _db = [FSTPersistenceTestHelpers levelDBPersistence]; - _remoteDocumentCache = [_db remoteDocumentCache]; - - // Add a couple initial items to the cache. - FSTWriteGroup *group = [_db startGroupWithAction:@"Add initial docs."]; - _kInitialADoc = FSTTestDoc(@"coll/a", 42, @{@"test" : @"data"}, NO); - [_remoteDocumentCache addEntry:_kInitialADoc group:group]; - - _kInitialBDoc = - [FSTDeletedDocument documentWithKey:FSTTestDocKey(@"coll/b") version:FSTTestVersion(314)]; - [_remoteDocumentCache addEntry:_kInitialBDoc group:group]; - [_db commitGroup:group]; - - _remoteDocumentBuffer = - [FSTRemoteDocumentChangeBuffer changeBufferWithCache:_remoteDocumentCache]; -} - -- (void)tearDown { - _remoteDocumentBuffer = nil; - _remoteDocumentCache = nil; - _db = nil; - - [super tearDown]; -} - -- (void)testReadUnchangedEntry { - XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")], - _kInitialADoc); -} - -- (void)testAddEntryAndReadItBack { - FSTMaybeDocument *newADoc = FSTTestDoc(@"coll/a", 43, @{@"new" : @"data"}, NO); - [_remoteDocumentBuffer addEntry:newADoc]; - XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")], newADoc); - - // B should still be unchanged. - XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/b")], - _kInitialBDoc); -} - -- (void)testApplyChanges { - FSTMaybeDocument *newADoc = FSTTestDoc(@"coll/a", 43, @{@"new" : @"data"}, NO); - [_remoteDocumentBuffer addEntry:newADoc]; - XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")], newADoc); - - // Reading directly against the cache should still yield the old result. - XCTAssertEqualObjects([_remoteDocumentCache entryForKey:FSTTestDocKey(@"coll/a")], _kInitialADoc); - - FSTWriteGroup *group = [_db startGroupWithAction:@"Apply changes"]; - [_remoteDocumentBuffer applyToWriteGroup:group]; - [_db commitGroup:group]; - - // Reading against the cache should now yield the new result. - XCTAssertEqualObjects([_remoteDocumentCache entryForKey:FSTTestDocKey(@"coll/a")], newADoc); -} - -- (void)testMethodsThrowAfterApply { - FSTWriteGroup *group = [_db startGroupWithAction:@"Apply changes"]; - [_remoteDocumentBuffer applyToWriteGroup:group]; - [_db commitGroup:group]; - - XCTAssertThrows([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")]); - XCTAssertThrows([_remoteDocumentBuffer addEntry:_kInitialADoc]); - XCTAssertThrows([_remoteDocumentBuffer applyToWriteGroup:group]); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.mm b/Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.mm new file mode 100644 index 0000000..1970779 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.mm @@ -0,0 +1,113 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.h" + +#import + +#import "Firestore/Source/Local/FSTLevelDB.h" +#import "Firestore/Source/Local/FSTRemoteDocumentCache.h" +#import "Firestore/Source/Model/FSTDocument.h" + +#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTRemoteDocumentChangeBufferTests : XCTestCase +@end + +@implementation FSTRemoteDocumentChangeBufferTests { + FSTLevelDB *_db; + id _remoteDocumentCache; + FSTRemoteDocumentChangeBuffer *_remoteDocumentBuffer; + + FSTMaybeDocument *_kInitialADoc; + FSTMaybeDocument *_kInitialBDoc; +} + +- (void)setUp { + [super setUp]; + + _db = [FSTPersistenceTestHelpers levelDBPersistence]; + _remoteDocumentCache = [_db remoteDocumentCache]; + + // Add a couple initial items to the cache. + FSTWriteGroup *group = [_db startGroupWithAction:@"Add initial docs."]; + _kInitialADoc = FSTTestDoc(@"coll/a", 42, @{@"test" : @"data"}, NO); + [_remoteDocumentCache addEntry:_kInitialADoc group:group]; + + _kInitialBDoc = + [FSTDeletedDocument documentWithKey:FSTTestDocKey(@"coll/b") version:FSTTestVersion(314)]; + [_remoteDocumentCache addEntry:_kInitialBDoc group:group]; + [_db commitGroup:group]; + + _remoteDocumentBuffer = + [FSTRemoteDocumentChangeBuffer changeBufferWithCache:_remoteDocumentCache]; +} + +- (void)tearDown { + _remoteDocumentBuffer = nil; + _remoteDocumentCache = nil; + _db = nil; + + [super tearDown]; +} + +- (void)testReadUnchangedEntry { + XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")], + _kInitialADoc); +} + +- (void)testAddEntryAndReadItBack { + FSTMaybeDocument *newADoc = FSTTestDoc(@"coll/a", 43, @{@"new" : @"data"}, NO); + [_remoteDocumentBuffer addEntry:newADoc]; + XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")], newADoc); + + // B should still be unchanged. + XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/b")], + _kInitialBDoc); +} + +- (void)testApplyChanges { + FSTMaybeDocument *newADoc = FSTTestDoc(@"coll/a", 43, @{@"new" : @"data"}, NO); + [_remoteDocumentBuffer addEntry:newADoc]; + XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")], newADoc); + + // Reading directly against the cache should still yield the old result. + XCTAssertEqualObjects([_remoteDocumentCache entryForKey:FSTTestDocKey(@"coll/a")], _kInitialADoc); + + FSTWriteGroup *group = [_db startGroupWithAction:@"Apply changes"]; + [_remoteDocumentBuffer applyToWriteGroup:group]; + [_db commitGroup:group]; + + // Reading against the cache should now yield the new result. + XCTAssertEqualObjects([_remoteDocumentCache entryForKey:FSTTestDocKey(@"coll/a")], newADoc); +} + +- (void)testMethodsThrowAfterApply { + FSTWriteGroup *group = [_db startGroupWithAction:@"Apply changes"]; + [_remoteDocumentBuffer applyToWriteGroup:group]; + [_db commitGroup:group]; + + XCTAssertThrows([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")]); + XCTAssertThrows([_remoteDocumentBuffer addEntry:_kInitialADoc]); + XCTAssertThrows([_remoteDocumentBuffer applyToWriteGroup:group]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTDatabaseIDTests.m b/Firestore/Example/Tests/Model/FSTDatabaseIDTests.m deleted file mode 100644 index cb1b19d..0000000 --- a/Firestore/Example/Tests/Model/FSTDatabaseIDTests.m +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Model/FSTDatabaseID.h" - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTDatabaseIDTests : XCTestCase -@end - -@implementation FSTDatabaseIDTests - -- (void)testConstructor { - FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; - XCTAssertEqualObjects(databaseID.projectID, @"p"); - XCTAssertEqualObjects(databaseID.databaseID, @"d"); - XCTAssertFalse([databaseID isDefaultDatabase]); -} - -- (void)testDefaultDatabase { - FSTDatabaseID *databaseID = - [FSTDatabaseID databaseIDWithProject:@"p" database:kDefaultDatabaseID]; - XCTAssertEqualObjects(databaseID.projectID, @"p"); - XCTAssertEqualObjects(databaseID.databaseID, @"(default)"); - XCTAssertTrue([databaseID isDefaultDatabase]); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTDatabaseIDTests.mm b/Firestore/Example/Tests/Model/FSTDatabaseIDTests.mm new file mode 100644 index 0000000..cb1b19d --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTDatabaseIDTests.mm @@ -0,0 +1,45 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Model/FSTDatabaseID.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDatabaseIDTests : XCTestCase +@end + +@implementation FSTDatabaseIDTests + +- (void)testConstructor { + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; + XCTAssertEqualObjects(databaseID.projectID, @"p"); + XCTAssertEqualObjects(databaseID.databaseID, @"d"); + XCTAssertFalse([databaseID isDefaultDatabase]); +} + +- (void)testDefaultDatabase { + FSTDatabaseID *databaseID = + [FSTDatabaseID databaseIDWithProject:@"p" database:kDefaultDatabaseID]; + XCTAssertEqualObjects(databaseID.projectID, @"p"); + XCTAssertEqualObjects(databaseID.databaseID, @"(default)"); + XCTAssertTrue([databaseID isDefaultDatabase]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTDocumentKeyTests.m b/Firestore/Example/Tests/Model/FSTDocumentKeyTests.m deleted file mode 100644 index d66ee73..0000000 --- a/Firestore/Example/Tests/Model/FSTDocumentKeyTests.m +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Model/FSTDocumentKey.h" - -#import - -#import "Firestore/Source/Model/FSTPath.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTDocumentKeyTests : XCTestCase -@end - -@implementation FSTDocumentKeyTests - -- (void)testConstructor { - FSTResourcePath *path = - [FSTResourcePath pathWithSegments:@[ @"rooms", @"firestore", @"messages", @"1" ]]; - FSTDocumentKey *key = [FSTDocumentKey keyWithPath:path]; - XCTAssertEqual(path, key.path); -} - -- (void)testComparison { - FSTDocumentKey *key1 = [FSTDocumentKey keyWithSegments:@[ @"a", @"b", @"c", @"d" ]]; - FSTDocumentKey *key2 = [FSTDocumentKey keyWithSegments:@[ @"a", @"b", @"c", @"d" ]]; - FSTDocumentKey *key3 = [FSTDocumentKey keyWithSegments:@[ @"x", @"y", @"z", @"w" ]]; - XCTAssertTrue([key1 isEqualToKey:key2]); - XCTAssertFalse([key1 isEqualToKey:key3]); - - FSTDocumentKey *empty = [FSTDocumentKey keyWithSegments:@[]]; - FSTDocumentKey *a = [FSTDocumentKey keyWithSegments:@[ @"a", @"a" ]]; - FSTDocumentKey *b = [FSTDocumentKey keyWithSegments:@[ @"b", @"b" ]]; - FSTDocumentKey *ab = [FSTDocumentKey keyWithSegments:@[ @"a", @"a", @"b", @"b" ]]; - - XCTAssertEqual(NSOrderedAscending, [empty compare:a]); - XCTAssertEqual(NSOrderedAscending, [a compare:b]); - XCTAssertEqual(NSOrderedAscending, [a compare:ab]); - - XCTAssertEqual(NSOrderedDescending, [a compare:empty]); - XCTAssertEqual(NSOrderedDescending, [b compare:a]); - XCTAssertEqual(NSOrderedDescending, [ab compare:a]); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTDocumentKeyTests.mm b/Firestore/Example/Tests/Model/FSTDocumentKeyTests.mm new file mode 100644 index 0000000..d66ee73 --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTDocumentKeyTests.mm @@ -0,0 +1,60 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Model/FSTDocumentKey.h" + +#import + +#import "Firestore/Source/Model/FSTPath.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDocumentKeyTests : XCTestCase +@end + +@implementation FSTDocumentKeyTests + +- (void)testConstructor { + FSTResourcePath *path = + [FSTResourcePath pathWithSegments:@[ @"rooms", @"firestore", @"messages", @"1" ]]; + FSTDocumentKey *key = [FSTDocumentKey keyWithPath:path]; + XCTAssertEqual(path, key.path); +} + +- (void)testComparison { + FSTDocumentKey *key1 = [FSTDocumentKey keyWithSegments:@[ @"a", @"b", @"c", @"d" ]]; + FSTDocumentKey *key2 = [FSTDocumentKey keyWithSegments:@[ @"a", @"b", @"c", @"d" ]]; + FSTDocumentKey *key3 = [FSTDocumentKey keyWithSegments:@[ @"x", @"y", @"z", @"w" ]]; + XCTAssertTrue([key1 isEqualToKey:key2]); + XCTAssertFalse([key1 isEqualToKey:key3]); + + FSTDocumentKey *empty = [FSTDocumentKey keyWithSegments:@[]]; + FSTDocumentKey *a = [FSTDocumentKey keyWithSegments:@[ @"a", @"a" ]]; + FSTDocumentKey *b = [FSTDocumentKey keyWithSegments:@[ @"b", @"b" ]]; + FSTDocumentKey *ab = [FSTDocumentKey keyWithSegments:@[ @"a", @"a", @"b", @"b" ]]; + + XCTAssertEqual(NSOrderedAscending, [empty compare:a]); + XCTAssertEqual(NSOrderedAscending, [a compare:b]); + XCTAssertEqual(NSOrderedAscending, [a compare:ab]); + + XCTAssertEqual(NSOrderedDescending, [a compare:empty]); + XCTAssertEqual(NSOrderedDescending, [b compare:a]); + XCTAssertEqual(NSOrderedDescending, [ab compare:a]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTDocumentSetTests.m b/Firestore/Example/Tests/Model/FSTDocumentSetTests.m deleted file mode 100644 index bf6cd21..0000000 --- a/Firestore/Example/Tests/Model/FSTDocumentSetTests.m +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Model/FSTDocumentSet.h" - -#import - -#import "Firestore/Source/Model/FSTDocument.h" - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTDocumentSetTests : XCTestCase -@end - -@implementation FSTDocumentSetTests { - NSComparator _comp; - FSTDocument *_doc1; - FSTDocument *_doc2; - FSTDocument *_doc3; -} - -- (void)setUp { - [super setUp]; - - _comp = FSTTestDocComparator(@"sort"); - _doc1 = FSTTestDoc(@"docs/1", 0, @{ @"sort" : @2 }, NO); - _doc2 = FSTTestDoc(@"docs/2", 0, @{ @"sort" : @3 }, NO); - _doc3 = FSTTestDoc(@"docs/3", 0, @{ @"sort" : @1 }, NO); -} - -- (void)testCount { - XCTAssertEqual([FSTTestDocSet(_comp, @[]) count], 0); - XCTAssertEqual([FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]) count], 3); -} - -- (void)testHasKey { - FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2 ]); - - XCTAssertTrue([set containsKey:_doc1.key]); - XCTAssertTrue([set containsKey:_doc2.key]); - XCTAssertFalse([set containsKey:_doc3.key]); -} - -- (void)testDocumentForKey { - FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2 ]); - - XCTAssertEqualObjects([set documentForKey:_doc1.key], _doc1); - XCTAssertEqualObjects([set documentForKey:_doc2.key], _doc2); - XCTAssertNil([set documentForKey:_doc3.key]); -} - -- (void)testFirstAndLastDocument { - FSTDocumentSet *set = FSTTestDocSet(_comp, @[]); - XCTAssertNil([set firstDocument]); - XCTAssertNil([set lastDocument]); - - set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); - XCTAssertEqualObjects([set firstDocument], _doc3); - XCTAssertEqualObjects([set lastDocument], _doc2); -} - -- (void)testKeepsDocumentsInTheRightOrder { - FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); - XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, _doc2 ])); -} - -- (void)testPredecessorDocumentForKey { - FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); - - XCTAssertNil([set predecessorDocumentForKey:_doc3.key]); - XCTAssertEqualObjects([set predecessorDocumentForKey:_doc1.key], _doc3); - XCTAssertEqualObjects([set predecessorDocumentForKey:_doc2.key], _doc1); -} - -- (void)testDeletes { - FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); - - FSTDocumentSet *setWithoutDoc1 = [set documentSetByRemovingKey:_doc1.key]; - XCTAssertEqualObjects([[setWithoutDoc1 documentEnumerator] allObjects], (@[ _doc3, _doc2 ])); - XCTAssertEqual([setWithoutDoc1 count], 2); - - // Original remains unchanged - XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, _doc2 ])); - - FSTDocumentSet *setWithoutDoc3 = [setWithoutDoc1 documentSetByRemovingKey:_doc3.key]; - XCTAssertEqualObjects([[setWithoutDoc3 documentEnumerator] allObjects], (@[ _doc2 ])); - XCTAssertEqual([setWithoutDoc3 count], 1); -} - -- (void)testUpdates { - FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); - - FSTDocument *doc2Prime = FSTTestDoc(@"docs/2", 0, @{ @"sort" : @9 }, NO); - - set = [set documentSetByAddingDocument:doc2Prime]; - XCTAssertEqual([set count], 3); - XCTAssertEqualObjects([set documentForKey:doc2Prime.key], doc2Prime); - XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, doc2Prime ])); -} - -- (void)testAddsDocsWithEqualComparisonValues { - FSTDocument *doc4 = FSTTestDoc(@"docs/4", 0, @{ @"sort" : @2 }, NO); - - FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, doc4 ]); - XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc1, doc4 ])); -} - -- (void)testIsEqual { - FSTDocumentSet *set1 = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2, _doc3 ]); - FSTDocumentSet *set2 = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2, _doc3 ]); - XCTAssertEqualObjects(set1, set1); - XCTAssertEqualObjects(set1, set2); - XCTAssertNotEqualObjects(set1, nil); - - FSTDocumentSet *sortedSet1 = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); - FSTDocumentSet *sortedSet2 = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); - XCTAssertEqualObjects(sortedSet1, sortedSet1); - XCTAssertEqualObjects(sortedSet1, sortedSet2); - XCTAssertNotEqualObjects(sortedSet1, nil); - - FSTDocumentSet *shortSet = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2 ]); - XCTAssertNotEqualObjects(set1, shortSet); - XCTAssertNotEqualObjects(set1, sortedSet1); -} -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTDocumentSetTests.mm b/Firestore/Example/Tests/Model/FSTDocumentSetTests.mm new file mode 100644 index 0000000..bf6cd21 --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTDocumentSetTests.mm @@ -0,0 +1,142 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Model/FSTDocumentSet.h" + +#import + +#import "Firestore/Source/Model/FSTDocument.h" + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDocumentSetTests : XCTestCase +@end + +@implementation FSTDocumentSetTests { + NSComparator _comp; + FSTDocument *_doc1; + FSTDocument *_doc2; + FSTDocument *_doc3; +} + +- (void)setUp { + [super setUp]; + + _comp = FSTTestDocComparator(@"sort"); + _doc1 = FSTTestDoc(@"docs/1", 0, @{ @"sort" : @2 }, NO); + _doc2 = FSTTestDoc(@"docs/2", 0, @{ @"sort" : @3 }, NO); + _doc3 = FSTTestDoc(@"docs/3", 0, @{ @"sort" : @1 }, NO); +} + +- (void)testCount { + XCTAssertEqual([FSTTestDocSet(_comp, @[]) count], 0); + XCTAssertEqual([FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]) count], 3); +} + +- (void)testHasKey { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2 ]); + + XCTAssertTrue([set containsKey:_doc1.key]); + XCTAssertTrue([set containsKey:_doc2.key]); + XCTAssertFalse([set containsKey:_doc3.key]); +} + +- (void)testDocumentForKey { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2 ]); + + XCTAssertEqualObjects([set documentForKey:_doc1.key], _doc1); + XCTAssertEqualObjects([set documentForKey:_doc2.key], _doc2); + XCTAssertNil([set documentForKey:_doc3.key]); +} + +- (void)testFirstAndLastDocument { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[]); + XCTAssertNil([set firstDocument]); + XCTAssertNil([set lastDocument]); + + set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + XCTAssertEqualObjects([set firstDocument], _doc3); + XCTAssertEqualObjects([set lastDocument], _doc2); +} + +- (void)testKeepsDocumentsInTheRightOrder { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, _doc2 ])); +} + +- (void)testPredecessorDocumentForKey { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + + XCTAssertNil([set predecessorDocumentForKey:_doc3.key]); + XCTAssertEqualObjects([set predecessorDocumentForKey:_doc1.key], _doc3); + XCTAssertEqualObjects([set predecessorDocumentForKey:_doc2.key], _doc1); +} + +- (void)testDeletes { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + + FSTDocumentSet *setWithoutDoc1 = [set documentSetByRemovingKey:_doc1.key]; + XCTAssertEqualObjects([[setWithoutDoc1 documentEnumerator] allObjects], (@[ _doc3, _doc2 ])); + XCTAssertEqual([setWithoutDoc1 count], 2); + + // Original remains unchanged + XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, _doc2 ])); + + FSTDocumentSet *setWithoutDoc3 = [setWithoutDoc1 documentSetByRemovingKey:_doc3.key]; + XCTAssertEqualObjects([[setWithoutDoc3 documentEnumerator] allObjects], (@[ _doc2 ])); + XCTAssertEqual([setWithoutDoc3 count], 1); +} + +- (void)testUpdates { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + + FSTDocument *doc2Prime = FSTTestDoc(@"docs/2", 0, @{ @"sort" : @9 }, NO); + + set = [set documentSetByAddingDocument:doc2Prime]; + XCTAssertEqual([set count], 3); + XCTAssertEqualObjects([set documentForKey:doc2Prime.key], doc2Prime); + XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, doc2Prime ])); +} + +- (void)testAddsDocsWithEqualComparisonValues { + FSTDocument *doc4 = FSTTestDoc(@"docs/4", 0, @{ @"sort" : @2 }, NO); + + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, doc4 ]); + XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc1, doc4 ])); +} + +- (void)testIsEqual { + FSTDocumentSet *set1 = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2, _doc3 ]); + FSTDocumentSet *set2 = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2, _doc3 ]); + XCTAssertEqualObjects(set1, set1); + XCTAssertEqualObjects(set1, set2); + XCTAssertNotEqualObjects(set1, nil); + + FSTDocumentSet *sortedSet1 = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + FSTDocumentSet *sortedSet2 = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + XCTAssertEqualObjects(sortedSet1, sortedSet1); + XCTAssertEqualObjects(sortedSet1, sortedSet2); + XCTAssertNotEqualObjects(sortedSet1, nil); + + FSTDocumentSet *shortSet = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2 ]); + XCTAssertNotEqualObjects(set1, shortSet); + XCTAssertNotEqualObjects(set1, sortedSet1); +} +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTDocumentTests.m b/Firestore/Example/Tests/Model/FSTDocumentTests.m deleted file mode 100644 index 59f526d..0000000 --- a/Firestore/Example/Tests/Model/FSTDocumentTests.m +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Model/FSTDocument.h" - -#import - -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTPath.h" - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTDocumentTests : XCTestCase -@end - -@implementation FSTDocumentTests - -- (void)testConstructor { - FSTDocumentKey *key = FSTTestDocKey(@"messages/first"); - FSTSnapshotVersion *version = FSTTestVersion(1); - FSTObjectValue *data = FSTTestObjectValue(@{ @"a" : @1 }); - FSTDocument *doc = - [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO]; - - XCTAssertEqualObjects(doc.key, FSTTestDocKey(@"messages/first")); - XCTAssertEqualObjects(doc.version, version); - XCTAssertEqualObjects(doc.data, data); - XCTAssertEqual(doc.hasLocalMutations, NO); -} - -- (void)testExtractsFields { - FSTDocumentKey *key = FSTTestDocKey(@"rooms/eros"); - FSTSnapshotVersion *version = FSTTestVersion(1); - FSTObjectValue *data = FSTTestObjectValue(@{ - @"desc" : @"Discuss all the project related stuff", - @"owner" : @{@"name" : @"Jonny", @"title" : @"scallywag"} - }); - FSTDocument *doc = - [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO]; - - XCTAssertEqualObjects([doc fieldForPath:FSTTestFieldPath(@"desc")], - [FSTStringValue stringValue:@"Discuss all the project related stuff"]); - XCTAssertEqualObjects([doc fieldForPath:FSTTestFieldPath(@"owner.title")], - [FSTStringValue stringValue:@"scallywag"]); -} - -- (void)testIsEqual { - XCTAssertEqualObjects(FSTTestDoc(@"messages/first", 1, - @{ @"a" : @1 }, NO), - FSTTestDoc(@"messages/first", 1, - @{ @"a" : @1 }, NO)); - XCTAssertNotEqualObjects(FSTTestDoc(@"messages/first", 1, - @{ @"a" : @1 }, NO), - FSTTestDoc(@"messages/first", 1, - @{ @"b" : @1 }, NO)); - XCTAssertNotEqualObjects(FSTTestDoc(@"messages/first", 1, - @{ @"a" : @1 }, NO), - FSTTestDoc(@"messages/second", 1, - @{ @"b" : @1 }, NO)); - XCTAssertNotEqualObjects(FSTTestDoc(@"messages/first", 1, - @{ @"a" : @1 }, NO), - FSTTestDoc(@"messages/first", 2, - @{ @"a" : @1 }, NO)); - XCTAssertNotEqualObjects(FSTTestDoc(@"messages/first", 1, - @{ @"a" : @1 }, NO), - FSTTestDoc(@"messages/first", 1, - @{ @"a" : @1 }, YES)); - - XCTAssertEqualObjects(FSTTestDoc(@"messages/first", 1, - @{ @"a" : @1 }, YES), - FSTTestDoc(@"messages/first", 1, - @{ @"a" : @1 }, 5)); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTDocumentTests.mm b/Firestore/Example/Tests/Model/FSTDocumentTests.mm new file mode 100644 index 0000000..59f526d --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTDocumentTests.mm @@ -0,0 +1,94 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Model/FSTDocument.h" + +#import + +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTPath.h" + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDocumentTests : XCTestCase +@end + +@implementation FSTDocumentTests + +- (void)testConstructor { + FSTDocumentKey *key = FSTTestDocKey(@"messages/first"); + FSTSnapshotVersion *version = FSTTestVersion(1); + FSTObjectValue *data = FSTTestObjectValue(@{ @"a" : @1 }); + FSTDocument *doc = + [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO]; + + XCTAssertEqualObjects(doc.key, FSTTestDocKey(@"messages/first")); + XCTAssertEqualObjects(doc.version, version); + XCTAssertEqualObjects(doc.data, data); + XCTAssertEqual(doc.hasLocalMutations, NO); +} + +- (void)testExtractsFields { + FSTDocumentKey *key = FSTTestDocKey(@"rooms/eros"); + FSTSnapshotVersion *version = FSTTestVersion(1); + FSTObjectValue *data = FSTTestObjectValue(@{ + @"desc" : @"Discuss all the project related stuff", + @"owner" : @{@"name" : @"Jonny", @"title" : @"scallywag"} + }); + FSTDocument *doc = + [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO]; + + XCTAssertEqualObjects([doc fieldForPath:FSTTestFieldPath(@"desc")], + [FSTStringValue stringValue:@"Discuss all the project related stuff"]); + XCTAssertEqualObjects([doc fieldForPath:FSTTestFieldPath(@"owner.title")], + [FSTStringValue stringValue:@"scallywag"]); +} + +- (void)testIsEqual { + XCTAssertEqualObjects(FSTTestDoc(@"messages/first", 1, + @{ @"a" : @1 }, NO), + FSTTestDoc(@"messages/first", 1, + @{ @"a" : @1 }, NO)); + XCTAssertNotEqualObjects(FSTTestDoc(@"messages/first", 1, + @{ @"a" : @1 }, NO), + FSTTestDoc(@"messages/first", 1, + @{ @"b" : @1 }, NO)); + XCTAssertNotEqualObjects(FSTTestDoc(@"messages/first", 1, + @{ @"a" : @1 }, NO), + FSTTestDoc(@"messages/second", 1, + @{ @"b" : @1 }, NO)); + XCTAssertNotEqualObjects(FSTTestDoc(@"messages/first", 1, + @{ @"a" : @1 }, NO), + FSTTestDoc(@"messages/first", 2, + @{ @"a" : @1 }, NO)); + XCTAssertNotEqualObjects(FSTTestDoc(@"messages/first", 1, + @{ @"a" : @1 }, NO), + FSTTestDoc(@"messages/first", 1, + @{ @"a" : @1 }, YES)); + + XCTAssertEqualObjects(FSTTestDoc(@"messages/first", 1, + @{ @"a" : @1 }, YES), + FSTTestDoc(@"messages/first", 1, + @{ @"a" : @1 }, 5)); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTFieldValueTests.m b/Firestore/Example/Tests/Model/FSTFieldValueTests.m deleted file mode 100644 index 56b885f..0000000 --- a/Firestore/Example/Tests/Model/FSTFieldValueTests.m +++ /dev/null @@ -1,582 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Model/FSTFieldValue.h" - -#import -#import - -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Core/FSTTimestamp.h" -#import "Firestore/Source/Model/FSTDatabaseID.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTPath.h" - -#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -/** Helper to wrap the values in a set of equality groups using FSTTestFieldValue(). */ -NSArray *FSTWrapGroups(NSArray *groups) { - NSMutableArray *wrapped = [NSMutableArray array]; - for (NSArray *group in groups) { - NSMutableArray *wrappedGroup = [NSMutableArray array]; - for (id value in group) { - FSTFieldValue *wrappedValue; - // Server Timestamp values can't be parsed directly, so we have a couple predefined sentinel - // strings that can be used instead. - if ([value isEqual:@"server-timestamp-1"]) { - wrappedValue = [FSTServerTimestampValue - serverTimestampValueWithLocalWriteTime:FSTTestTimestamp(2016, 5, 20, 10, 20, 0) - previousValue:nil]; - } else if ([value isEqual:@"server-timestamp-2"]) { - wrappedValue = [FSTServerTimestampValue - serverTimestampValueWithLocalWriteTime:FSTTestTimestamp(2016, 10, 21, 15, 32, 0) - previousValue:nil]; - } else if ([value isKindOfClass:[FSTDocumentKeyReference class]]) { - // We directly convert these here so that the databaseIDs can be different. - FSTDocumentKeyReference *reference = (FSTDocumentKeyReference *)value; - wrappedValue = - [FSTReferenceValue referenceValue:reference.key databaseID:reference.databaseID]; - } else { - wrappedValue = FSTTestFieldValue(value); - } - [wrappedGroup addObject:wrappedValue]; - } - [wrapped addObject:wrappedGroup]; - } - return wrapped; -} - -@interface FSTFieldValueTests : XCTestCase -@end - -@implementation FSTFieldValueTests { - NSDate *date1; - NSDate *date2; -} - -- (void)setUp { - [super setUp]; - // Create a couple date objects for use in tests. - date1 = FSTTestDate(2016, 5, 20, 10, 20, 0); - date2 = FSTTestDate(2016, 10, 21, 15, 32, 0); -} - -- (void)testWrapIntegers { - NSArray *values = @[ - @(INT_MIN), @(-1), @0, @1, @2, @(UCHAR_MAX), @(INT_MAX), // Standard integers - @(LONG_MIN), @(LONG_MAX), @(LLONG_MIN), @(LLONG_MAX) // Larger values - ]; - for (id value in values) { - FSTFieldValue *wrapped = FSTTestFieldValue(value); - XCTAssertEqualObjects([wrapped class], [FSTIntegerValue class]); - XCTAssertEqualObjects([wrapped value], @([value longLongValue])); - } -} - -- (void)testWrapsDoubles { - // Note that 0x1.0p-1074 is a hex floating point literal representing the minimum subnormal - // number: . - NSArray *values = @[ - @(-INFINITY), @(-DBL_MAX), @(LLONG_MIN * -1.0), @(-1.1), @(-0x1.0p-1074), @(-0.0), @(0.0), - @(0x1.0p-1074), @(DBL_MIN), @(1.1), @(LLONG_MAX * 1.0), @(DBL_MAX), @(INFINITY) - ]; - for (id value in values) { - FSTFieldValue *wrapped = FSTTestFieldValue(value); - XCTAssertEqualObjects([wrapped class], [FSTDoubleValue class]); - XCTAssertEqualObjects([wrapped value], value); - } -} - -- (void)testWrapsNilAndNSNull { - FSTNullValue *nullValue = [FSTNullValue nullValue]; - XCTAssertEqual(FSTTestFieldValue(nil), nullValue); - XCTAssertEqual(FSTTestFieldValue([NSNull null]), nullValue); - XCTAssertEqual([nullValue value], [NSNull null]); -} - -- (void)testWrapsBooleans { - NSArray *values = @[ @YES, @NO, [NSNumber numberWithChar:1], [NSNumber numberWithChar:0] ]; - for (id value in values) { - FSTFieldValue *wrapped = FSTTestFieldValue(value); - XCTAssertEqualObjects([wrapped class], [FSTBooleanValue class]); - XCTAssertEqualObjects([wrapped value], value); - } - - // Unsigned chars could conceivably be handled consistently with signed chars but on arm64 these - // end up being stored as signed shorts. - FSTFieldValue *wrapped = FSTTestFieldValue([NSNumber numberWithUnsignedChar:1]); - XCTAssertEqualObjects(wrapped, [FSTIntegerValue integerValue:1]); -} - -union DoubleBits { - double d; - uint64_t bits; -}; - -- (void)testNormalizesNaNs { - // NOTE: With v1beta1 query semantics, it's no longer as important that our NaN representation - // matches the backend, since all NaNs are defined to sort as equal, but we preserve the - // normalization and this test regardless for now. - - // We use a canonical NaN bit pattern that's common for both Java and Objective-C. Specifically: - // - sign: 0 - // - exponent: 11 bits, all 1 - // - significand: 52 bits, MSB=1, rest=0 - // - // This matches the Firestore backend which uses Java's Double.doubleToLongBits which is defined - // to normalize all NaNs to this value. - union DoubleBits canonical = {.bits = 0x7ff8000000000000ULL}; - - // IEEE 754 specifies that NaN isn't equal to itself. - XCTAssertTrue(isnan(canonical.d)); - XCTAssertEqual(canonical.bits, canonical.bits); - XCTAssertNotEqual(canonical.d, canonical.d); - - // All permutations of the 51 other non-MSB significand bits are also NaNs. - union DoubleBits alternate = {.bits = 0x7fff000000000000ULL}; - XCTAssertTrue(isnan(alternate.d)); - XCTAssertNotEqual(alternate.bits, canonical.bits); - XCTAssertNotEqual(alternate.d, canonical.d); - - // Even though at the C-level assignment preserves non-canonical NaNs, NSNumber normalizes all - // NaNs to single shared instance, kCFNumberNaN. That NaN has no public definition for its value - // but it happens to match what we need. - union DoubleBits normalized = {.d = [[NSNumber numberWithDouble:alternate.d] doubleValue]}; - XCTAssertEqual(normalized.bits, canonical.bits); - - // Ensure we get the same normalization behavior (currently implemented explicitly by checking - // for isnan() and then explicitly assigning NAN). - union DoubleBits result; - result.d = [[FSTDoubleValue doubleValue:canonical.d] internalValue]; - XCTAssertEqual(result.bits, canonical.bits); - - result.d = [[FSTDoubleValue doubleValue:alternate.d] internalValue]; - XCTAssertEqual(result.bits, canonical.bits); - - // A NaN that's canonical except it has the sign bit set (would be negative if signs mattered) - union DoubleBits negative = {.bits = 0xfff8000000000000ULL}; - result.d = [[FSTDoubleValue doubleValue:negative.d] internalValue]; - XCTAssertTrue(isnan(negative.d)); - XCTAssertEqual(result.bits, canonical.bits); - - // A signaling NaN with significand where MSB is 0, and some non-MSB bit is one. - union DoubleBits signaling = {.bits = 0xfff4000000000000ULL}; - XCTAssertTrue(isnan(signaling.d)); - result.d = [[FSTDoubleValue doubleValue:signaling.d] internalValue]; - XCTAssertEqual(result.bits, canonical.bits); -} - -- (void)testZeros { - // Floating point numbers have an explicit sign bit so it's possible to end up with negative - // zero as a distinct value from positive zero. - union DoubleBits zero = {.d = 0.0}; - union DoubleBits negativeZero = {.d = -0.0}; - - // IEEE 754 requires these two zeros to compare equal. - XCTAssertNotEqual(zero.bits, negativeZero.bits); - XCTAssertEqual(zero.d, negativeZero.d); - - // NSNumber preserves the negative zero value but compares equal according to IEEE 754. - union DoubleBits normalized = {.d = [[NSNumber numberWithDouble:negativeZero.d] doubleValue]}; - XCTAssertEqual(normalized.bits, negativeZero.bits); - XCTAssertEqualObjects([NSNumber numberWithDouble:0.0], [NSNumber numberWithDouble:-0.0]); - - // FSTDoubleValue preserves positive/negative zero - union DoubleBits result; - result.d = [[[FSTDoubleValue doubleValue:zero.d] value] doubleValue]; - XCTAssertEqual(result.bits, zero.bits); - result.d = [[[FSTDoubleValue doubleValue:negativeZero.d] value] doubleValue]; - XCTAssertEqual(result.bits, negativeZero.bits); - - // ... but compares positive/negative zero as unequal, compatibly with Firestore. - XCTAssertNotEqualObjects([FSTDoubleValue doubleValue:0.0], [FSTDoubleValue doubleValue:-0.0]); -} - -- (void)testWrapStrings { - NSArray *values = @[ @"", @"abc" ]; - for (id value in values) { - FSTFieldValue *wrapped = FSTTestFieldValue(value); - XCTAssertEqualObjects([wrapped class], [FSTStringValue class]); - XCTAssertEqualObjects([wrapped value], value); - } -} - -- (void)testWrapDates { - NSArray *values = @[ FSTTestDate(1900, 12, 1, 1, 20, 30), FSTTestDate(2017, 4, 24, 13, 20, 30) ]; - for (id value in values) { - FSTFieldValue *wrapped = FSTTestFieldValue(value); - XCTAssertEqualObjects([wrapped class], [FSTTimestampValue class]); - XCTAssertEqualObjects([wrapped value], value); - - XCTAssertEqualObjects(((FSTTimestampValue *)wrapped).internalValue, - [FSTTimestamp timestampWithDate:value]); - } -} - -- (void)testWrapGeoPoints { - NSArray *values = @[ FSTTestGeoPoint(1.24, 4.56), FSTTestGeoPoint(-20, 100) ]; - - for (id value in values) { - FSTFieldValue *wrapped = FSTTestFieldValue(value); - XCTAssertEqualObjects([wrapped class], [FSTGeoPointValue class]); - XCTAssertEqualObjects([wrapped value], value); - } -} - -- (void)testWrapBlobs { - NSArray *values = @[ FSTTestData(1, 2, 3), FSTTestData(1, 2) ]; - for (id value in values) { - FSTFieldValue *wrapped = FSTTestFieldValue(value); - XCTAssertEqualObjects([wrapped class], [FSTBlobValue class]); - XCTAssertEqualObjects([wrapped value], value); - } -} - -- (void)testWrapResourceNames { - NSArray *values = @[ - FSTTestRef(@"project", kDefaultDatabaseID, @"foo/bar"), - FSTTestRef(@"project", kDefaultDatabaseID, @"foo/baz") - ]; - for (FSTDocumentKeyReference *value in values) { - FSTFieldValue *wrapped = FSTTestFieldValue(value); - XCTAssertEqualObjects([wrapped class], [FSTReferenceValue class]); - XCTAssertEqualObjects([wrapped value], value.key); - XCTAssertEqualObjects(((FSTDatabaseID *)wrapped).databaseID, value.databaseID); - } -} - -- (void)testWrapsEmptyObjects { - XCTAssertEqualObjects(FSTTestFieldValue(@{}), [FSTObjectValue objectValue]); -} - -- (void)testWrapsSimpleObjects { - FSTObjectValue *actual = FSTTestObjectValue( - @{ @"a" : @"foo", - @"b" : @(1L), - @"c" : @YES, - @"d" : [NSNull null] }); - FSTObjectValue *expected = [[FSTObjectValue alloc] initWithDictionary:@{ - @"a" : [FSTStringValue stringValue:@"foo"], - @"b" : [FSTIntegerValue integerValue:1LL], - @"c" : [FSTBooleanValue trueValue], - @"d" : [FSTNullValue nullValue] - }]; - XCTAssertEqualObjects(actual, expected); -} - -- (void)testWrapsNestedObjects { - FSTObjectValue *actual = FSTTestObjectValue(@{ @"a" : @{@"b" : @{@"c" : @"foo"}, @"d" : @YES} }); - FSTObjectValue *expected = [[FSTObjectValue alloc] initWithDictionary:@{ - @"a" : [[FSTObjectValue alloc] initWithDictionary:@{ - @"b" : - [[FSTObjectValue alloc] initWithDictionary:@{@"c" : [FSTStringValue stringValue:@"foo"]}], - @"d" : [FSTBooleanValue booleanValue:YES] - }] - }]; - XCTAssertEqualObjects(actual, expected); -} - -- (void)testExtractsFields { - FSTObjectValue *obj = FSTTestObjectValue(@{ @"foo" : @{@"a" : @YES, @"b" : @"string"} }); - FSTAssertIsKindOfClass(obj, FSTObjectValue); - - FSTAssertIsKindOfClass([obj valueForPath:FSTTestFieldPath(@"foo")], FSTObjectValue); - XCTAssertEqualObjects([obj valueForPath:FSTTestFieldPath(@"foo.a")], [FSTBooleanValue trueValue]); - XCTAssertEqualObjects([obj valueForPath:FSTTestFieldPath(@"foo.b")], - [FSTStringValue stringValue:@"string"]); - - XCTAssertNil([obj valueForPath:FSTTestFieldPath(@"foo.a.b")]); - XCTAssertNil([obj valueForPath:FSTTestFieldPath(@"bar")]); - XCTAssertNil([obj valueForPath:FSTTestFieldPath(@"bar.a")]); -} - -- (void)testOverwritesExistingFields { - FSTObjectValue *old = FSTTestObjectValue(@{@"a" : @"old"}); - FSTObjectValue *mod = - [old objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"a")]; - - // Should return a new object, leaving the old one unmodified. - XCTAssertNotEqual(old, mod); - XCTAssertEqualObjects(old, FSTTestFieldValue(@{@"a" : @"old"})); - XCTAssertEqualObjects(mod, FSTTestFieldValue(@{@"a" : @"mod"})); -} - -- (void)testAddsNewFields { - FSTObjectValue *empty = [FSTObjectValue objectValue]; - FSTObjectValue *mod = - [empty objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"a")]; - XCTAssertNotEqual(empty, mod); - XCTAssertEqualObjects(empty, FSTTestFieldValue(@{})); - XCTAssertEqualObjects(mod, FSTTestFieldValue(@{@"a" : @"mod"})); - - FSTObjectValue *old = mod; - mod = [old objectBySettingValue:FSTTestFieldValue(@1) forPath:FSTTestFieldPath(@"b")]; - XCTAssertNotEqual(old, mod); - XCTAssertEqualObjects(old, FSTTestFieldValue(@{@"a" : @"mod"})); - XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @"mod", @"b" : @1 })); -} - -- (void)testImplicitlyCreatesObjects { - FSTObjectValue *old = FSTTestObjectValue(@{@"a" : @"old"}); - FSTObjectValue *mod = - [old objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"b.c.d")]; - XCTAssertNotEqual(old, mod); - XCTAssertEqualObjects(old, FSTTestFieldValue(@{@"a" : @"old"})); - XCTAssertEqualObjects(mod, FSTTestFieldValue( - @{ @"a" : @"old", - @"b" : @{@"c" : @{@"d" : @"mod"}} })); -} - -- (void)testCanOverwritePrimitivesWithObjects { - FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @{@"b" : @"old"} }); - FSTObjectValue *mod = - [old objectBySettingValue:FSTTestFieldValue(@{@"b" : @"mod"}) forPath:FSTTestFieldPath(@"a")]; - XCTAssertNotEqual(old, mod); - XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @"old"} })); - XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @"mod"} })); -} - -- (void)testAddsToNestedObjects { - FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @{@"b" : @"old"} }); - FSTObjectValue *mod = - [old objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"a.c")]; - XCTAssertNotEqual(old, mod); - XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @"old"} })); - XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @"old", @"c" : @"mod"} })); -} - -- (void)testDeletesKeys { - FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @1, @"b" : @2 }); - FSTObjectValue *mod = [old objectByDeletingPath:FSTTestFieldPath(@"a")]; - XCTAssertNotEqual(old, mod); - XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @1, @"b" : @2 })); - XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"b" : @2 })); - - FSTObjectValue *empty = [mod objectByDeletingPath:FSTTestFieldPath(@"b")]; - XCTAssertNotEqual(mod, empty); - XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"b" : @2 })); - XCTAssertEqualObjects(empty, FSTTestFieldValue(@{})); -} - -- (void)testDeletesHandleMissingKeys { - FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @{@"b" : @1, @"c" : @2} }); - FSTObjectValue *mod = [old objectByDeletingPath:FSTTestFieldPath(@"b")]; - XCTAssertEqualObjects(old, mod); - XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @2} })); - - mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.d")]; - XCTAssertEqualObjects(old, mod); - XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @2} })); - - mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.b.c")]; - XCTAssertEqualObjects(old, mod); - XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @2} })); -} - -- (void)testDeletesNestedKeys { - FSTObjectValue *old = FSTTestObjectValue( - @{ @"a" : @{@"b" : @1, @"c" : @{@"d" : @2, @"e" : @3}} }); - FSTObjectValue *mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.c.d")]; - XCTAssertNotEqual(old, mod); - XCTAssertEqualObjects(old, FSTTestFieldValue( - @{ @"a" : @{@"b" : @1, @"c" : @{@"d" : @2, @"e" : @3}} })); - XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @{@"e" : @3}} })); - - old = mod; - mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.c")]; - XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @{@"e" : @3}} })); - XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1} })); - - old = mod; - mod = [old objectByDeletingPath:FSTTestFieldPath(@"a")]; - XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @1} })); - XCTAssertEqualObjects(mod, FSTTestFieldValue(@{})); -} - -- (void)testArrays { - FSTArrayValue *expected = [[FSTArrayValue alloc] - initWithValueNoCopy:@[ [FSTStringValue stringValue:@"value"], [FSTBooleanValue trueValue] ]]; - - FSTArrayValue *actual = (FSTArrayValue *)FSTTestFieldValue(@[ @"value", @YES ]); - XCTAssertEqualObjects(actual, expected); -} - -- (void)testValueEquality { - NSArray *groups = @[ - @[ FSTTestFieldValue(@YES), [FSTBooleanValue booleanValue:YES] ], - @[ FSTTestFieldValue(@NO), [FSTBooleanValue booleanValue:NO] ], - @[ FSTTestFieldValue([NSNull null]), [FSTNullValue nullValue] ], - @[ FSTTestFieldValue(@(0.0 / 0.0)), FSTTestFieldValue(@(NAN)), [FSTDoubleValue nanValue] ], - // -0.0 and 0.0 compare: the same (but are not isEqual:) - @[ FSTTestFieldValue(@(-0.0)) ], @[ FSTTestFieldValue(@0.0) ], - @[ FSTTestFieldValue(@1), FSTTestFieldValue(@1LL), [FSTIntegerValue integerValue:1LL] ], - // double and unit64_t values can compare: the same (but won't be isEqual:) - @[ FSTTestFieldValue(@1.0), [FSTDoubleValue doubleValue:1.0] ], - @[ FSTTestFieldValue(@1.1), [FSTDoubleValue doubleValue:1.1] ], - @[ - FSTTestFieldValue(FSTTestData(0, 1, 2, -1)), [FSTBlobValue blobValue:FSTTestData(0, 1, 2, -1)] - ], - @[ FSTTestFieldValue(FSTTestData(0, 1, -1)) ], - @[ FSTTestFieldValue(@"string"), [FSTStringValue stringValue:@"string"] ], - @[ FSTTestFieldValue(@"strin") ], - @[ FSTTestFieldValue(@"e\u0301b") ], // latin small letter e + combining acute accent - @[ FSTTestFieldValue(@"\u00e9a") ], // latin small letter e with acute accent - @[ - FSTTestFieldValue(date1), - [FSTTimestampValue timestampValue:[FSTTimestamp timestampWithDate:date1]] - ], - @[ FSTTestFieldValue(date2) ], - @[ - // NOTE: ServerTimestampValues can't be parsed via FSTTestFieldValue(). - [FSTServerTimestampValue - serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date1] - previousValue:nil], - [FSTServerTimestampValue - serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date1] - previousValue:nil] - ], - @[ [FSTServerTimestampValue - serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date2] - previousValue:nil] ], - @[ - FSTTestFieldValue(FSTTestGeoPoint(0, 1)), - [FSTGeoPointValue geoPointValue:FSTTestGeoPoint(0, 1)] - ], - @[ FSTTestFieldValue(FSTTestGeoPoint(1, 0)) ], - @[ - [FSTReferenceValue referenceValue:FSTTestDocKey(@"coll/doc1") - databaseID:[FSTDatabaseID databaseIDWithProject:@"project" - database:kDefaultDatabaseID]], - FSTTestFieldValue(FSTTestRef(@"project", kDefaultDatabaseID, @"coll/doc1")) - ], - @[ FSTTestRef(@"project", @"(default)", @"coll/doc2") ], - @[ FSTTestFieldValue(@[ @"foo", @"bar" ]), FSTTestFieldValue(@[ @"foo", @"bar" ]) ], - @[ FSTTestFieldValue(@[ @"foo", @"bar", @"baz" ]) ], @[ FSTTestFieldValue(@[ @"foo" ]) ], - @[ - FSTTestFieldValue( - @{ @"bar" : @1, - @"foo" : @2 }), - FSTTestFieldValue( - @{ @"foo" : @2, - @"bar" : @1 }) - ], - @[ FSTTestFieldValue( - @{ @"bar" : @2, - @"foo" : @1 }) ], - @[ FSTTestFieldValue( - @{ @"bar" : @1, - @"foo" : @1 }) ], - @[ FSTTestFieldValue( - @{ @"foo" : @1 }) ] - ]; - - FSTAssertEqualityGroups(groups); -} - -- (void)testValueOrdering { - NSArray *groups = @[ - // null first - @[ [NSNull null] ], - - // booleans - @[ @NO ], @[ @YES ], - - // numbers - @[ @(0.0 / 0.0) ], @[ @(-INFINITY) ], @[ @(-DBL_MAX) ], @[ @(LLONG_MIN) ], @[ @(-1.1) ], - @[ @(-1.0), @(-1LL) ], // longs and doubles compare the same - @[ @(-DBL_MIN) ], - @[ @(-0x1.0p-1074) ], // negative smallest subnormal - @[ @(-0.0), @(0.0), @(0LL) ], // zeros all compare the same - @[ @(0x1.0p-1074) ], // positive smallest subnormal - @[ @(DBL_MIN) ], @[ @1.0, @1LL ], // longs and doubles compare the same - @[ @1.1 ], @[ @(LLONG_MAX) ], @[ @(DBL_MAX) ], @[ @(INFINITY) ], - - // timestamps - @[ date1 ], @[ date2 ], - - // server timestamps come after all concrete timestamps. - // NOTE: server timestamps can't be parsed directly, so we have special sentinel strings (see - // FSTWrapGroups()). - @[ @"server-timestamp-1" ], @[ @"server-timestamp-2" ], - - // strings - @[ @"" ], @[ @"\000\ud7ff\ue000\uffff" ], @[ @"(╯°□°)╯︵ ┻━┻" ], @[ @"a" ], @[ @"abc def" ], - @[ @"e\u0301b" ], // latin small letter e + combining acute accent + latin small letter b - @[ @"æ" ], - @[ @"\u00e9a" ], // latin small letter e with acute accent + latin small letter a - - // blobs - @[ FSTTestData(-1) ], @[ FSTTestData(0, -1) ], @[ FSTTestData(0, 1, 2, 3, 4, -1) ], - @[ FSTTestData(0, 1, 2, 4, 3, -1) ], @[ FSTTestData(255, -1) ], - - // resource names - @[ FSTTestRef(@"p1", @"d1", @"c1/doc1") ], @[ FSTTestRef(@"p1", @"d1", @"c1/doc2") ], - @[ FSTTestRef(@"p1", @"d1", @"c10/doc1") ], @[ FSTTestRef(@"p1", @"d1", @"c2/doc1") ], - @[ FSTTestRef(@"p1", @"d2", @"c1/doc1") ], @[ FSTTestRef(@"p2", @"d1", @"c1/doc1") ], - - // Geo points - @[ FSTTestGeoPoint(-90, -180) ], @[ FSTTestGeoPoint(-90, 0) ], @[ FSTTestGeoPoint(-90, 180) ], - @[ FSTTestGeoPoint(0, -180) ], @[ FSTTestGeoPoint(0, 0) ], @[ FSTTestGeoPoint(0, 180) ], - @[ FSTTestGeoPoint(1, -180) ], @[ FSTTestGeoPoint(1, 0) ], @[ FSTTestGeoPoint(1, 180) ], - @[ FSTTestGeoPoint(90, -180) ], @[ FSTTestGeoPoint(90, 0) ], @[ FSTTestGeoPoint(90, 180) ], - - // Arrays - @[ @[] ], @[ @[ @"bar" ] ], @[ @[ @"foo" ] ], @[ @[ @"foo", @1 ] ], @[ @[ @"foo", @2 ] ], - @[ @[ @"foo", @"0" ] ], - - // Objects - @[ - @{ @"bar" : @0 } - ], - @[ - @{ @"bar" : @0, - @"foo" : @1 } - ], - @[ - @{ @"foo" : @1 } - ], - @[ - @{ @"foo" : @2 } - ], - @[ @{@"foo" : @"0"} ] - ]; - - NSArray *wrapped = FSTWrapGroups(groups); - FSTAssertComparisons(wrapped); -} - -- (void)testValue { - NSDate *date = [NSDate date]; - id input = @{ @"array" : @[ @1, date ], @"obj" : @{@"date" : date, @"string" : @"hi"} }; - FSTObjectValue *value = FSTTestObjectValue(input); - id output = [value value]; - { - XCTAssertTrue([output[@"array"][1] isKindOfClass:[NSDate class]]); - NSDate *actual = output[@"array"][1]; - XCTAssertEqualWithAccuracy(date.timeIntervalSince1970, actual.timeIntervalSince1970, - 0.000000001); - } - { - XCTAssertTrue([output[@"obj"][@"date"] isKindOfClass:[NSDate class]]); - NSDate *actual = output[@"obj"][@"date"]; - XCTAssertEqualWithAccuracy(date.timeIntervalSince1970, actual.timeIntervalSince1970, - 0.000000001); - } -} - -@end diff --git a/Firestore/Example/Tests/Model/FSTFieldValueTests.mm b/Firestore/Example/Tests/Model/FSTFieldValueTests.mm new file mode 100644 index 0000000..56b885f --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTFieldValueTests.mm @@ -0,0 +1,582 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Model/FSTFieldValue.h" + +#import +#import + +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FSTUserDataConverter.h" +#import "Firestore/Source/Core/FSTTimestamp.h" +#import "Firestore/Source/Model/FSTDatabaseID.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTPath.h" + +#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +/** Helper to wrap the values in a set of equality groups using FSTTestFieldValue(). */ +NSArray *FSTWrapGroups(NSArray *groups) { + NSMutableArray *wrapped = [NSMutableArray array]; + for (NSArray *group in groups) { + NSMutableArray *wrappedGroup = [NSMutableArray array]; + for (id value in group) { + FSTFieldValue *wrappedValue; + // Server Timestamp values can't be parsed directly, so we have a couple predefined sentinel + // strings that can be used instead. + if ([value isEqual:@"server-timestamp-1"]) { + wrappedValue = [FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:FSTTestTimestamp(2016, 5, 20, 10, 20, 0) + previousValue:nil]; + } else if ([value isEqual:@"server-timestamp-2"]) { + wrappedValue = [FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:FSTTestTimestamp(2016, 10, 21, 15, 32, 0) + previousValue:nil]; + } else if ([value isKindOfClass:[FSTDocumentKeyReference class]]) { + // We directly convert these here so that the databaseIDs can be different. + FSTDocumentKeyReference *reference = (FSTDocumentKeyReference *)value; + wrappedValue = + [FSTReferenceValue referenceValue:reference.key databaseID:reference.databaseID]; + } else { + wrappedValue = FSTTestFieldValue(value); + } + [wrappedGroup addObject:wrappedValue]; + } + [wrapped addObject:wrappedGroup]; + } + return wrapped; +} + +@interface FSTFieldValueTests : XCTestCase +@end + +@implementation FSTFieldValueTests { + NSDate *date1; + NSDate *date2; +} + +- (void)setUp { + [super setUp]; + // Create a couple date objects for use in tests. + date1 = FSTTestDate(2016, 5, 20, 10, 20, 0); + date2 = FSTTestDate(2016, 10, 21, 15, 32, 0); +} + +- (void)testWrapIntegers { + NSArray *values = @[ + @(INT_MIN), @(-1), @0, @1, @2, @(UCHAR_MAX), @(INT_MAX), // Standard integers + @(LONG_MIN), @(LONG_MAX), @(LLONG_MIN), @(LLONG_MAX) // Larger values + ]; + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTIntegerValue class]); + XCTAssertEqualObjects([wrapped value], @([value longLongValue])); + } +} + +- (void)testWrapsDoubles { + // Note that 0x1.0p-1074 is a hex floating point literal representing the minimum subnormal + // number: . + NSArray *values = @[ + @(-INFINITY), @(-DBL_MAX), @(LLONG_MIN * -1.0), @(-1.1), @(-0x1.0p-1074), @(-0.0), @(0.0), + @(0x1.0p-1074), @(DBL_MIN), @(1.1), @(LLONG_MAX * 1.0), @(DBL_MAX), @(INFINITY) + ]; + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTDoubleValue class]); + XCTAssertEqualObjects([wrapped value], value); + } +} + +- (void)testWrapsNilAndNSNull { + FSTNullValue *nullValue = [FSTNullValue nullValue]; + XCTAssertEqual(FSTTestFieldValue(nil), nullValue); + XCTAssertEqual(FSTTestFieldValue([NSNull null]), nullValue); + XCTAssertEqual([nullValue value], [NSNull null]); +} + +- (void)testWrapsBooleans { + NSArray *values = @[ @YES, @NO, [NSNumber numberWithChar:1], [NSNumber numberWithChar:0] ]; + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTBooleanValue class]); + XCTAssertEqualObjects([wrapped value], value); + } + + // Unsigned chars could conceivably be handled consistently with signed chars but on arm64 these + // end up being stored as signed shorts. + FSTFieldValue *wrapped = FSTTestFieldValue([NSNumber numberWithUnsignedChar:1]); + XCTAssertEqualObjects(wrapped, [FSTIntegerValue integerValue:1]); +} + +union DoubleBits { + double d; + uint64_t bits; +}; + +- (void)testNormalizesNaNs { + // NOTE: With v1beta1 query semantics, it's no longer as important that our NaN representation + // matches the backend, since all NaNs are defined to sort as equal, but we preserve the + // normalization and this test regardless for now. + + // We use a canonical NaN bit pattern that's common for both Java and Objective-C. Specifically: + // - sign: 0 + // - exponent: 11 bits, all 1 + // - significand: 52 bits, MSB=1, rest=0 + // + // This matches the Firestore backend which uses Java's Double.doubleToLongBits which is defined + // to normalize all NaNs to this value. + union DoubleBits canonical = {.bits = 0x7ff8000000000000ULL}; + + // IEEE 754 specifies that NaN isn't equal to itself. + XCTAssertTrue(isnan(canonical.d)); + XCTAssertEqual(canonical.bits, canonical.bits); + XCTAssertNotEqual(canonical.d, canonical.d); + + // All permutations of the 51 other non-MSB significand bits are also NaNs. + union DoubleBits alternate = {.bits = 0x7fff000000000000ULL}; + XCTAssertTrue(isnan(alternate.d)); + XCTAssertNotEqual(alternate.bits, canonical.bits); + XCTAssertNotEqual(alternate.d, canonical.d); + + // Even though at the C-level assignment preserves non-canonical NaNs, NSNumber normalizes all + // NaNs to single shared instance, kCFNumberNaN. That NaN has no public definition for its value + // but it happens to match what we need. + union DoubleBits normalized = {.d = [[NSNumber numberWithDouble:alternate.d] doubleValue]}; + XCTAssertEqual(normalized.bits, canonical.bits); + + // Ensure we get the same normalization behavior (currently implemented explicitly by checking + // for isnan() and then explicitly assigning NAN). + union DoubleBits result; + result.d = [[FSTDoubleValue doubleValue:canonical.d] internalValue]; + XCTAssertEqual(result.bits, canonical.bits); + + result.d = [[FSTDoubleValue doubleValue:alternate.d] internalValue]; + XCTAssertEqual(result.bits, canonical.bits); + + // A NaN that's canonical except it has the sign bit set (would be negative if signs mattered) + union DoubleBits negative = {.bits = 0xfff8000000000000ULL}; + result.d = [[FSTDoubleValue doubleValue:negative.d] internalValue]; + XCTAssertTrue(isnan(negative.d)); + XCTAssertEqual(result.bits, canonical.bits); + + // A signaling NaN with significand where MSB is 0, and some non-MSB bit is one. + union DoubleBits signaling = {.bits = 0xfff4000000000000ULL}; + XCTAssertTrue(isnan(signaling.d)); + result.d = [[FSTDoubleValue doubleValue:signaling.d] internalValue]; + XCTAssertEqual(result.bits, canonical.bits); +} + +- (void)testZeros { + // Floating point numbers have an explicit sign bit so it's possible to end up with negative + // zero as a distinct value from positive zero. + union DoubleBits zero = {.d = 0.0}; + union DoubleBits negativeZero = {.d = -0.0}; + + // IEEE 754 requires these two zeros to compare equal. + XCTAssertNotEqual(zero.bits, negativeZero.bits); + XCTAssertEqual(zero.d, negativeZero.d); + + // NSNumber preserves the negative zero value but compares equal according to IEEE 754. + union DoubleBits normalized = {.d = [[NSNumber numberWithDouble:negativeZero.d] doubleValue]}; + XCTAssertEqual(normalized.bits, negativeZero.bits); + XCTAssertEqualObjects([NSNumber numberWithDouble:0.0], [NSNumber numberWithDouble:-0.0]); + + // FSTDoubleValue preserves positive/negative zero + union DoubleBits result; + result.d = [[[FSTDoubleValue doubleValue:zero.d] value] doubleValue]; + XCTAssertEqual(result.bits, zero.bits); + result.d = [[[FSTDoubleValue doubleValue:negativeZero.d] value] doubleValue]; + XCTAssertEqual(result.bits, negativeZero.bits); + + // ... but compares positive/negative zero as unequal, compatibly with Firestore. + XCTAssertNotEqualObjects([FSTDoubleValue doubleValue:0.0], [FSTDoubleValue doubleValue:-0.0]); +} + +- (void)testWrapStrings { + NSArray *values = @[ @"", @"abc" ]; + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTStringValue class]); + XCTAssertEqualObjects([wrapped value], value); + } +} + +- (void)testWrapDates { + NSArray *values = @[ FSTTestDate(1900, 12, 1, 1, 20, 30), FSTTestDate(2017, 4, 24, 13, 20, 30) ]; + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTTimestampValue class]); + XCTAssertEqualObjects([wrapped value], value); + + XCTAssertEqualObjects(((FSTTimestampValue *)wrapped).internalValue, + [FSTTimestamp timestampWithDate:value]); + } +} + +- (void)testWrapGeoPoints { + NSArray *values = @[ FSTTestGeoPoint(1.24, 4.56), FSTTestGeoPoint(-20, 100) ]; + + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTGeoPointValue class]); + XCTAssertEqualObjects([wrapped value], value); + } +} + +- (void)testWrapBlobs { + NSArray *values = @[ FSTTestData(1, 2, 3), FSTTestData(1, 2) ]; + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTBlobValue class]); + XCTAssertEqualObjects([wrapped value], value); + } +} + +- (void)testWrapResourceNames { + NSArray *values = @[ + FSTTestRef(@"project", kDefaultDatabaseID, @"foo/bar"), + FSTTestRef(@"project", kDefaultDatabaseID, @"foo/baz") + ]; + for (FSTDocumentKeyReference *value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTReferenceValue class]); + XCTAssertEqualObjects([wrapped value], value.key); + XCTAssertEqualObjects(((FSTDatabaseID *)wrapped).databaseID, value.databaseID); + } +} + +- (void)testWrapsEmptyObjects { + XCTAssertEqualObjects(FSTTestFieldValue(@{}), [FSTObjectValue objectValue]); +} + +- (void)testWrapsSimpleObjects { + FSTObjectValue *actual = FSTTestObjectValue( + @{ @"a" : @"foo", + @"b" : @(1L), + @"c" : @YES, + @"d" : [NSNull null] }); + FSTObjectValue *expected = [[FSTObjectValue alloc] initWithDictionary:@{ + @"a" : [FSTStringValue stringValue:@"foo"], + @"b" : [FSTIntegerValue integerValue:1LL], + @"c" : [FSTBooleanValue trueValue], + @"d" : [FSTNullValue nullValue] + }]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testWrapsNestedObjects { + FSTObjectValue *actual = FSTTestObjectValue(@{ @"a" : @{@"b" : @{@"c" : @"foo"}, @"d" : @YES} }); + FSTObjectValue *expected = [[FSTObjectValue alloc] initWithDictionary:@{ + @"a" : [[FSTObjectValue alloc] initWithDictionary:@{ + @"b" : + [[FSTObjectValue alloc] initWithDictionary:@{@"c" : [FSTStringValue stringValue:@"foo"]}], + @"d" : [FSTBooleanValue booleanValue:YES] + }] + }]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testExtractsFields { + FSTObjectValue *obj = FSTTestObjectValue(@{ @"foo" : @{@"a" : @YES, @"b" : @"string"} }); + FSTAssertIsKindOfClass(obj, FSTObjectValue); + + FSTAssertIsKindOfClass([obj valueForPath:FSTTestFieldPath(@"foo")], FSTObjectValue); + XCTAssertEqualObjects([obj valueForPath:FSTTestFieldPath(@"foo.a")], [FSTBooleanValue trueValue]); + XCTAssertEqualObjects([obj valueForPath:FSTTestFieldPath(@"foo.b")], + [FSTStringValue stringValue:@"string"]); + + XCTAssertNil([obj valueForPath:FSTTestFieldPath(@"foo.a.b")]); + XCTAssertNil([obj valueForPath:FSTTestFieldPath(@"bar")]); + XCTAssertNil([obj valueForPath:FSTTestFieldPath(@"bar.a")]); +} + +- (void)testOverwritesExistingFields { + FSTObjectValue *old = FSTTestObjectValue(@{@"a" : @"old"}); + FSTObjectValue *mod = + [old objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"a")]; + + // Should return a new object, leaving the old one unmodified. + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue(@{@"a" : @"old"})); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{@"a" : @"mod"})); +} + +- (void)testAddsNewFields { + FSTObjectValue *empty = [FSTObjectValue objectValue]; + FSTObjectValue *mod = + [empty objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"a")]; + XCTAssertNotEqual(empty, mod); + XCTAssertEqualObjects(empty, FSTTestFieldValue(@{})); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{@"a" : @"mod"})); + + FSTObjectValue *old = mod; + mod = [old objectBySettingValue:FSTTestFieldValue(@1) forPath:FSTTestFieldPath(@"b")]; + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue(@{@"a" : @"mod"})); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @"mod", @"b" : @1 })); +} + +- (void)testImplicitlyCreatesObjects { + FSTObjectValue *old = FSTTestObjectValue(@{@"a" : @"old"}); + FSTObjectValue *mod = + [old objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"b.c.d")]; + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue(@{@"a" : @"old"})); + XCTAssertEqualObjects(mod, FSTTestFieldValue( + @{ @"a" : @"old", + @"b" : @{@"c" : @{@"d" : @"mod"}} })); +} + +- (void)testCanOverwritePrimitivesWithObjects { + FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @{@"b" : @"old"} }); + FSTObjectValue *mod = + [old objectBySettingValue:FSTTestFieldValue(@{@"b" : @"mod"}) forPath:FSTTestFieldPath(@"a")]; + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @"old"} })); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @"mod"} })); +} + +- (void)testAddsToNestedObjects { + FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @{@"b" : @"old"} }); + FSTObjectValue *mod = + [old objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"a.c")]; + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @"old"} })); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @"old", @"c" : @"mod"} })); +} + +- (void)testDeletesKeys { + FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @1, @"b" : @2 }); + FSTObjectValue *mod = [old objectByDeletingPath:FSTTestFieldPath(@"a")]; + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @1, @"b" : @2 })); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"b" : @2 })); + + FSTObjectValue *empty = [mod objectByDeletingPath:FSTTestFieldPath(@"b")]; + XCTAssertNotEqual(mod, empty); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"b" : @2 })); + XCTAssertEqualObjects(empty, FSTTestFieldValue(@{})); +} + +- (void)testDeletesHandleMissingKeys { + FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @{@"b" : @1, @"c" : @2} }); + FSTObjectValue *mod = [old objectByDeletingPath:FSTTestFieldPath(@"b")]; + XCTAssertEqualObjects(old, mod); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @2} })); + + mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.d")]; + XCTAssertEqualObjects(old, mod); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @2} })); + + mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.b.c")]; + XCTAssertEqualObjects(old, mod); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @2} })); +} + +- (void)testDeletesNestedKeys { + FSTObjectValue *old = FSTTestObjectValue( + @{ @"a" : @{@"b" : @1, @"c" : @{@"d" : @2, @"e" : @3}} }); + FSTObjectValue *mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.c.d")]; + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue( + @{ @"a" : @{@"b" : @1, @"c" : @{@"d" : @2, @"e" : @3}} })); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @{@"e" : @3}} })); + + old = mod; + mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.c")]; + XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @{@"e" : @3}} })); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1} })); + + old = mod; + mod = [old objectByDeletingPath:FSTTestFieldPath(@"a")]; + XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @1} })); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{})); +} + +- (void)testArrays { + FSTArrayValue *expected = [[FSTArrayValue alloc] + initWithValueNoCopy:@[ [FSTStringValue stringValue:@"value"], [FSTBooleanValue trueValue] ]]; + + FSTArrayValue *actual = (FSTArrayValue *)FSTTestFieldValue(@[ @"value", @YES ]); + XCTAssertEqualObjects(actual, expected); +} + +- (void)testValueEquality { + NSArray *groups = @[ + @[ FSTTestFieldValue(@YES), [FSTBooleanValue booleanValue:YES] ], + @[ FSTTestFieldValue(@NO), [FSTBooleanValue booleanValue:NO] ], + @[ FSTTestFieldValue([NSNull null]), [FSTNullValue nullValue] ], + @[ FSTTestFieldValue(@(0.0 / 0.0)), FSTTestFieldValue(@(NAN)), [FSTDoubleValue nanValue] ], + // -0.0 and 0.0 compare: the same (but are not isEqual:) + @[ FSTTestFieldValue(@(-0.0)) ], @[ FSTTestFieldValue(@0.0) ], + @[ FSTTestFieldValue(@1), FSTTestFieldValue(@1LL), [FSTIntegerValue integerValue:1LL] ], + // double and unit64_t values can compare: the same (but won't be isEqual:) + @[ FSTTestFieldValue(@1.0), [FSTDoubleValue doubleValue:1.0] ], + @[ FSTTestFieldValue(@1.1), [FSTDoubleValue doubleValue:1.1] ], + @[ + FSTTestFieldValue(FSTTestData(0, 1, 2, -1)), [FSTBlobValue blobValue:FSTTestData(0, 1, 2, -1)] + ], + @[ FSTTestFieldValue(FSTTestData(0, 1, -1)) ], + @[ FSTTestFieldValue(@"string"), [FSTStringValue stringValue:@"string"] ], + @[ FSTTestFieldValue(@"strin") ], + @[ FSTTestFieldValue(@"e\u0301b") ], // latin small letter e + combining acute accent + @[ FSTTestFieldValue(@"\u00e9a") ], // latin small letter e with acute accent + @[ + FSTTestFieldValue(date1), + [FSTTimestampValue timestampValue:[FSTTimestamp timestampWithDate:date1]] + ], + @[ FSTTestFieldValue(date2) ], + @[ + // NOTE: ServerTimestampValues can't be parsed via FSTTestFieldValue(). + [FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date1] + previousValue:nil], + [FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date1] + previousValue:nil] + ], + @[ [FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date2] + previousValue:nil] ], + @[ + FSTTestFieldValue(FSTTestGeoPoint(0, 1)), + [FSTGeoPointValue geoPointValue:FSTTestGeoPoint(0, 1)] + ], + @[ FSTTestFieldValue(FSTTestGeoPoint(1, 0)) ], + @[ + [FSTReferenceValue referenceValue:FSTTestDocKey(@"coll/doc1") + databaseID:[FSTDatabaseID databaseIDWithProject:@"project" + database:kDefaultDatabaseID]], + FSTTestFieldValue(FSTTestRef(@"project", kDefaultDatabaseID, @"coll/doc1")) + ], + @[ FSTTestRef(@"project", @"(default)", @"coll/doc2") ], + @[ FSTTestFieldValue(@[ @"foo", @"bar" ]), FSTTestFieldValue(@[ @"foo", @"bar" ]) ], + @[ FSTTestFieldValue(@[ @"foo", @"bar", @"baz" ]) ], @[ FSTTestFieldValue(@[ @"foo" ]) ], + @[ + FSTTestFieldValue( + @{ @"bar" : @1, + @"foo" : @2 }), + FSTTestFieldValue( + @{ @"foo" : @2, + @"bar" : @1 }) + ], + @[ FSTTestFieldValue( + @{ @"bar" : @2, + @"foo" : @1 }) ], + @[ FSTTestFieldValue( + @{ @"bar" : @1, + @"foo" : @1 }) ], + @[ FSTTestFieldValue( + @{ @"foo" : @1 }) ] + ]; + + FSTAssertEqualityGroups(groups); +} + +- (void)testValueOrdering { + NSArray *groups = @[ + // null first + @[ [NSNull null] ], + + // booleans + @[ @NO ], @[ @YES ], + + // numbers + @[ @(0.0 / 0.0) ], @[ @(-INFINITY) ], @[ @(-DBL_MAX) ], @[ @(LLONG_MIN) ], @[ @(-1.1) ], + @[ @(-1.0), @(-1LL) ], // longs and doubles compare the same + @[ @(-DBL_MIN) ], + @[ @(-0x1.0p-1074) ], // negative smallest subnormal + @[ @(-0.0), @(0.0), @(0LL) ], // zeros all compare the same + @[ @(0x1.0p-1074) ], // positive smallest subnormal + @[ @(DBL_MIN) ], @[ @1.0, @1LL ], // longs and doubles compare the same + @[ @1.1 ], @[ @(LLONG_MAX) ], @[ @(DBL_MAX) ], @[ @(INFINITY) ], + + // timestamps + @[ date1 ], @[ date2 ], + + // server timestamps come after all concrete timestamps. + // NOTE: server timestamps can't be parsed directly, so we have special sentinel strings (see + // FSTWrapGroups()). + @[ @"server-timestamp-1" ], @[ @"server-timestamp-2" ], + + // strings + @[ @"" ], @[ @"\000\ud7ff\ue000\uffff" ], @[ @"(╯°□°)╯︵ ┻━┻" ], @[ @"a" ], @[ @"abc def" ], + @[ @"e\u0301b" ], // latin small letter e + combining acute accent + latin small letter b + @[ @"æ" ], + @[ @"\u00e9a" ], // latin small letter e with acute accent + latin small letter a + + // blobs + @[ FSTTestData(-1) ], @[ FSTTestData(0, -1) ], @[ FSTTestData(0, 1, 2, 3, 4, -1) ], + @[ FSTTestData(0, 1, 2, 4, 3, -1) ], @[ FSTTestData(255, -1) ], + + // resource names + @[ FSTTestRef(@"p1", @"d1", @"c1/doc1") ], @[ FSTTestRef(@"p1", @"d1", @"c1/doc2") ], + @[ FSTTestRef(@"p1", @"d1", @"c10/doc1") ], @[ FSTTestRef(@"p1", @"d1", @"c2/doc1") ], + @[ FSTTestRef(@"p1", @"d2", @"c1/doc1") ], @[ FSTTestRef(@"p2", @"d1", @"c1/doc1") ], + + // Geo points + @[ FSTTestGeoPoint(-90, -180) ], @[ FSTTestGeoPoint(-90, 0) ], @[ FSTTestGeoPoint(-90, 180) ], + @[ FSTTestGeoPoint(0, -180) ], @[ FSTTestGeoPoint(0, 0) ], @[ FSTTestGeoPoint(0, 180) ], + @[ FSTTestGeoPoint(1, -180) ], @[ FSTTestGeoPoint(1, 0) ], @[ FSTTestGeoPoint(1, 180) ], + @[ FSTTestGeoPoint(90, -180) ], @[ FSTTestGeoPoint(90, 0) ], @[ FSTTestGeoPoint(90, 180) ], + + // Arrays + @[ @[] ], @[ @[ @"bar" ] ], @[ @[ @"foo" ] ], @[ @[ @"foo", @1 ] ], @[ @[ @"foo", @2 ] ], + @[ @[ @"foo", @"0" ] ], + + // Objects + @[ + @{ @"bar" : @0 } + ], + @[ + @{ @"bar" : @0, + @"foo" : @1 } + ], + @[ + @{ @"foo" : @1 } + ], + @[ + @{ @"foo" : @2 } + ], + @[ @{@"foo" : @"0"} ] + ]; + + NSArray *wrapped = FSTWrapGroups(groups); + FSTAssertComparisons(wrapped); +} + +- (void)testValue { + NSDate *date = [NSDate date]; + id input = @{ @"array" : @[ @1, date ], @"obj" : @{@"date" : date, @"string" : @"hi"} }; + FSTObjectValue *value = FSTTestObjectValue(input); + id output = [value value]; + { + XCTAssertTrue([output[@"array"][1] isKindOfClass:[NSDate class]]); + NSDate *actual = output[@"array"][1]; + XCTAssertEqualWithAccuracy(date.timeIntervalSince1970, actual.timeIntervalSince1970, + 0.000000001); + } + { + XCTAssertTrue([output[@"obj"][@"date"] isKindOfClass:[NSDate class]]); + NSDate *actual = output[@"obj"][@"date"]; + XCTAssertEqualWithAccuracy(date.timeIntervalSince1970, actual.timeIntervalSince1970, + 0.000000001); + } +} + +@end diff --git a/Firestore/Example/Tests/Model/FSTMutationTests.m b/Firestore/Example/Tests/Model/FSTMutationTests.m deleted file mode 100644 index 47fa9b3..0000000 --- a/Firestore/Example/Tests/Model/FSTMutationTests.m +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Model/FSTMutation.h" - -#import - -#import "Firestore/Source/Core/FSTTimestamp.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTPath.h" - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -@interface FSTMutationTests : XCTestCase -@end - -@implementation FSTMutationTests { - FSTTimestamp *_timestamp; -} - -- (void)setUp { - _timestamp = [FSTTimestamp timestamp]; -} - -- (void)testAppliesSetsToDocuments { - NSDictionary *docData = @{@"foo" : @"foo-value", @"baz" : @"baz-value"}; - FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); - - FSTMutation *set = FSTTestSetMutation(@"collection/key", @{@"bar" : @"bar-value"}); - FSTMaybeDocument *setDoc = [set applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; - - NSDictionary *expectedData = @{@"bar" : @"bar-value"}; - XCTAssertEqualObjects(setDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); -} - -- (void)testAppliesPatchesToDocuments { - NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" }; - FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); - - FSTMutation *patch = - FSTTestPatchMutation(@"collection/key", @{@"foo.bar" : @"new-bar-value"}, nil); - FSTMaybeDocument *patchedDoc = - [patch applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; - - NSDictionary *expectedData = @{ @"foo" : @{@"bar" : @"new-bar-value"}, @"baz" : @"baz-value" }; - XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); -} - -- (void)testDeletesValuesFromTheFieldMask { - NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value", @"baz" : @"baz-value"} }; - FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); - - FSTDocumentKey *key = FSTTestDocKey(@"collection/key"); - FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:@[ FSTTestFieldPath(@"foo.bar") ]]; - FSTMutation *patch = [[FSTPatchMutation alloc] initWithKey:key - fieldMask:mask - value:[FSTObjectValue objectValue] - precondition:[FSTPrecondition none]]; - FSTMaybeDocument *patchedDoc = - [patch applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; - - NSDictionary *expectedData = @{ @"foo" : @{@"baz" : @"baz-value"} }; - XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); -} - -- (void)testPatchesPrimitiveValue { - NSDictionary *docData = @{@"foo" : @"foo-value", @"baz" : @"baz-value"}; - FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); - - FSTMutation *patch = - FSTTestPatchMutation(@"collection/key", @{@"foo.bar" : @"new-bar-value"}, nil); - FSTMaybeDocument *patchedDoc = - [patch applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; - - NSDictionary *expectedData = @{ @"foo" : @{@"bar" : @"new-bar-value"}, @"baz" : @"baz-value" }; - XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); -} - -- (void)testPatchingDeletedDocumentsDoesNothing { - FSTMaybeDocument *baseDoc = FSTTestDeletedDoc(@"collection/key", 0); - FSTMutation *patch = FSTTestPatchMutation(@"collection/key", @{@"foo" : @"bar"}, nil); - FSTMaybeDocument *patchedDoc = - [patch applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; - XCTAssertEqualObjects(patchedDoc, baseDoc); -} - -- (void)testAppliesLocalTransformsToDocuments { - NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" }; - FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); - - FSTMutation *transform = FSTTestTransformMutation(@"collection/key", @[ @"foo.bar" ]); - FSTMaybeDocument *transformedDoc = - [transform applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; - - // Server timestamps aren't parsed, so we manually insert it. - FSTObjectValue *expectedData = FSTTestObjectValue( - @{ @"foo" : @{@"bar" : @""}, - @"baz" : @"baz-value" }); - expectedData = - [expectedData objectBySettingValue:[FSTServerTimestampValue - serverTimestampValueWithLocalWriteTime:_timestamp - previousValue:nil] - forPath:FSTTestFieldPath(@"foo.bar")]; - - FSTDocument *expectedDoc = [FSTDocument documentWithData:expectedData - key:FSTTestDocKey(@"collection/key") - version:FSTTestVersion(0) - hasLocalMutations:YES]; - - XCTAssertEqualObjects(transformedDoc, expectedDoc); -} - -- (void)testAppliesServerAckedTransformsToDocuments { - NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" }; - FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); - - FSTMutation *transform = FSTTestTransformMutation(@"collection/key", @[ @"foo.bar" ]); - - FSTMutationResult *mutationResult = [[FSTMutationResult alloc] - initWithVersion:FSTTestVersion(1) - transformResults:@[ [FSTTimestampValue timestampValue:_timestamp] ]]; - - FSTMaybeDocument *transformedDoc = [transform applyTo:baseDoc - baseDocument:baseDoc - localWriteTime:_timestamp - mutationResult:mutationResult]; - - NSDictionary *expectedData = - @{ @"foo" : @{@"bar" : _timestamp.approximateDateValue}, - @"baz" : @"baz-value" }; - XCTAssertEqualObjects(transformedDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO)); -} - -- (void)testDeleteDeletes { - NSDictionary *docData = @{@"foo" : @"bar"}; - FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); - - FSTMutation *mutation = FSTTestDeleteMutation(@"collection/key"); - FSTMaybeDocument *result = - [mutation applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; - XCTAssertEqualObjects(result, FSTTestDeletedDoc(@"collection/key", 0)); -} - -- (void)testSetWithMutationResult { - NSDictionary *docData = @{@"foo" : @"bar"}; - FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); - - FSTMutation *set = FSTTestSetMutation(@"collection/key", @{@"foo" : @"new-bar"}); - FSTMutationResult *mutationResult = - [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(4) transformResults:nil]; - FSTMaybeDocument *setDoc = [set applyTo:baseDoc - baseDocument:baseDoc - localWriteTime:_timestamp - mutationResult:mutationResult]; - - NSDictionary *expectedData = @{@"foo" : @"new-bar"}; - XCTAssertEqualObjects(setDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO)); -} - -- (void)testPatchWithMutationResult { - NSDictionary *docData = @{@"foo" : @"bar"}; - FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); - - FSTMutation *patch = FSTTestPatchMutation(@"collection/key", @{@"foo" : @"new-bar"}, nil); - FSTMutationResult *mutationResult = - [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(4) transformResults:nil]; - FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc - baseDocument:baseDoc - localWriteTime:_timestamp - mutationResult:mutationResult]; - - NSDictionary *expectedData = @{@"foo" : @"new-bar"}; - XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO)); -} - -#define ASSERT_VERSION_TRANSITION(mutation, base, expected) \ - do { \ - FSTMutationResult *mutationResult = \ - [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(0) transformResults:nil]; \ - FSTMaybeDocument *actual = [mutation applyTo:base \ - baseDocument:base \ - localWriteTime:_timestamp \ - mutationResult:mutationResult]; \ - XCTAssertEqualObjects(actual, expected); \ - } while (0); - -/** - * Tests the transition table documented in FSTMutation.h. - */ -- (void)testTransitions { - FSTDocument *docV0 = FSTTestDoc(@"collection/key", 0, @{}, NO); - FSTDeletedDocument *deletedV0 = FSTTestDeletedDoc(@"collection/key", 0); - - FSTDocument *docV3 = FSTTestDoc(@"collection/key", 3, @{}, NO); - FSTDeletedDocument *deletedV3 = FSTTestDeletedDoc(@"collection/key", 3); - - FSTMutation *setMutation = FSTTestSetMutation(@"collection/key", @{}); - FSTMutation *patchMutation = FSTTestPatchMutation(@"collection/key", @{}, nil); - FSTMutation *deleteMutation = FSTTestDeleteMutation(@"collection/key"); - - ASSERT_VERSION_TRANSITION(setMutation, docV3, docV3); - ASSERT_VERSION_TRANSITION(setMutation, deletedV3, docV0); - ASSERT_VERSION_TRANSITION(setMutation, nil, docV0); - - ASSERT_VERSION_TRANSITION(patchMutation, docV3, docV3); - ASSERT_VERSION_TRANSITION(patchMutation, deletedV3, deletedV3); - ASSERT_VERSION_TRANSITION(patchMutation, nil, nil); - - ASSERT_VERSION_TRANSITION(deleteMutation, docV3, deletedV0); - ASSERT_VERSION_TRANSITION(deleteMutation, deletedV3, deletedV0); - ASSERT_VERSION_TRANSITION(deleteMutation, nil, deletedV0); -} - -#undef ASSERT_TRANSITION - -@end diff --git a/Firestore/Example/Tests/Model/FSTMutationTests.mm b/Firestore/Example/Tests/Model/FSTMutationTests.mm new file mode 100644 index 0000000..47fa9b3 --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTMutationTests.mm @@ -0,0 +1,231 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Model/FSTMutation.h" + +#import + +#import "Firestore/Source/Core/FSTTimestamp.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTPath.h" + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +@interface FSTMutationTests : XCTestCase +@end + +@implementation FSTMutationTests { + FSTTimestamp *_timestamp; +} + +- (void)setUp { + _timestamp = [FSTTimestamp timestamp]; +} + +- (void)testAppliesSetsToDocuments { + NSDictionary *docData = @{@"foo" : @"foo-value", @"baz" : @"baz-value"}; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *set = FSTTestSetMutation(@"collection/key", @{@"bar" : @"bar-value"}); + FSTMaybeDocument *setDoc = [set applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; + + NSDictionary *expectedData = @{@"bar" : @"bar-value"}; + XCTAssertEqualObjects(setDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); +} + +- (void)testAppliesPatchesToDocuments { + NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" }; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *patch = + FSTTestPatchMutation(@"collection/key", @{@"foo.bar" : @"new-bar-value"}, nil); + FSTMaybeDocument *patchedDoc = + [patch applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; + + NSDictionary *expectedData = @{ @"foo" : @{@"bar" : @"new-bar-value"}, @"baz" : @"baz-value" }; + XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); +} + +- (void)testDeletesValuesFromTheFieldMask { + NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value", @"baz" : @"baz-value"} }; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTDocumentKey *key = FSTTestDocKey(@"collection/key"); + FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:@[ FSTTestFieldPath(@"foo.bar") ]]; + FSTMutation *patch = [[FSTPatchMutation alloc] initWithKey:key + fieldMask:mask + value:[FSTObjectValue objectValue] + precondition:[FSTPrecondition none]]; + FSTMaybeDocument *patchedDoc = + [patch applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; + + NSDictionary *expectedData = @{ @"foo" : @{@"baz" : @"baz-value"} }; + XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); +} + +- (void)testPatchesPrimitiveValue { + NSDictionary *docData = @{@"foo" : @"foo-value", @"baz" : @"baz-value"}; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *patch = + FSTTestPatchMutation(@"collection/key", @{@"foo.bar" : @"new-bar-value"}, nil); + FSTMaybeDocument *patchedDoc = + [patch applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; + + NSDictionary *expectedData = @{ @"foo" : @{@"bar" : @"new-bar-value"}, @"baz" : @"baz-value" }; + XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); +} + +- (void)testPatchingDeletedDocumentsDoesNothing { + FSTMaybeDocument *baseDoc = FSTTestDeletedDoc(@"collection/key", 0); + FSTMutation *patch = FSTTestPatchMutation(@"collection/key", @{@"foo" : @"bar"}, nil); + FSTMaybeDocument *patchedDoc = + [patch applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; + XCTAssertEqualObjects(patchedDoc, baseDoc); +} + +- (void)testAppliesLocalTransformsToDocuments { + NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" }; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *transform = FSTTestTransformMutation(@"collection/key", @[ @"foo.bar" ]); + FSTMaybeDocument *transformedDoc = + [transform applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; + + // Server timestamps aren't parsed, so we manually insert it. + FSTObjectValue *expectedData = FSTTestObjectValue( + @{ @"foo" : @{@"bar" : @""}, + @"baz" : @"baz-value" }); + expectedData = + [expectedData objectBySettingValue:[FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:_timestamp + previousValue:nil] + forPath:FSTTestFieldPath(@"foo.bar")]; + + FSTDocument *expectedDoc = [FSTDocument documentWithData:expectedData + key:FSTTestDocKey(@"collection/key") + version:FSTTestVersion(0) + hasLocalMutations:YES]; + + XCTAssertEqualObjects(transformedDoc, expectedDoc); +} + +- (void)testAppliesServerAckedTransformsToDocuments { + NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" }; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *transform = FSTTestTransformMutation(@"collection/key", @[ @"foo.bar" ]); + + FSTMutationResult *mutationResult = [[FSTMutationResult alloc] + initWithVersion:FSTTestVersion(1) + transformResults:@[ [FSTTimestampValue timestampValue:_timestamp] ]]; + + FSTMaybeDocument *transformedDoc = [transform applyTo:baseDoc + baseDocument:baseDoc + localWriteTime:_timestamp + mutationResult:mutationResult]; + + NSDictionary *expectedData = + @{ @"foo" : @{@"bar" : _timestamp.approximateDateValue}, + @"baz" : @"baz-value" }; + XCTAssertEqualObjects(transformedDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO)); +} + +- (void)testDeleteDeletes { + NSDictionary *docData = @{@"foo" : @"bar"}; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *mutation = FSTTestDeleteMutation(@"collection/key"); + FSTMaybeDocument *result = + [mutation applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; + XCTAssertEqualObjects(result, FSTTestDeletedDoc(@"collection/key", 0)); +} + +- (void)testSetWithMutationResult { + NSDictionary *docData = @{@"foo" : @"bar"}; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *set = FSTTestSetMutation(@"collection/key", @{@"foo" : @"new-bar"}); + FSTMutationResult *mutationResult = + [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(4) transformResults:nil]; + FSTMaybeDocument *setDoc = [set applyTo:baseDoc + baseDocument:baseDoc + localWriteTime:_timestamp + mutationResult:mutationResult]; + + NSDictionary *expectedData = @{@"foo" : @"new-bar"}; + XCTAssertEqualObjects(setDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO)); +} + +- (void)testPatchWithMutationResult { + NSDictionary *docData = @{@"foo" : @"bar"}; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *patch = FSTTestPatchMutation(@"collection/key", @{@"foo" : @"new-bar"}, nil); + FSTMutationResult *mutationResult = + [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(4) transformResults:nil]; + FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc + baseDocument:baseDoc + localWriteTime:_timestamp + mutationResult:mutationResult]; + + NSDictionary *expectedData = @{@"foo" : @"new-bar"}; + XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO)); +} + +#define ASSERT_VERSION_TRANSITION(mutation, base, expected) \ + do { \ + FSTMutationResult *mutationResult = \ + [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(0) transformResults:nil]; \ + FSTMaybeDocument *actual = [mutation applyTo:base \ + baseDocument:base \ + localWriteTime:_timestamp \ + mutationResult:mutationResult]; \ + XCTAssertEqualObjects(actual, expected); \ + } while (0); + +/** + * Tests the transition table documented in FSTMutation.h. + */ +- (void)testTransitions { + FSTDocument *docV0 = FSTTestDoc(@"collection/key", 0, @{}, NO); + FSTDeletedDocument *deletedV0 = FSTTestDeletedDoc(@"collection/key", 0); + + FSTDocument *docV3 = FSTTestDoc(@"collection/key", 3, @{}, NO); + FSTDeletedDocument *deletedV3 = FSTTestDeletedDoc(@"collection/key", 3); + + FSTMutation *setMutation = FSTTestSetMutation(@"collection/key", @{}); + FSTMutation *patchMutation = FSTTestPatchMutation(@"collection/key", @{}, nil); + FSTMutation *deleteMutation = FSTTestDeleteMutation(@"collection/key"); + + ASSERT_VERSION_TRANSITION(setMutation, docV3, docV3); + ASSERT_VERSION_TRANSITION(setMutation, deletedV3, docV0); + ASSERT_VERSION_TRANSITION(setMutation, nil, docV0); + + ASSERT_VERSION_TRANSITION(patchMutation, docV3, docV3); + ASSERT_VERSION_TRANSITION(patchMutation, deletedV3, deletedV3); + ASSERT_VERSION_TRANSITION(patchMutation, nil, nil); + + ASSERT_VERSION_TRANSITION(deleteMutation, docV3, deletedV0); + ASSERT_VERSION_TRANSITION(deleteMutation, deletedV3, deletedV0); + ASSERT_VERSION_TRANSITION(deleteMutation, nil, deletedV0); +} + +#undef ASSERT_TRANSITION + +@end diff --git a/Firestore/Example/Tests/Model/FSTPathTests.m b/Firestore/Example/Tests/Model/FSTPathTests.m deleted file mode 100644 index b8529e5..0000000 --- a/Firestore/Example/Tests/Model/FSTPathTests.m +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Example/Tests/Util/FSTHelpers.h" -#import "Firestore/Source/Model/FSTPath.h" - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTFieldPathTests : XCTestCase -@end - -@implementation FSTFieldPathTests - -- (void)testConstructor { - FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; - XCTAssertEqual(3, path.length); -} - -- (void)testIndexing { - FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; - XCTAssertEqualObjects(@"rooms", path.firstSegment); - XCTAssertEqualObjects(@"rooms", [path segmentAtIndex:0]); - XCTAssertEqualObjects(@"rooms", path[0]); - - XCTAssertEqualObjects(@"Eros", [path segmentAtIndex:1]); - XCTAssertEqualObjects(@"Eros", path[1]); - - XCTAssertEqualObjects(@"messages", [path segmentAtIndex:2]); - XCTAssertEqualObjects(@"messages", path[2]); - XCTAssertEqualObjects(@"messages", path.lastSegment); -} - -- (void)testPathByRemovingFirstSegment { - FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; - FSTFieldPath *same = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; - FSTFieldPath *second = [FSTFieldPath pathWithSegments:@[ @"Eros", @"messages" ]]; - FSTFieldPath *third = [FSTFieldPath pathWithSegments:@[ @"messages" ]]; - FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]]; - - XCTAssertEqualObjects(second, path.pathByRemovingFirstSegment); - XCTAssertEqualObjects(third, path.pathByRemovingFirstSegment.pathByRemovingFirstSegment); - XCTAssertEqualObjects( - empty, path.pathByRemovingFirstSegment.pathByRemovingFirstSegment.pathByRemovingFirstSegment); - // unmodified original - XCTAssertEqualObjects(same, path); -} - -- (void)testPathByRemovingLastSegment { - FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; - FSTFieldPath *same = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; - FSTFieldPath *second = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros" ]]; - FSTFieldPath *third = [FSTFieldPath pathWithSegments:@[ @"rooms" ]]; - FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]]; - - XCTAssertEqualObjects(second, path.pathByRemovingLastSegment); - XCTAssertEqualObjects(third, path.pathByRemovingLastSegment.pathByRemovingLastSegment); - XCTAssertEqualObjects( - empty, path.pathByRemovingLastSegment.pathByRemovingLastSegment.pathByRemovingLastSegment); - // unmodified original - XCTAssertEqualObjects(same, path); -} - -- (void)testPathByAppendingSegment { - FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms" ]]; - FSTFieldPath *rooms = [FSTFieldPath pathWithSegments:@[ @"rooms" ]]; - FSTFieldPath *roomsEros = [FSTFieldPath pathWithSegments:@[ @"rooms", @"eros" ]]; - FSTFieldPath *roomsEros1 = [FSTFieldPath pathWithSegments:@[ @"rooms", @"eros", @"1" ]]; - - XCTAssertEqualObjects(roomsEros, [path pathByAppendingSegment:@"eros"]); - XCTAssertEqualObjects(roomsEros1, - [[path pathByAppendingSegment:@"eros"] pathByAppendingSegment:@"1"]); - // unmodified original - XCTAssertEqualObjects(rooms, path); - - FSTFieldPath *sub = [FSTTestFieldPath(@"rooms.eros.1") pathByRemovingFirstSegment]; - FSTFieldPath *appended = [sub pathByAppendingSegment:@"2"]; - XCTAssertEqualObjects(appended, FSTTestFieldPath(@"eros.1.2")); -} - -- (void)testPathComparison { - FSTFieldPath *path1 = [FSTFieldPath pathWithSegments:@[ @"a", @"b", @"c" ]]; - FSTFieldPath *path2 = [FSTFieldPath pathWithSegments:@[ @"a", @"b", @"c" ]]; - FSTFieldPath *path3 = [FSTFieldPath pathWithSegments:@[ @"x", @"y", @"z" ]]; - XCTAssertTrue([path1 isEqual:path2]); - XCTAssertFalse([path1 isEqual:path3]); - - FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]]; - FSTFieldPath *a = [FSTFieldPath pathWithSegments:@[ @"a" ]]; - FSTFieldPath *b = [FSTFieldPath pathWithSegments:@[ @"b" ]]; - FSTFieldPath *ab = [FSTFieldPath pathWithSegments:@[ @"a", @"b" ]]; - - XCTAssertEqual(NSOrderedAscending, [empty compare:a]); - XCTAssertEqual(NSOrderedAscending, [a compare:b]); - XCTAssertEqual(NSOrderedAscending, [a compare:ab]); - - XCTAssertEqual(NSOrderedDescending, [a compare:empty]); - XCTAssertEqual(NSOrderedDescending, [b compare:a]); - XCTAssertEqual(NSOrderedDescending, [ab compare:a]); -} - -- (void)testIsPrefixOfPath { - FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]]; - FSTFieldPath *a = [FSTFieldPath pathWithSegments:@[ @"a" ]]; - FSTFieldPath *ab = [FSTFieldPath pathWithSegments:@[ @"a", @"b" ]]; - FSTFieldPath *abc = [FSTFieldPath pathWithSegments:@[ @"a", @"b", @"c" ]]; - FSTFieldPath *b = [FSTFieldPath pathWithSegments:@[ @"b" ]]; - FSTFieldPath *ba = [FSTFieldPath pathWithSegments:@[ @"b", @"a" ]]; - - XCTAssertTrue([empty isPrefixOfPath:a]); - XCTAssertTrue([empty isPrefixOfPath:ab]); - XCTAssertTrue([empty isPrefixOfPath:abc]); - XCTAssertTrue([empty isPrefixOfPath:empty]); - XCTAssertTrue([empty isPrefixOfPath:b]); - XCTAssertTrue([empty isPrefixOfPath:ba]); - - XCTAssertTrue([a isPrefixOfPath:a]); - XCTAssertTrue([a isPrefixOfPath:ab]); - XCTAssertTrue([a isPrefixOfPath:abc]); - XCTAssertFalse([a isPrefixOfPath:empty]); - XCTAssertFalse([a isPrefixOfPath:b]); - XCTAssertFalse([a isPrefixOfPath:ba]); - - XCTAssertFalse([ab isPrefixOfPath:a]); - XCTAssertTrue([ab isPrefixOfPath:ab]); - XCTAssertTrue([ab isPrefixOfPath:abc]); - XCTAssertFalse([ab isPrefixOfPath:empty]); - XCTAssertFalse([ab isPrefixOfPath:b]); - XCTAssertFalse([ab isPrefixOfPath:ba]); - - XCTAssertFalse([abc isPrefixOfPath:a]); - XCTAssertFalse([abc isPrefixOfPath:ab]); - XCTAssertTrue([abc isPrefixOfPath:abc]); - XCTAssertFalse([abc isPrefixOfPath:empty]); - XCTAssertFalse([abc isPrefixOfPath:b]); - XCTAssertFalse([abc isPrefixOfPath:ba]); -} - -- (void)testInvalidPaths { - XCTAssertThrows(FSTTestFieldPath(@"")); - XCTAssertThrows(FSTTestFieldPath(@".")); - XCTAssertThrows(FSTTestFieldPath(@".foo")); - XCTAssertThrows(FSTTestFieldPath(@"foo.")); - XCTAssertThrows(FSTTestFieldPath(@"foo..bar")); -} - -#define ASSERT_ROUND_TRIP(str, segments) \ - do { \ - FSTFieldPath *path = [FSTFieldPath pathWithServerFormat:str]; \ - XCTAssertEqual([path length], segments); \ - NSString *canonical = [path canonicalString]; \ - XCTAssertEqualObjects(canonical, str); \ - } while (0); - -- (void)testCanonicalString { - ASSERT_ROUND_TRIP(@"foo", 1); - ASSERT_ROUND_TRIP(@"foo.bar", 2); - ASSERT_ROUND_TRIP(@"foo.bar.baz", 3); - ASSERT_ROUND_TRIP(@"`.foo\\\\`", 1); - ASSERT_ROUND_TRIP(@"`.foo\\\\`.`.foo`", 2); - ASSERT_ROUND_TRIP(@"foo.`\\``.bar", 3); -} - -#undef ASSERT_ROUND_TRIP - -- (void)testCanonicalStringOfSubstring { - FSTFieldPath *path = [FSTFieldPath pathWithServerFormat:@"foo.bar.baz"]; - XCTAssertEqualObjects([path canonicalString], @"foo.bar.baz"); - - FSTFieldPath *pathTail = [path pathByRemovingFirstSegment]; - XCTAssertEqualObjects([pathTail canonicalString], @"bar.baz"); - - FSTFieldPath *pathHead = [path pathByRemovingLastSegment]; - XCTAssertEqualObjects([pathHead canonicalString], @"foo.bar"); - - XCTAssertEqualObjects([[pathTail pathByRemovingLastSegment] canonicalString], @"bar"); - XCTAssertEqualObjects([[pathHead pathByRemovingFirstSegment] canonicalString], @"bar"); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTPathTests.mm b/Firestore/Example/Tests/Model/FSTPathTests.mm new file mode 100644 index 0000000..b8529e5 --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTPathTests.mm @@ -0,0 +1,196 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Example/Tests/Util/FSTHelpers.h" +#import "Firestore/Source/Model/FSTPath.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTFieldPathTests : XCTestCase +@end + +@implementation FSTFieldPathTests + +- (void)testConstructor { + FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; + XCTAssertEqual(3, path.length); +} + +- (void)testIndexing { + FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; + XCTAssertEqualObjects(@"rooms", path.firstSegment); + XCTAssertEqualObjects(@"rooms", [path segmentAtIndex:0]); + XCTAssertEqualObjects(@"rooms", path[0]); + + XCTAssertEqualObjects(@"Eros", [path segmentAtIndex:1]); + XCTAssertEqualObjects(@"Eros", path[1]); + + XCTAssertEqualObjects(@"messages", [path segmentAtIndex:2]); + XCTAssertEqualObjects(@"messages", path[2]); + XCTAssertEqualObjects(@"messages", path.lastSegment); +} + +- (void)testPathByRemovingFirstSegment { + FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; + FSTFieldPath *same = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; + FSTFieldPath *second = [FSTFieldPath pathWithSegments:@[ @"Eros", @"messages" ]]; + FSTFieldPath *third = [FSTFieldPath pathWithSegments:@[ @"messages" ]]; + FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]]; + + XCTAssertEqualObjects(second, path.pathByRemovingFirstSegment); + XCTAssertEqualObjects(third, path.pathByRemovingFirstSegment.pathByRemovingFirstSegment); + XCTAssertEqualObjects( + empty, path.pathByRemovingFirstSegment.pathByRemovingFirstSegment.pathByRemovingFirstSegment); + // unmodified original + XCTAssertEqualObjects(same, path); +} + +- (void)testPathByRemovingLastSegment { + FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; + FSTFieldPath *same = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; + FSTFieldPath *second = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros" ]]; + FSTFieldPath *third = [FSTFieldPath pathWithSegments:@[ @"rooms" ]]; + FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]]; + + XCTAssertEqualObjects(second, path.pathByRemovingLastSegment); + XCTAssertEqualObjects(third, path.pathByRemovingLastSegment.pathByRemovingLastSegment); + XCTAssertEqualObjects( + empty, path.pathByRemovingLastSegment.pathByRemovingLastSegment.pathByRemovingLastSegment); + // unmodified original + XCTAssertEqualObjects(same, path); +} + +- (void)testPathByAppendingSegment { + FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms" ]]; + FSTFieldPath *rooms = [FSTFieldPath pathWithSegments:@[ @"rooms" ]]; + FSTFieldPath *roomsEros = [FSTFieldPath pathWithSegments:@[ @"rooms", @"eros" ]]; + FSTFieldPath *roomsEros1 = [FSTFieldPath pathWithSegments:@[ @"rooms", @"eros", @"1" ]]; + + XCTAssertEqualObjects(roomsEros, [path pathByAppendingSegment:@"eros"]); + XCTAssertEqualObjects(roomsEros1, + [[path pathByAppendingSegment:@"eros"] pathByAppendingSegment:@"1"]); + // unmodified original + XCTAssertEqualObjects(rooms, path); + + FSTFieldPath *sub = [FSTTestFieldPath(@"rooms.eros.1") pathByRemovingFirstSegment]; + FSTFieldPath *appended = [sub pathByAppendingSegment:@"2"]; + XCTAssertEqualObjects(appended, FSTTestFieldPath(@"eros.1.2")); +} + +- (void)testPathComparison { + FSTFieldPath *path1 = [FSTFieldPath pathWithSegments:@[ @"a", @"b", @"c" ]]; + FSTFieldPath *path2 = [FSTFieldPath pathWithSegments:@[ @"a", @"b", @"c" ]]; + FSTFieldPath *path3 = [FSTFieldPath pathWithSegments:@[ @"x", @"y", @"z" ]]; + XCTAssertTrue([path1 isEqual:path2]); + XCTAssertFalse([path1 isEqual:path3]); + + FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]]; + FSTFieldPath *a = [FSTFieldPath pathWithSegments:@[ @"a" ]]; + FSTFieldPath *b = [FSTFieldPath pathWithSegments:@[ @"b" ]]; + FSTFieldPath *ab = [FSTFieldPath pathWithSegments:@[ @"a", @"b" ]]; + + XCTAssertEqual(NSOrderedAscending, [empty compare:a]); + XCTAssertEqual(NSOrderedAscending, [a compare:b]); + XCTAssertEqual(NSOrderedAscending, [a compare:ab]); + + XCTAssertEqual(NSOrderedDescending, [a compare:empty]); + XCTAssertEqual(NSOrderedDescending, [b compare:a]); + XCTAssertEqual(NSOrderedDescending, [ab compare:a]); +} + +- (void)testIsPrefixOfPath { + FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]]; + FSTFieldPath *a = [FSTFieldPath pathWithSegments:@[ @"a" ]]; + FSTFieldPath *ab = [FSTFieldPath pathWithSegments:@[ @"a", @"b" ]]; + FSTFieldPath *abc = [FSTFieldPath pathWithSegments:@[ @"a", @"b", @"c" ]]; + FSTFieldPath *b = [FSTFieldPath pathWithSegments:@[ @"b" ]]; + FSTFieldPath *ba = [FSTFieldPath pathWithSegments:@[ @"b", @"a" ]]; + + XCTAssertTrue([empty isPrefixOfPath:a]); + XCTAssertTrue([empty isPrefixOfPath:ab]); + XCTAssertTrue([empty isPrefixOfPath:abc]); + XCTAssertTrue([empty isPrefixOfPath:empty]); + XCTAssertTrue([empty isPrefixOfPath:b]); + XCTAssertTrue([empty isPrefixOfPath:ba]); + + XCTAssertTrue([a isPrefixOfPath:a]); + XCTAssertTrue([a isPrefixOfPath:ab]); + XCTAssertTrue([a isPrefixOfPath:abc]); + XCTAssertFalse([a isPrefixOfPath:empty]); + XCTAssertFalse([a isPrefixOfPath:b]); + XCTAssertFalse([a isPrefixOfPath:ba]); + + XCTAssertFalse([ab isPrefixOfPath:a]); + XCTAssertTrue([ab isPrefixOfPath:ab]); + XCTAssertTrue([ab isPrefixOfPath:abc]); + XCTAssertFalse([ab isPrefixOfPath:empty]); + XCTAssertFalse([ab isPrefixOfPath:b]); + XCTAssertFalse([ab isPrefixOfPath:ba]); + + XCTAssertFalse([abc isPrefixOfPath:a]); + XCTAssertFalse([abc isPrefixOfPath:ab]); + XCTAssertTrue([abc isPrefixOfPath:abc]); + XCTAssertFalse([abc isPrefixOfPath:empty]); + XCTAssertFalse([abc isPrefixOfPath:b]); + XCTAssertFalse([abc isPrefixOfPath:ba]); +} + +- (void)testInvalidPaths { + XCTAssertThrows(FSTTestFieldPath(@"")); + XCTAssertThrows(FSTTestFieldPath(@".")); + XCTAssertThrows(FSTTestFieldPath(@".foo")); + XCTAssertThrows(FSTTestFieldPath(@"foo.")); + XCTAssertThrows(FSTTestFieldPath(@"foo..bar")); +} + +#define ASSERT_ROUND_TRIP(str, segments) \ + do { \ + FSTFieldPath *path = [FSTFieldPath pathWithServerFormat:str]; \ + XCTAssertEqual([path length], segments); \ + NSString *canonical = [path canonicalString]; \ + XCTAssertEqualObjects(canonical, str); \ + } while (0); + +- (void)testCanonicalString { + ASSERT_ROUND_TRIP(@"foo", 1); + ASSERT_ROUND_TRIP(@"foo.bar", 2); + ASSERT_ROUND_TRIP(@"foo.bar.baz", 3); + ASSERT_ROUND_TRIP(@"`.foo\\\\`", 1); + ASSERT_ROUND_TRIP(@"`.foo\\\\`.`.foo`", 2); + ASSERT_ROUND_TRIP(@"foo.`\\``.bar", 3); +} + +#undef ASSERT_ROUND_TRIP + +- (void)testCanonicalStringOfSubstring { + FSTFieldPath *path = [FSTFieldPath pathWithServerFormat:@"foo.bar.baz"]; + XCTAssertEqualObjects([path canonicalString], @"foo.bar.baz"); + + FSTFieldPath *pathTail = [path pathByRemovingFirstSegment]; + XCTAssertEqualObjects([pathTail canonicalString], @"bar.baz"); + + FSTFieldPath *pathHead = [path pathByRemovingLastSegment]; + XCTAssertEqualObjects([pathHead canonicalString], @"foo.bar"); + + XCTAssertEqualObjects([[pathTail pathByRemovingLastSegment] canonicalString], @"bar"); + XCTAssertEqualObjects([[pathHead pathByRemovingFirstSegment] canonicalString], @"bar"); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTDatastoreTests.m b/Firestore/Example/Tests/Remote/FSTDatastoreTests.m deleted file mode 100644 index 6d6e912..0000000 --- a/Firestore/Example/Tests/Remote/FSTDatastoreTests.m +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Remote/FSTDatastore.h" - -#import -#import -#import - -@interface FSTDatastoreTests : XCTestCase -@end - -@implementation FSTDatastoreTests - -- (void)testIsPermanentWriteError { - // From GRPCCall -cancel - NSError *error = [NSError errorWithDomain:FIRFirestoreErrorDomain - code:FIRFirestoreErrorCodeCancelled - userInfo:@{NSLocalizedDescriptionKey : @"Canceled by app"}]; - XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); - - // From GRPCCall -startNextRead - error = - [NSError errorWithDomain:FIRFirestoreErrorDomain - code:FIRFirestoreErrorCodeResourceExhausted - userInfo:@{ - NSLocalizedDescriptionKey : - @"Client does not have enough memory to hold the server response." - }]; - XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); - - // From GRPCCall -startWithWriteable - error = [NSError errorWithDomain:FIRFirestoreErrorDomain - code:FIRFirestoreErrorCodeUnavailable - userInfo:@{NSLocalizedDescriptionKey : @"Connectivity lost."}]; - XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); - - // User info doesn't matter: - error = [NSError errorWithDomain:FIRFirestoreErrorDomain - code:FIRFirestoreErrorCodeUnavailable - userInfo:nil]; - XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); -} - -@end diff --git a/Firestore/Example/Tests/Remote/FSTDatastoreTests.mm b/Firestore/Example/Tests/Remote/FSTDatastoreTests.mm new file mode 100644 index 0000000..6d6e912 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTDatastoreTests.mm @@ -0,0 +1,58 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Remote/FSTDatastore.h" + +#import +#import +#import + +@interface FSTDatastoreTests : XCTestCase +@end + +@implementation FSTDatastoreTests + +- (void)testIsPermanentWriteError { + // From GRPCCall -cancel + NSError *error = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeCancelled + userInfo:@{NSLocalizedDescriptionKey : @"Canceled by app"}]; + XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); + + // From GRPCCall -startNextRead + error = + [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeResourceExhausted + userInfo:@{ + NSLocalizedDescriptionKey : + @"Client does not have enough memory to hold the server response." + }]; + XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); + + // From GRPCCall -startWithWriteable + error = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnavailable + userInfo:@{NSLocalizedDescriptionKey : @"Connectivity lost."}]; + XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); + + // User info doesn't matter: + error = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnavailable + userInfo:nil]; + XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); +} + +@end diff --git a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.m b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.m deleted file mode 100644 index a947eb4..0000000 --- a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.m +++ /dev/null @@ -1,556 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Remote/FSTRemoteEvent.h" - -#import - -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Remote/FSTExistenceFilter.h" -#import "Firestore/Source/Remote/FSTWatchChange.h" - -#import "Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h" -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTRemoteEventTests : XCTestCase -@end - -@implementation FSTRemoteEventTests { - NSData *_resumeToken1; - NSMutableDictionary *_noPendingResponses; -} - -- (void)setUp { - _resumeToken1 = [@"resume1" dataUsingEncoding:NSUTF8StringEncoding]; - _noPendingResponses = [NSMutableDictionary dictionary]; -} - -- (FSTWatchChangeAggregator *)aggregatorWithTargets:(NSArray *)targets - outstanding: - (NSDictionary *)outstanding - changes:(NSArray *)watchChanges { - NSMutableDictionary *listens = [NSMutableDictionary dictionary]; - FSTQueryData *dummyQueryData = [FSTQueryData alloc]; - for (NSNumber *targetID in targets) { - listens[targetID] = dummyQueryData; - } - FSTWatchChangeAggregator *aggregator = - [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:FSTTestVersion(3) - listenTargets:listens - pendingTargetResponses:outstanding]; - [aggregator addWatchChanges:watchChanges]; - return aggregator; -} - -- (void)testWillAccumulateDocumentAddedAndRemovedEvents { - FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); - FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); - - FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2, @3 ] - removedTargetIDs:@[ @4, @5, @6 ] - documentKey:doc1.key - document:doc1]; - - FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @4 ] - removedTargetIDs:@[ @2, @6 ] - documentKey:doc2.key - document:doc2]; - - FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2, @3, @4, @5, @6 ] - outstanding:_noPendingResponses - changes:@[ change1, change2 ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); - XCTAssertEqual(event.documentUpdates.count, 2); - XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); - XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); - - XCTAssertEqual(event.targetChanges.count, 6); - - FSTUpdateMapping *mapping1 = - [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; - XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); - - FSTUpdateMapping *mapping2 = - [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[ doc2 ]]; - XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2); - - FSTUpdateMapping *mapping3 = - [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[]]; - XCTAssertEqualObjects(event.targetChanges[@3].mapping, mapping3); - - FSTUpdateMapping *mapping4 = - [FSTUpdateMapping mappingWithAddedDocuments:@[ doc2 ] removedDocuments:@[ doc1 ]]; - XCTAssertEqualObjects(event.targetChanges[@4].mapping, mapping4); - - FSTUpdateMapping *mapping5 = - [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1 ]]; - XCTAssertEqualObjects(event.targetChanges[@5].mapping, mapping5); - - FSTUpdateMapping *mapping6 = - [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1, doc2 ]]; - XCTAssertEqualObjects(event.targetChanges[@6].mapping, mapping6); -} - -- (void)testWillIgnoreEventsForPendingTargets { - FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); - FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); - - FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] - removedTargetIDs:@[] - documentKey:doc1.key - document:doc1]; - - FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved - targetIDs:@[ @1 ] - cause:nil]; - - FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded - targetIDs:@[ @1 ] - cause:nil]; - - FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] - removedTargetIDs:@[] - documentKey:doc2.key - document:doc2]; - - // We're waiting for the unwatch and watch ack - NSDictionary *pendingResponses = @{ @1 : @2 }; - - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1 ] - outstanding:pendingResponses - changes:@[ change1, change2, change3, change4 ]]; - FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); - // doc1 is ignored because it was part of an inactive target, but doc2 is in the changes - // because it become active. - XCTAssertEqual(event.documentUpdates.count, 1); - XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); - - XCTAssertEqual(event.targetChanges.count, 1); -} - -- (void)testWillIgnoreEventsForRemovedTargets { - FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); - - FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] - removedTargetIDs:@[] - documentKey:doc1.key - document:doc1]; - - FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved - targetIDs:@[ @1 ] - cause:nil]; - - // We're waiting for the unwatch ack - NSDictionary *pendingResponses = @{ @1 : @1 }; - - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[] outstanding:pendingResponses changes:@[ change1, change2 ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); - // doc1 is ignored because it was part of an inactive target - XCTAssertEqual(event.documentUpdates.count, 0); - - // Target 1 is ignored because it was removed - XCTAssertEqual(event.targetChanges.count, 0); -} - -- (void)testWillKeepResetMappingEvenWithUpdates { - FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); - FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); - FSTDocument *doc3 = FSTTestDoc(@"docs/3", 3, @{ @"value" : @3 }, NO); - - FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] - removedTargetIDs:@[] - documentKey:doc1.key - document:doc1]; - // Reset stream, ignoring doc1 - FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset - targetIDs:@[ @1 ] - cause:nil]; - - // Add doc2, doc3 - FSTWatchChange *change3 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] - removedTargetIDs:@[] - documentKey:doc2.key - document:doc2]; - FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] - removedTargetIDs:@[] - documentKey:doc3.key - document:doc3]; - - // Remove doc2 again, should not show up in reset mapping - FSTWatchChange *change5 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] - removedTargetIDs:@[ @1 ] - documentKey:doc2.key - document:doc2]; - - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1 ] - outstanding:_noPendingResponses - changes:@[ change1, change2, change3, change4, change5 ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); - XCTAssertEqual(event.documentUpdates.count, 3); - XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); - XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); - XCTAssertEqualObjects(event.documentUpdates[doc3.key], doc3); - - XCTAssertEqual(event.targetChanges.count, 1); - - // Only doc3 is part of the new mapping - FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[ doc3 ]]; - - XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping); -} - -- (void)testWillHandleSingleReset { - // Reset target - FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset - targetIDs:@[ @1 ] - cause:nil]; - - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); - XCTAssertEqual(event.documentUpdates.count, 0); - - XCTAssertEqual(event.targetChanges.count, 1); - - // Reset mapping is empty - FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[]]; - XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping); -} - -- (void)testWillHandleTargetAddAndRemovalInSameBatch { - FSTDocument *doc1a = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); - FSTDocument *doc1b = FSTTestDoc(@"docs/1", 1, @{ @"value" : @2 }, NO); - - FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] - removedTargetIDs:@[ @2 ] - documentKey:doc1a.key - document:doc1a]; - - FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @2 ] - removedTargetIDs:@[ @1 ] - documentKey:doc1b.key - document:doc1b]; - FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2 ] - outstanding:_noPendingResponses - changes:@[ change1, change2 ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); - XCTAssertEqual(event.documentUpdates.count, 1); - XCTAssertEqualObjects(event.documentUpdates[doc1b.key], doc1b); - - XCTAssertEqual(event.targetChanges.count, 2); - - FSTUpdateMapping *mapping1 = - [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1b ]]; - XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); - - FSTUpdateMapping *mapping2 = - [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1b ] removedDocuments:@[]]; - XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2); -} - -- (void)testTargetCurrentChangeWillMarkTheTargetCurrent { - FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent - targetIDs:@[ @1 ] - resumeToken:_resumeToken1]; - - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); - XCTAssertEqual(event.documentUpdates.count, 0); - XCTAssertEqual(event.targetChanges.count, 1); - FSTTargetChange *targetChange = event.targetChanges[@1]; - XCTAssertEqualObjects(targetChange.mapping, [[FSTUpdateMapping alloc] init]); - XCTAssertEqual(targetChange.currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); - XCTAssertEqualObjects(targetChange.resumeToken, _resumeToken1); -} - -- (void)testTargetAddedChangeWillResetPreviousState { - FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); - FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); - - FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @3 ] - removedTargetIDs:@[ @2 ] - documentKey:doc1.key - document:doc1]; - FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent - targetIDs:@[ @1, @2, @3 ] - resumeToken:_resumeToken1]; - FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved - targetIDs:@[ @1 ] - cause:nil]; - FSTWatchChange *change4 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved - targetIDs:@[ @2 ] - cause:nil]; - FSTWatchChange *change5 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded - targetIDs:@[ @1 ] - cause:nil]; - FSTWatchChange *change6 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] - removedTargetIDs:@[ @3 ] - documentKey:doc2.key - document:doc2]; - - NSDictionary *pendingResponses = @{ @1 : @2, @2 : @1 }; - - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1, @3 ] - outstanding:pendingResponses - changes:@[ change1, change2, change3, change4, change5, change6 ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); - XCTAssertEqual(event.documentUpdates.count, 2); - XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); - XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); - - // target 1 and 3 are affected (1 because of re-add), target 2 is not because of remove - XCTAssertEqual(event.targetChanges.count, 2); - - // doc1 was before the remove, so it does not show up in the mapping - FSTUpdateMapping *mapping1 = - [FSTUpdateMapping mappingWithAddedDocuments:@[ doc2 ] removedDocuments:@[]]; - XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); - // Current was before the remove - XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateNone); - - // Doc1 was before the remove - FSTUpdateMapping *mapping3 = - [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[ doc2 ]]; - XCTAssertEqualObjects(event.targetChanges[@3].mapping, mapping3); - // Current was before the remove - XCTAssertEqual(event.targetChanges[@3].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); - XCTAssertEqualObjects(event.targetChanges[@3].resumeToken, _resumeToken1); -} - -- (void)testNoChangeWillStillMarkTheAffectedTargets { - FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange - targetIDs:@[ @1 ] - resumeToken:_resumeToken1]; - - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); - XCTAssertEqual(event.documentUpdates.count, 0); - XCTAssertEqual(event.targetChanges.count, 1); - XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTUpdateMapping alloc] init]); - XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateNone); - XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); -} - -- (void)testExistenceFiltersWillReplacePreviousExistenceFilters { - FSTExistenceFilter *filter1 = [FSTExistenceFilter filterWithCount:1]; - FSTExistenceFilter *filter2 = [FSTExistenceFilter filterWithCount:2]; - FSTWatchChange *change1 = [FSTExistenceFilterWatchChange changeWithFilter:filter1 targetID:1]; - FSTWatchChange *change2 = [FSTExistenceFilterWatchChange changeWithFilter:filter1 targetID:2]; - // replace filter1 for target 2 - FSTWatchChange *change3 = [FSTExistenceFilterWatchChange changeWithFilter:filter2 targetID:2]; - - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1, @2 ] - outstanding:_noPendingResponses - changes:@[ change1, change2, change3 ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); - XCTAssertEqual(event.documentUpdates.count, 0); - XCTAssertEqual(event.targetChanges.count, 0); - XCTAssertEqual(aggregator.existenceFilters.count, 2); - XCTAssertEqual(aggregator.existenceFilters[@1], filter1); - XCTAssertEqual(aggregator.existenceFilters[@2], filter2); -} - -- (void)testExistenceFilterMismatchResetsTarget { - FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); - FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); - - FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] - removedTargetIDs:@[] - documentKey:doc1.key - document:doc1]; - - FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] - removedTargetIDs:@[] - documentKey:doc2.key - document:doc2]; - - FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent - targetIDs:@[ @1 ] - resumeToken:_resumeToken1]; - - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1 ] - outstanding:_noPendingResponses - changes:@[ change1, change2, change3 ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); - XCTAssertEqual(event.documentUpdates.count, 2); - XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); - XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); - - XCTAssertEqual(event.targetChanges.count, 1); - - FSTUpdateMapping *mapping1 = - [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; - XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); - XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); - XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); - XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); - - [event handleExistenceFilterMismatchForTargetID:@1]; - - // Mapping is reset - XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTResetMapping alloc] init]); - // Reset the resume snapshot - XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(0)); - // Target needs to be set to not current - XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkNotCurrent); - XCTAssertEqual(event.targetChanges[@1].resumeToken.length, 0); -} - -- (void)testDocumentUpdate { - FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); - FSTDeletedDocument *deletedDoc1 = - [FSTDeletedDocument documentWithKey:doc1.key version:FSTTestVersion(3)]; - FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); - FSTDocument *doc3 = FSTTestDoc(@"docs/3", 3, @{ @"value" : @3 }, NO); - - FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] - removedTargetIDs:@[] - documentKey:doc1.key - document:doc1]; - - FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] - removedTargetIDs:@[] - documentKey:doc2.key - document:doc2]; - - FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1 ] - outstanding:_noPendingResponses - changes:@[ change1, change2 ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); - XCTAssertEqual(event.documentUpdates.count, 2); - XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); - XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); - - // Update doc1 - [event addDocumentUpdate:deletedDoc1]; - [event addDocumentUpdate:doc3]; - - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); - XCTAssertEqual(event.documentUpdates.count, 3); - // doc1 is replaced - XCTAssertEqualObjects(event.documentUpdates[doc1.key], deletedDoc1); - // doc2 is untouched - XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); - // doc3 is new - XCTAssertEqualObjects(event.documentUpdates[doc3.key], doc3); - - // Target is unchanged - XCTAssertEqual(event.targetChanges.count, 1); - - FSTUpdateMapping *mapping1 = - [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; - XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); -} - -- (void)testResumeTokensHandledPerTarget { - NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding]; - FSTWatchChange *change1 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent - targetIDs:@[ @1 ] - resumeToken:_resumeToken1]; - FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent - targetIDs:@[ @2 ] - resumeToken:resumeToken2]; - FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2 ] - outstanding:_noPendingResponses - changes:@[ change1, change2 ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqual(event.targetChanges.count, 2); - - FSTUpdateMapping *mapping1 = - [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[]]; - XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); - XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); - XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); - XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); - - XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1); - XCTAssertEqualObjects(event.targetChanges[@2].snapshotVersion, FSTTestVersion(3)); - XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); - XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken2); -} - -- (void)testLastResumeTokenWins { - NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding]; - NSData *resumeToken3 = [@"resume3" dataUsingEncoding:NSUTF8StringEncoding]; - - FSTWatchChange *change1 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent - targetIDs:@[ @1 ] - resumeToken:_resumeToken1]; - FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset - targetIDs:@[ @1 ] - resumeToken:resumeToken2]; - FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset - targetIDs:@[ @2 ] - resumeToken:resumeToken3]; - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1, @2 ] - outstanding:_noPendingResponses - changes:@[ change1, change2, change3 ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqual(event.targetChanges.count, 2); - - FSTResetMapping *mapping1 = [FSTResetMapping mappingWithDocuments:@[]]; - XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); - XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); - XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); - XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, resumeToken2); - - XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1); - XCTAssertEqualObjects(event.targetChanges[@2].snapshotVersion, FSTTestVersion(3)); - XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateNone); - XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken3); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm new file mode 100644 index 0000000..a947eb4 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm @@ -0,0 +1,556 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Remote/FSTRemoteEvent.h" + +#import + +#import "Firestore/Source/Local/FSTQueryData.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Remote/FSTExistenceFilter.h" +#import "Firestore/Source/Remote/FSTWatchChange.h" + +#import "Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h" +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTRemoteEventTests : XCTestCase +@end + +@implementation FSTRemoteEventTests { + NSData *_resumeToken1; + NSMutableDictionary *_noPendingResponses; +} + +- (void)setUp { + _resumeToken1 = [@"resume1" dataUsingEncoding:NSUTF8StringEncoding]; + _noPendingResponses = [NSMutableDictionary dictionary]; +} + +- (FSTWatchChangeAggregator *)aggregatorWithTargets:(NSArray *)targets + outstanding: + (NSDictionary *)outstanding + changes:(NSArray *)watchChanges { + NSMutableDictionary *listens = [NSMutableDictionary dictionary]; + FSTQueryData *dummyQueryData = [FSTQueryData alloc]; + for (NSNumber *targetID in targets) { + listens[targetID] = dummyQueryData; + } + FSTWatchChangeAggregator *aggregator = + [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:FSTTestVersion(3) + listenTargets:listens + pendingTargetResponses:outstanding]; + [aggregator addWatchChanges:watchChanges]; + return aggregator; +} + +- (void)testWillAccumulateDocumentAddedAndRemovedEvents { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2, @3 ] + removedTargetIDs:@[ @4, @5, @6 ] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @4 ] + removedTargetIDs:@[ @2, @6 ] + documentKey:doc2.key + document:doc2]; + + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2, @3, @4, @5, @6 ] + outstanding:_noPendingResponses + changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 2); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + XCTAssertEqual(event.targetChanges.count, 6); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + + FSTUpdateMapping *mapping2 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[ doc2 ]]; + XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2); + + FSTUpdateMapping *mapping3 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@3].mapping, mapping3); + + FSTUpdateMapping *mapping4 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc2 ] removedDocuments:@[ doc1 ]]; + XCTAssertEqualObjects(event.targetChanges[@4].mapping, mapping4); + + FSTUpdateMapping *mapping5 = + [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1 ]]; + XCTAssertEqualObjects(event.targetChanges[@5].mapping, mapping5); + + FSTUpdateMapping *mapping6 = + [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1, doc2 ]]; + XCTAssertEqualObjects(event.targetChanges[@6].mapping, mapping6); +} + +- (void)testWillIgnoreEventsForPendingTargets { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @1 ] + cause:nil]; + + FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded + targetIDs:@[ @1 ] + cause:nil]; + + FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc2.key + document:doc2]; + + // We're waiting for the unwatch and watch ack + NSDictionary *pendingResponses = @{ @1 : @2 }; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] + outstanding:pendingResponses + changes:@[ change1, change2, change3, change4 ]]; + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + // doc1 is ignored because it was part of an inactive target, but doc2 is in the changes + // because it become active. + XCTAssertEqual(event.documentUpdates.count, 1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + XCTAssertEqual(event.targetChanges.count, 1); +} + +- (void)testWillIgnoreEventsForRemovedTargets { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @1 ] + cause:nil]; + + // We're waiting for the unwatch ack + NSDictionary *pendingResponses = @{ @1 : @1 }; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[] outstanding:pendingResponses changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + // doc1 is ignored because it was part of an inactive target + XCTAssertEqual(event.documentUpdates.count, 0); + + // Target 1 is ignored because it was removed + XCTAssertEqual(event.targetChanges.count, 0); +} + +- (void)testWillKeepResetMappingEvenWithUpdates { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"docs/3", 3, @{ @"value" : @3 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + // Reset stream, ignoring doc1 + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @1 ] + cause:nil]; + + // Add doc2, doc3 + FSTWatchChange *change3 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc2.key + document:doc2]; + FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc3.key + document:doc3]; + + // Remove doc2 again, should not show up in reset mapping + FSTWatchChange *change5 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:@[ @1 ] + documentKey:doc2.key + document:doc2]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] + outstanding:_noPendingResponses + changes:@[ change1, change2, change3, change4, change5 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 3); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + XCTAssertEqualObjects(event.documentUpdates[doc3.key], doc3); + + XCTAssertEqual(event.targetChanges.count, 1); + + // Only doc3 is part of the new mapping + FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[ doc3 ]]; + + XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping); +} + +- (void)testWillHandleSingleReset { + // Reset target + FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @1 ] + cause:nil]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 0); + + XCTAssertEqual(event.targetChanges.count, 1); + + // Reset mapping is empty + FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping); +} + +- (void)testWillHandleTargetAddAndRemovalInSameBatch { + FSTDocument *doc1a = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc1b = FSTTestDoc(@"docs/1", 1, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[ @2 ] + documentKey:doc1a.key + document:doc1a]; + + FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @2 ] + removedTargetIDs:@[ @1 ] + documentKey:doc1b.key + document:doc1b]; + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2 ] + outstanding:_noPendingResponses + changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 1); + XCTAssertEqualObjects(event.documentUpdates[doc1b.key], doc1b); + + XCTAssertEqual(event.targetChanges.count, 2); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1b ]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + + FSTUpdateMapping *mapping2 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1b ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2); +} + +- (void)testTargetCurrentChangeWillMarkTheTargetCurrent { + FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 0); + XCTAssertEqual(event.targetChanges.count, 1); + FSTTargetChange *targetChange = event.targetChanges[@1]; + XCTAssertEqualObjects(targetChange.mapping, [[FSTUpdateMapping alloc] init]); + XCTAssertEqual(targetChange.currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(targetChange.resumeToken, _resumeToken1); +} + +- (void)testTargetAddedChangeWillResetPreviousState { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @3 ] + removedTargetIDs:@[ @2 ] + documentKey:doc1.key + document:doc1]; + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1, @2, @3 ] + resumeToken:_resumeToken1]; + FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @1 ] + cause:nil]; + FSTWatchChange *change4 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @2 ] + cause:nil]; + FSTWatchChange *change5 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded + targetIDs:@[ @1 ] + cause:nil]; + FSTWatchChange *change6 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[ @3 ] + documentKey:doc2.key + document:doc2]; + + NSDictionary *pendingResponses = @{ @1 : @2, @2 : @1 }; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1, @3 ] + outstanding:pendingResponses + changes:@[ change1, change2, change3, change4, change5, change6 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 2); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + // target 1 and 3 are affected (1 because of re-add), target 2 is not because of remove + XCTAssertEqual(event.targetChanges.count, 2); + + // doc1 was before the remove, so it does not show up in the mapping + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc2 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + // Current was before the remove + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateNone); + + // Doc1 was before the remove + FSTUpdateMapping *mapping3 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[ doc2 ]]; + XCTAssertEqualObjects(event.targetChanges[@3].mapping, mapping3); + // Current was before the remove + XCTAssertEqual(event.targetChanges[@3].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@3].resumeToken, _resumeToken1); +} + +- (void)testNoChangeWillStillMarkTheAffectedTargets { + FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 0); + XCTAssertEqual(event.targetChanges.count, 1); + XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTUpdateMapping alloc] init]); + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateNone); + XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); +} + +- (void)testExistenceFiltersWillReplacePreviousExistenceFilters { + FSTExistenceFilter *filter1 = [FSTExistenceFilter filterWithCount:1]; + FSTExistenceFilter *filter2 = [FSTExistenceFilter filterWithCount:2]; + FSTWatchChange *change1 = [FSTExistenceFilterWatchChange changeWithFilter:filter1 targetID:1]; + FSTWatchChange *change2 = [FSTExistenceFilterWatchChange changeWithFilter:filter1 targetID:2]; + // replace filter1 for target 2 + FSTWatchChange *change3 = [FSTExistenceFilterWatchChange changeWithFilter:filter2 targetID:2]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1, @2 ] + outstanding:_noPendingResponses + changes:@[ change1, change2, change3 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 0); + XCTAssertEqual(event.targetChanges.count, 0); + XCTAssertEqual(aggregator.existenceFilters.count, 2); + XCTAssertEqual(aggregator.existenceFilters[@1], filter1); + XCTAssertEqual(aggregator.existenceFilters[@2], filter2); +} + +- (void)testExistenceFilterMismatchResetsTarget { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc2.key + document:doc2]; + + FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] + outstanding:_noPendingResponses + changes:@[ change1, change2, change3 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 2); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + XCTAssertEqual(event.targetChanges.count, 1); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); + + [event handleExistenceFilterMismatchForTargetID:@1]; + + // Mapping is reset + XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTResetMapping alloc] init]); + // Reset the resume snapshot + XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(0)); + // Target needs to be set to not current + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkNotCurrent); + XCTAssertEqual(event.targetChanges[@1].resumeToken.length, 0); +} + +- (void)testDocumentUpdate { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDeletedDocument *deletedDoc1 = + [FSTDeletedDocument documentWithKey:doc1.key version:FSTTestVersion(3)]; + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"docs/3", 3, @{ @"value" : @3 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc2.key + document:doc2]; + + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1 ] + outstanding:_noPendingResponses + changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 2); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + // Update doc1 + [event addDocumentUpdate:deletedDoc1]; + [event addDocumentUpdate:doc3]; + + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 3); + // doc1 is replaced + XCTAssertEqualObjects(event.documentUpdates[doc1.key], deletedDoc1); + // doc2 is untouched + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + // doc3 is new + XCTAssertEqualObjects(event.documentUpdates[doc3.key], doc3); + + // Target is unchanged + XCTAssertEqual(event.targetChanges.count, 1); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); +} + +- (void)testResumeTokensHandledPerTarget { + NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding]; + FSTWatchChange *change1 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @2 ] + resumeToken:resumeToken2]; + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2 ] + outstanding:_noPendingResponses + changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqual(event.targetChanges.count, 2); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); + + XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@2].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken2); +} + +- (void)testLastResumeTokenWins { + NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *resumeToken3 = [@"resume3" dataUsingEncoding:NSUTF8StringEncoding]; + + FSTWatchChange *change1 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @1 ] + resumeToken:resumeToken2]; + FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @2 ] + resumeToken:resumeToken3]; + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1, @2 ] + outstanding:_noPendingResponses + changes:@[ change1, change2, change3 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqual(event.targetChanges.count, 2); + + FSTResetMapping *mapping1 = [FSTResetMapping mappingWithDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, resumeToken2); + + XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@2].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateNone); + XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken3); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m deleted file mode 100644 index 1ea0ff2..0000000 --- a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m +++ /dev/null @@ -1,801 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Remote/FSTSerializerBeta.h" - -#import -#import -#import -#import -#import - -#import "Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h" -#import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" -#import "Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.h" -#import "Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.h" -#import "Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h" -#import "Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.h" -#import "Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.h" -#import "Firestore/Protos/objc/google/rpc/Status.pbobjc.h" -#import "Firestore/Protos/objc/google/type/Latlng.pbobjc.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Core/FSTTimestamp.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTDatabaseID.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Remote/FSTWatchChange.h" - -#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTSerializerBeta (Test) -- (GCFSValue *)encodedNull; -- (GCFSValue *)encodedBool:(BOOL)value; -- (GCFSValue *)encodedDouble:(double)value; -- (GCFSValue *)encodedInteger:(int64_t)value; -- (GCFSValue *)encodedString:(NSString *)value; -- (GCFSValue *)encodedDate:(NSDate *)value; - -- (GCFSDocumentMask *)encodedFieldMask:(FSTFieldMask *)fieldMask; -- (NSMutableArray *)encodedFieldTransforms: - (NSArray *)fieldTransforms; - -- (GCFSStructuredQuery_Filter *)encodedRelationFilter:(FSTRelationFilter *)filter; -@end - -@interface GCFSStructuredQuery_Order (Test) -+ (instancetype)messageWithProperty:(NSString *)property ascending:(BOOL)ascending; -@end - -@implementation GCFSStructuredQuery_Order (Test) - -+ (instancetype)messageWithProperty:(NSString *)property ascending:(BOOL)ascending { - GCFSStructuredQuery_Order *order = [GCFSStructuredQuery_Order message]; - order.field.fieldPath = property; - order.direction = ascending ? GCFSStructuredQuery_Direction_Ascending - : GCFSStructuredQuery_Direction_Descending; - return order; -} -@end - -@interface FSTSerializerBetaTests : XCTestCase -@property(nonatomic, strong) FSTSerializerBeta *serializer; -@end - -@implementation FSTSerializerBetaTests - -- (void)setUp { - FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; - self.serializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseID]; -} - -- (void)testEncodesNull { - FSTFieldValue *model = [FSTNullValue nullValue]; - - GCFSValue *proto = [GCFSValue message]; - proto.nullValue = GPBNullValue_NullValue; - - [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_NullValue]; -} - -- (void)testEncodesBool { - NSArray *examples = @[ @YES, @NO ]; - for (NSNumber *example in examples) { - FSTFieldValue *model = FSTTestFieldValue(example); - - GCFSValue *proto = [GCFSValue message]; - proto.booleanValue = [example boolValue]; - - [self assertRoundTripForModel:model - proto:proto - type:GCFSValue_ValueType_OneOfCase_BooleanValue]; - } -} - -- (void)testEncodesIntegers { - NSArray *examples = @[ @(LLONG_MIN), @(-100), @(-1), @0, @1, @100, @(LLONG_MAX) ]; - for (NSNumber *example in examples) { - FSTFieldValue *model = FSTTestFieldValue(example); - - GCFSValue *proto = [GCFSValue message]; - proto.integerValue = [example longLongValue]; - - [self assertRoundTripForModel:model - proto:proto - type:GCFSValue_ValueType_OneOfCase_IntegerValue]; - } -} - -- (void)testEncodesDoubles { - NSArray *examples = @[ - // normal negative numbers. - @(-INFINITY), @(-DBL_MAX), @(LLONG_MIN * 1.0 - 1.0), @(-2.0), @(-1.1), @(-1.0), @(-DBL_MIN), - - // negative smallest subnormal, zeroes, positive smallest subnormal - @(-0x1.0p-1074), @(-0.0), @(0.0), @(0x1.0p-1074), - - // and the rest - @(DBL_MIN), @0.1, @1.1, @(LLONG_MAX * 1.0), @(DBL_MAX), @(INFINITY), - - // NaN. - @(0.0 / 0.0) - ]; - for (NSNumber *example in examples) { - FSTFieldValue *model = FSTTestFieldValue(example); - - GCFSValue *proto = [GCFSValue message]; - proto.doubleValue = [example doubleValue]; - - [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_DoubleValue]; - } -} - -- (void)testEncodesStrings { - NSArray *examples = @[ - @"", - @"a", - @"abc def", - @"æ", - @"\0\ud7ff\ue000\uffff", - @"(╯°□°)╯︵ ┻━┻", - ]; - for (NSString *example in examples) { - FSTFieldValue *model = FSTTestFieldValue(example); - - GCFSValue *proto = [GCFSValue message]; - proto.stringValue = example; - - [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_StringValue]; - } -} - -- (void)testEncodesDates { - NSDateComponents *dateWithNanos = FSTTestDateComponents(2016, 1, 2, 10, 20, 50); - dateWithNanos.nanosecond = 500000000; - - NSArray *examples = @[ - [[NSCalendar currentCalendar] dateFromComponents:dateWithNanos], - FSTTestDate(2016, 6, 17, 10, 50, 15) - ]; - - GCFSValue *timestamp1 = [GCFSValue message]; - timestamp1.timestampValue.seconds = 1451730050; - timestamp1.timestampValue.nanos = 500000000; - - GCFSValue *timestamp2 = [GCFSValue message]; - timestamp2.timestampValue.seconds = 1466160615; - timestamp2.timestampValue.nanos = 0; - NSArray *expectedTimestamps = @[ timestamp1, timestamp2 ]; - - for (NSUInteger i = 0; i < [examples count]; i++) { - [self assertRoundTripForModel:FSTTestFieldValue(examples[i]) - proto:expectedTimestamps[i] - type:GCFSValue_ValueType_OneOfCase_TimestampValue]; - } -} - -- (void)testEncodesGeoPoints { - NSArray *examples = - @[ FSTTestGeoPoint(0, 0), FSTTestGeoPoint(1.24, 4.56), FSTTestGeoPoint(-90, 180) ]; - for (FIRGeoPoint *example in examples) { - FSTFieldValue *model = FSTTestFieldValue(example); - - GCFSValue *proto = [GCFSValue message]; - proto.geoPointValue = [GTPLatLng message]; - proto.geoPointValue.latitude = example.latitude; - proto.geoPointValue.longitude = example.longitude; - - [self assertRoundTripForModel:model - proto:proto - type:GCFSValue_ValueType_OneOfCase_GeoPointValue]; - } -} - -- (void)testEncodesBlobs { - NSArray *examples = @[ - FSTTestData(-1), - FSTTestData(0, -1), - FSTTestData(0, 1, 2, -1), - FSTTestData(255, -1), - FSTTestData(0, 1, 255, -1), - ]; - for (NSData *example in examples) { - FSTFieldValue *model = FSTTestFieldValue(example); - - GCFSValue *proto = [GCFSValue message]; - proto.bytesValue = example; - - [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_BytesValue]; - } -} - -- (void)testEncodesResourceNames { - FSTDocumentKeyReference *reference = FSTTestRef(@"project", kDefaultDatabaseID, @"foo/bar"); - GCFSValue *proto = [GCFSValue message]; - proto.referenceValue = @"projects/project/databases/(default)/documents/foo/bar"; - - [self assertRoundTripForModel:FSTTestFieldValue(reference) - proto:proto - type:GCFSValue_ValueType_OneOfCase_ReferenceValue]; -} - -- (void)testEncodesArrays { - FSTFieldValue *model = FSTTestFieldValue(@[ @YES, @"foo" ]); - - GCFSValue *proto = [GCFSValue message]; - [proto.arrayValue.valuesArray addObjectsFromArray:@[ - [self.serializer encodedBool:YES], [self.serializer encodedString:@"foo"] - ]]; - - [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_ArrayValue]; -} - -- (void)testEncodesEmptyMap { - FSTFieldValue *model = [FSTObjectValue objectValue]; - - GCFSValue *proto = [GCFSValue message]; - proto.mapValue = [GCFSMapValue message]; - - [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_MapValue]; -} - -- (void)testEncodesNestedObjects { - FSTFieldValue *model = FSTTestFieldValue(@{ - @"b" : @YES, - @"d" : @(DBL_MAX), - @"i" : @1, - @"n" : [NSNull null], - @"s" : @"foo", - @"a" : @[ @2, @"bar", - @{ @"b" : @NO } ], - @"o" : @{ - @"d" : @100, - @"nested" : @{@"e" : @(LLONG_MIN)}, - }, - }); - - GCFSValue *innerObject = [GCFSValue message]; - innerObject.mapValue.fields[@"b"] = [self.serializer encodedBool:NO]; - - GCFSValue *middleArray = [GCFSValue message]; - [middleArray.arrayValue.valuesArray addObjectsFromArray:@[ - [self.serializer encodedInteger:2], [self.serializer encodedString:@"bar"], innerObject - ]]; - - innerObject = [GCFSValue message]; - innerObject.mapValue.fields[@"e"] = [self.serializer encodedInteger:LLONG_MIN]; - - GCFSValue *middleObject = [GCFSValue message]; - [middleObject.mapValue.fields addEntriesFromDictionary:@{ - @"d" : [self.serializer encodedInteger:100], - @"nested" : innerObject - }]; - - GCFSValue *proto = [GCFSValue message]; - [proto.mapValue.fields addEntriesFromDictionary:@{ - @"b" : [self.serializer encodedBool:YES], - @"d" : [self.serializer encodedDouble:DBL_MAX], - @"i" : [self.serializer encodedInteger:1], - @"n" : [self.serializer encodedNull], - @"s" : [self.serializer encodedString:@"foo"], - @"a" : middleArray, - @"o" : middleObject - }]; - - [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_MapValue]; -} - -- (void)assertRoundTripForModel:(FSTFieldValue *)model - proto:(GCFSValue *)value - type:(GCFSValue_ValueType_OneOfCase)type { - GCFSValue *actualProto = [self.serializer encodedFieldValue:model]; - XCTAssertEqual(actualProto.valueTypeOneOfCase, type); - XCTAssertEqualObjects(actualProto, value); - - FSTFieldValue *actualModel = [self.serializer decodedFieldValue:value]; - XCTAssertEqualObjects(actualModel, model); -} - -- (void)testEncodesSetMutation { - FSTSetMutation *mutation = FSTTestSetMutation(@"docs/1", @{ @"a" : @"b", @"num" : @1 }); - GCFSWrite *proto = [GCFSWrite message]; - proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key]; - - [self assertRoundTripForMutation:mutation proto:proto]; -} - -- (void)testEncodesPatchMutation { - FSTPatchMutation *mutation = - FSTTestPatchMutation(@"docs/1", - @{ @"a" : @"b", - @"num" : @1, - @"some.de\\\\ep.th\\ing'" : @2 }, - nil); - GCFSWrite *proto = [GCFSWrite message]; - proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key]; - proto.updateMask = [self.serializer encodedFieldMask:mutation.fieldMask]; - proto.currentDocument.exists = YES; - - [self assertRoundTripForMutation:mutation proto:proto]; -} - -- (void)testEncodesDeleteMutation { - FSTDeleteMutation *mutation = FSTTestDeleteMutation(@"docs/1"); - GCFSWrite *proto = [GCFSWrite message]; - proto.delete_p = @"projects/p/databases/d/documents/docs/1"; - - [self assertRoundTripForMutation:mutation proto:proto]; -} - -- (void)testEncodesTransformMutation { - FSTTransformMutation *mutation = FSTTestTransformMutation(@"docs/1", @[ @"a", @"bar.baz" ]); - GCFSWrite *proto = [GCFSWrite message]; - proto.transform = [GCFSDocumentTransform message]; - proto.transform.document = [self.serializer encodedDocumentKey:mutation.key]; - proto.transform.fieldTransformsArray = - [self.serializer encodedFieldTransforms:mutation.fieldTransforms]; - proto.currentDocument.exists = YES; - - [self assertRoundTripForMutation:mutation proto:proto]; -} - -- (void)testEncodesSetMutationWithPrecondition { - FSTSetMutation *mutation = [[FSTSetMutation alloc] - initWithKey:FSTTestDocKey(@"foo/bar") - value:FSTTestObjectValue( - @{ @"a" : @"b", - @"num" : @1 }) - precondition:[FSTPrecondition preconditionWithUpdateTime:FSTTestVersion(4)]]; - GCFSWrite *proto = [GCFSWrite message]; - proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key]; - proto.currentDocument.updateTime = - [self.serializer encodedTimestamp:[[FSTTimestamp alloc] initWithSeconds:0 nanos:4000]]; - - [self assertRoundTripForMutation:mutation proto:proto]; -} - -- (void)assertRoundTripForMutation:(FSTMutation *)mutation proto:(GCFSWrite *)proto { - GCFSWrite *actualProto = [self.serializer encodedMutation:mutation]; - XCTAssertEqualObjects(actualProto, proto); - - FSTMutation *actualMutation = [self.serializer decodedMutation:proto]; - XCTAssertEqualObjects(actualMutation, mutation); -} - -- (void)testRoundTripSpecialFieldNames { - FSTMutation *set = FSTTestSetMutation(@"collection/key", @{ - @"field" : [NSString stringWithFormat:@"field %d", 1], - @"field.dot" : @2, - @"field\\slash" : @3 - }); - GCFSWrite *encoded = [self.serializer encodedMutation:set]; - FSTMutation *decoded = [self.serializer decodedMutation:encoded]; - XCTAssertEqualObjects(set, decoded); -} - -- (void)testEncodesListenRequestLabels { - FSTQuery *query = FSTTestQuery(@"collection/key"); - FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query - targetID:2 - listenSequenceNumber:3 - purpose:FSTQueryPurposeListen]; - - NSDictionary *result = - [self.serializer encodedListenRequestLabelsForQueryData:queryData]; - XCTAssertNil(result); - - queryData = [[FSTQueryData alloc] initWithQuery:query - targetID:2 - listenSequenceNumber:3 - purpose:FSTQueryPurposeLimboResolution]; - result = [self.serializer encodedListenRequestLabelsForQueryData:queryData]; - XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"limbo-document"}); - - queryData = [[FSTQueryData alloc] initWithQuery:query - targetID:2 - listenSequenceNumber:3 - purpose:FSTQueryPurposeExistenceFilterMismatch]; - result = [self.serializer encodedListenRequestLabelsForQueryData:queryData]; - XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"existence-filter-mismatch"}); -} - -- (void)testEncodesRelationFilter { - FSTRelationFilter *input = FSTTestFilter(@"item.part.top", @"==", @"food"); - GCFSStructuredQuery_Filter *actual = [self.serializer encodedRelationFilter:input]; - - GCFSStructuredQuery_Filter *expected = [GCFSStructuredQuery_Filter message]; - GCFSStructuredQuery_FieldFilter *prop = expected.fieldFilter; - prop.field.fieldPath = @"item.part.top"; - prop.op = GCFSStructuredQuery_FieldFilter_Operator_Equal; - prop.value.stringValue = @"food"; - XCTAssertEqualObjects(actual, expected); -} - -#pragma mark - encodedQuery - -- (void)testEncodesFirstLevelKeyQueries { - FSTQuery *q = FSTTestQuery(@"docs/1"); - FSTQueryData *model = [self queryDataForQuery:q]; - - GCFSTarget *expected = [GCFSTarget message]; - [expected.documents.documentsArray addObject:@"projects/p/databases/d/documents/docs/1"]; - expected.targetId = 1; - - [self assertRoundTripForQueryData:model proto:expected]; -} - -- (void)testEncodesFirstLevelAncestorQueries { - FSTQuery *q = FSTTestQuery(@"messages"); - FSTQueryData *model = [self queryDataForQuery:q]; - - GCFSTarget *expected = [GCFSTarget message]; - expected.query.parent = @"projects/p/databases/d"; - GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; - from.collectionId = @"messages"; - [expected.query.structuredQuery.fromArray addObject:from]; - [expected.query.structuredQuery.orderByArray - addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; - expected.targetId = 1; - - [self assertRoundTripForQueryData:model proto:expected]; -} - -- (void)testEncodesNestedAncestorQueries { - FSTQuery *q = FSTTestQuery(@"rooms/1/messages/10/attachments"); - FSTQueryData *model = [self queryDataForQuery:q]; - - GCFSTarget *expected = [GCFSTarget message]; - expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10"; - GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; - from.collectionId = @"attachments"; - [expected.query.structuredQuery.fromArray addObject:from]; - [expected.query.structuredQuery.orderByArray - addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; - expected.targetId = 1; - - [self assertRoundTripForQueryData:model proto:expected]; -} - -- (void)testEncodesSingleFiltersAtFirstLevelCollections { - FSTQuery *q = [FSTTestQuery(@"docs") queryByAddingFilter:FSTTestFilter(@"prop", @"<", @(42))]; - FSTQueryData *model = [self queryDataForQuery:q]; - - GCFSTarget *expected = [GCFSTarget message]; - expected.query.parent = @"projects/p/databases/d"; - GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; - from.collectionId = @"docs"; - [expected.query.structuredQuery.fromArray addObject:from]; - [expected.query.structuredQuery.orderByArray - addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]]; - [expected.query.structuredQuery.orderByArray - addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; - - GCFSStructuredQuery_FieldFilter *filter = expected.query.structuredQuery.where.fieldFilter; - filter.field.fieldPath = @"prop"; - filter.op = GCFSStructuredQuery_FieldFilter_Operator_LessThan; - filter.value.integerValue = 42; - expected.targetId = 1; - - [self assertRoundTripForQueryData:model proto:expected]; -} - -- (void)testEncodesMultipleFiltersOnDeeperCollections { - FSTQuery *q = [[FSTTestQuery(@"rooms/1/messages/10/attachments") - queryByAddingFilter:FSTTestFilter(@"prop", @">=", @(42))] - queryByAddingFilter:FSTTestFilter(@"author", @"==", @"dimond")]; - FSTQueryData *model = [self queryDataForQuery:q]; - - GCFSTarget *expected = [GCFSTarget message]; - expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10"; - GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; - from.collectionId = @"attachments"; - [expected.query.structuredQuery.fromArray addObject:from]; - - GCFSStructuredQuery_Filter *filter1 = [GCFSStructuredQuery_Filter message]; - GCFSStructuredQuery_FieldFilter *field1 = filter1.fieldFilter; - field1.field.fieldPath = @"prop"; - field1.op = GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual; - field1.value.integerValue = 42; - - GCFSStructuredQuery_Filter *filter2 = [GCFSStructuredQuery_Filter message]; - GCFSStructuredQuery_FieldFilter *field2 = filter2.fieldFilter; - field2.field.fieldPath = @"author"; - field2.op = GCFSStructuredQuery_FieldFilter_Operator_Equal; - field2.value.stringValue = @"dimond"; - - GCFSStructuredQuery_CompositeFilter *composite = - expected.query.structuredQuery.where.compositeFilter; - composite.op = GCFSStructuredQuery_CompositeFilter_Operator_And; - [composite.filtersArray addObject:filter1]; - [composite.filtersArray addObject:filter2]; - - [expected.query.structuredQuery.orderByArray - addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]]; - [expected.query.structuredQuery.orderByArray - addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; - expected.targetId = 1; - - [self assertRoundTripForQueryData:model proto:expected]; -} - -- (void)testEncodesNullFilter { - [self unaryFilterTestWithValue:[NSNull null] - expectedUnaryOperator:GCFSStructuredQuery_UnaryFilter_Operator_IsNull]; -} - -- (void)testEncodesNanFilter { - [self unaryFilterTestWithValue:@(NAN) - expectedUnaryOperator:GCFSStructuredQuery_UnaryFilter_Operator_IsNan]; -} - -- (void)unaryFilterTestWithValue:(id)value - expectedUnaryOperator:(GCFSStructuredQuery_UnaryFilter_Operator) - operator{ - FSTQuery *q = [FSTTestQuery(@"docs") queryByAddingFilter:FSTTestFilter(@"prop", @"==", value)]; - FSTQueryData *model = [self queryDataForQuery:q]; - - GCFSTarget *expected = [GCFSTarget message]; - expected.query.parent = @"projects/p/databases/d"; - GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; - from.collectionId = @"docs"; - [expected.query.structuredQuery.fromArray addObject:from]; - [expected.query.structuredQuery.orderByArray - addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; - - GCFSStructuredQuery_UnaryFilter *filter = expected.query.structuredQuery.where.unaryFilter; - filter.field.fieldPath = @"prop"; - filter.op = operator; - expected.targetId = 1; - - [self assertRoundTripForQueryData:model proto:expected]; -} - -- (void)testEncodesSortOrders { - FSTQuery *q = [FSTTestQuery(@"docs") - queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"prop") - ascending:YES]]; - FSTQueryData *model = [self queryDataForQuery:q]; - - GCFSTarget *expected = [GCFSTarget message]; - expected.query.parent = @"projects/p/databases/d"; - GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; - from.collectionId = @"docs"; - [expected.query.structuredQuery.fromArray addObject:from]; - [expected.query.structuredQuery.orderByArray - addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]]; - [expected.query.structuredQuery.orderByArray - addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; - expected.targetId = 1; - - [self assertRoundTripForQueryData:model proto:expected]; -} - -- (void)testEncodesSortOrdersDescending { - FSTQuery *q = [FSTTestQuery(@"rooms/1/messages/10/attachments") - queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"prop") - ascending:NO]]; - FSTQueryData *model = [self queryDataForQuery:q]; - - GCFSTarget *expected = [GCFSTarget message]; - expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10"; - GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; - from.collectionId = @"attachments"; - [expected.query.structuredQuery.fromArray addObject:from]; - [expected.query.structuredQuery.orderByArray - addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:NO]]; - [expected.query.structuredQuery.orderByArray - addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:NO]]; - expected.targetId = 1; - - [self assertRoundTripForQueryData:model proto:expected]; -} - -- (void)testEncodesLimits { - FSTQuery *q = [FSTTestQuery(@"docs") queryBySettingLimit:26]; - FSTQueryData *model = [self queryDataForQuery:q]; - - GCFSTarget *expected = [GCFSTarget message]; - expected.query.parent = @"projects/p/databases/d"; - GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; - from.collectionId = @"docs"; - [expected.query.structuredQuery.fromArray addObject:from]; - [expected.query.structuredQuery.orderByArray - addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; - expected.query.structuredQuery.limit.value = 26; - expected.targetId = 1; - - [self assertRoundTripForQueryData:model proto:expected]; -} - -- (void)testEncodesResumeTokens { - FSTQuery *q = FSTTestQuery(@"docs"); - FSTQueryData *model = [[FSTQueryData alloc] initWithQuery:q - targetID:1 - listenSequenceNumber:0 - purpose:FSTQueryPurposeListen - snapshotVersion:[FSTSnapshotVersion noVersion] - resumeToken:FSTTestData(1, 2, 3, -1)]; - - GCFSTarget *expected = [GCFSTarget message]; - expected.query.parent = @"projects/p/databases/d"; - GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; - from.collectionId = @"docs"; - [expected.query.structuredQuery.fromArray addObject:from]; - [expected.query.structuredQuery.orderByArray - addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; - expected.targetId = 1; - expected.resumeToken = FSTTestData(1, 2, 3, -1); - - [self assertRoundTripForQueryData:model proto:expected]; -} - -- (FSTQueryData *)queryDataForQuery:(FSTQuery *)query { - return [[FSTQueryData alloc] initWithQuery:query - targetID:1 - listenSequenceNumber:0 - purpose:FSTQueryPurposeListen - snapshotVersion:[FSTSnapshotVersion noVersion] - resumeToken:[NSData data]]; -} - -- (void)assertRoundTripForQueryData:(FSTQueryData *)queryData proto:(GCFSTarget *)proto { - // Verify that the encoded FSTQueryData matches the target. - GCFSTarget *actualProto = [self.serializer encodedTarget:queryData]; - XCTAssertEqualObjects(actualProto, proto); - - // We don't have deserialization logic for full targets since they're not used for RPC - // interaction, but the query deserialization only *is* used for the local store. - FSTQuery *actualModel; - if (proto.targetTypeOneOfCase == GCFSTarget_TargetType_OneOfCase_Query) { - actualModel = [self.serializer decodedQueryFromQueryTarget:proto.query]; - } else { - actualModel = [self.serializer decodedQueryFromDocumentsTarget:proto.documents]; - } - XCTAssertEqualObjects(actualModel, queryData.query); -} - -- (void)testConvertsTargetChangeWithAdded { - FSTWatchChange *expected = - [[FSTWatchTargetChange alloc] initWithState:FSTWatchTargetChangeStateAdded - targetIDs:@[ @1, @4 ] - resumeToken:[NSData data] - cause:nil]; - GCFSListenResponse *listenResponse = [GCFSListenResponse message]; - listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_Add; - [listenResponse.targetChange.targetIdsArray addValue:1]; - [listenResponse.targetChange.targetIdsArray addValue:4]; - FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; - - XCTAssertEqualObjects(actual, expected); -} - -- (void)testConvertsTargetChangeWithRemoved { - FSTWatchChange *expected = [[FSTWatchTargetChange alloc] - initWithState:FSTWatchTargetChangeStateRemoved - targetIDs:@[ @1, @4 ] - resumeToken:FSTTestData(0, 1, 2, -1) - cause:[NSError errorWithDomain:FIRFirestoreErrorDomain - code:FIRFirestoreErrorCodePermissionDenied - userInfo:@{ - NSLocalizedDescriptionKey : @"Error message", - }]]; - GCFSListenResponse *listenResponse = [GCFSListenResponse message]; - listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_Remove; - listenResponse.targetChange.cause.code = FIRFirestoreErrorCodePermissionDenied; - listenResponse.targetChange.cause.message = @"Error message"; - listenResponse.targetChange.resumeToken = FSTTestData(0, 1, 2, -1); - [listenResponse.targetChange.targetIdsArray addValue:1]; - [listenResponse.targetChange.targetIdsArray addValue:4]; - FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; - - XCTAssertEqualObjects(actual, expected); -} - -- (void)testConvertsTargetChangeWithNoChange { - FSTWatchChange *expected = - [[FSTWatchTargetChange alloc] initWithState:FSTWatchTargetChangeStateNoChange - targetIDs:@[ @1, @4 ] - resumeToken:[NSData data] - cause:nil]; - GCFSListenResponse *listenResponse = [GCFSListenResponse message]; - listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_NoChange; - [listenResponse.targetChange.targetIdsArray addValue:1]; - [listenResponse.targetChange.targetIdsArray addValue:4]; - FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; - - XCTAssertEqualObjects(actual, expected); -} - -- (void)testConvertsDocumentChangeWithTargetIds { - FSTWatchChange *expected = [[FSTDocumentWatchChange alloc] - initWithUpdatedTargetIDs:@[ @1, @2 ] - removedTargetIDs:@[] - documentKey:FSTTestDocKey(@"coll/1") - document:FSTTestDoc(@"coll/1", 5, @{@"foo" : @"bar"}, NO)]; - GCFSListenResponse *listenResponse = [GCFSListenResponse message]; - listenResponse.documentChange.document.name = @"projects/p/databases/d/documents/coll/1"; - listenResponse.documentChange.document.updateTime.nanos = 5000; - GCFSValue *fooValue = [GCFSValue message]; - fooValue.stringValue = @"bar"; - [listenResponse.documentChange.document.fields setObject:fooValue forKey:@"foo"]; - [listenResponse.documentChange.targetIdsArray addValue:1]; - [listenResponse.documentChange.targetIdsArray addValue:2]; - FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; - - XCTAssertEqualObjects(actual, expected); -} - -- (void)testConvertsDocumentChangeWithRemovedTargetIds { - FSTWatchChange *expected = [[FSTDocumentWatchChange alloc] - initWithUpdatedTargetIDs:@[ @2 ] - removedTargetIDs:@[ @1 ] - documentKey:FSTTestDocKey(@"coll/1") - document:FSTTestDoc(@"coll/1", 5, @{@"foo" : @"bar"}, NO)]; - GCFSListenResponse *listenResponse = [GCFSListenResponse message]; - listenResponse.documentChange.document.name = @"projects/p/databases/d/documents/coll/1"; - listenResponse.documentChange.document.updateTime.nanos = 5000; - GCFSValue *fooValue = [GCFSValue message]; - fooValue.stringValue = @"bar"; - [listenResponse.documentChange.document.fields setObject:fooValue forKey:@"foo"]; - [listenResponse.documentChange.removedTargetIdsArray addValue:1]; - [listenResponse.documentChange.targetIdsArray addValue:2]; - FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; - - XCTAssertEqualObjects(actual, expected); -} - -- (void)testConvertsDocumentChangeWithDeletions { - FSTWatchChange *expected = - [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] - removedTargetIDs:@[ @1, @2 ] - documentKey:FSTTestDocKey(@"coll/1") - document:FSTTestDeletedDoc(@"coll/1", 5)]; - GCFSListenResponse *listenResponse = [GCFSListenResponse message]; - listenResponse.documentDelete.document = @"projects/p/databases/d/documents/coll/1"; - listenResponse.documentDelete.readTime.nanos = 5000; - [listenResponse.documentDelete.removedTargetIdsArray addValue:1]; - [listenResponse.documentDelete.removedTargetIdsArray addValue:2]; - FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; - - XCTAssertEqualObjects(actual, expected); -} - -- (void)testConvertsDocumentChangeWithRemoves { - FSTWatchChange *expected = - [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] - removedTargetIDs:@[ @1, @2 ] - documentKey:FSTTestDocKey(@"coll/1") - document:nil]; - GCFSListenResponse *listenResponse = [GCFSListenResponse message]; - listenResponse.documentRemove.document = @"projects/p/databases/d/documents/coll/1"; - [listenResponse.documentRemove.removedTargetIdsArray addValue:1]; - [listenResponse.documentRemove.removedTargetIdsArray addValue:2]; - FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; - - XCTAssertEqualObjects(actual, expected); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm new file mode 100644 index 0000000..de4a07a --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm @@ -0,0 +1,800 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Remote/FSTSerializerBeta.h" + +#import +#import +#import +#import +#import + +#import "Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h" +#import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" +#import "Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.h" +#import "Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.h" +#import "Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h" +#import "Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.h" +#import "Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.h" +#import "Firestore/Protos/objc/google/rpc/Status.pbobjc.h" +#import "Firestore/Protos/objc/google/type/Latlng.pbobjc.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Core/FSTTimestamp.h" +#import "Firestore/Source/Local/FSTQueryData.h" +#import "Firestore/Source/Model/FSTDatabaseID.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Model/FSTMutationBatch.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Remote/FSTWatchChange.h" + +#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTSerializerBeta (Test) +- (GCFSValue *)encodedNull; +- (GCFSValue *)encodedBool:(BOOL)value; +- (GCFSValue *)encodedDouble:(double)value; +- (GCFSValue *)encodedInteger:(int64_t)value; +- (GCFSValue *)encodedString:(NSString *)value; +- (GCFSValue *)encodedDate:(NSDate *)value; + +- (GCFSDocumentMask *)encodedFieldMask:(FSTFieldMask *)fieldMask; +- (NSMutableArray *)encodedFieldTransforms: + (NSArray *)fieldTransforms; + +- (GCFSStructuredQuery_Filter *)encodedRelationFilter:(FSTRelationFilter *)filter; +@end + +@interface GCFSStructuredQuery_Order (Test) ++ (instancetype)messageWithProperty:(NSString *)property ascending:(BOOL)ascending; +@end + +@implementation GCFSStructuredQuery_Order (Test) + ++ (instancetype)messageWithProperty:(NSString *)property ascending:(BOOL)ascending { + GCFSStructuredQuery_Order *order = [GCFSStructuredQuery_Order message]; + order.field.fieldPath = property; + order.direction = ascending ? GCFSStructuredQuery_Direction_Ascending + : GCFSStructuredQuery_Direction_Descending; + return order; +} +@end + +@interface FSTSerializerBetaTests : XCTestCase +@property(nonatomic, strong) FSTSerializerBeta *serializer; +@end + +@implementation FSTSerializerBetaTests + +- (void)setUp { + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; + self.serializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseID]; +} + +- (void)testEncodesNull { + FSTFieldValue *model = [FSTNullValue nullValue]; + + GCFSValue *proto = [GCFSValue message]; + proto.nullValue = GPBNullValue_NullValue; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_NullValue]; +} + +- (void)testEncodesBool { + NSArray *examples = @[ @YES, @NO ]; + for (NSNumber *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.booleanValue = [example boolValue]; + + [self assertRoundTripForModel:model + proto:proto + type:GCFSValue_ValueType_OneOfCase_BooleanValue]; + } +} + +- (void)testEncodesIntegers { + NSArray *examples = @[ @(LLONG_MIN), @(-100), @(-1), @0, @1, @100, @(LLONG_MAX) ]; + for (NSNumber *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.integerValue = [example longLongValue]; + + [self assertRoundTripForModel:model + proto:proto + type:GCFSValue_ValueType_OneOfCase_IntegerValue]; + } +} + +- (void)testEncodesDoubles { + NSArray *examples = @[ + // normal negative numbers. + @(-INFINITY), @(-DBL_MAX), @(LLONG_MIN * 1.0 - 1.0), @(-2.0), @(-1.1), @(-1.0), @(-DBL_MIN), + + // negative smallest subnormal, zeroes, positive smallest subnormal + @(-0x1.0p-1074), @(-0.0), @(0.0), @(0x1.0p-1074), + + // and the rest + @(DBL_MIN), @0.1, @1.1, @(LLONG_MAX * 1.0), @(DBL_MAX), @(INFINITY), + + // NaN. + @(0.0 / 0.0) + ]; + for (NSNumber *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.doubleValue = [example doubleValue]; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_DoubleValue]; + } +} + +- (void)testEncodesStrings { + NSArray *examples = @[ + @"", + @"a", + @"abc def", + @"æ", + @"\0\ud7ff\ue000\uffff", + @"(╯°□°)╯︵ ┻━┻", + ]; + for (NSString *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.stringValue = example; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_StringValue]; + } +} + +- (void)testEncodesDates { + NSDateComponents *dateWithNanos = FSTTestDateComponents(2016, 1, 2, 10, 20, 50); + dateWithNanos.nanosecond = 500000000; + + NSArray *examples = @[ + [[NSCalendar currentCalendar] dateFromComponents:dateWithNanos], + FSTTestDate(2016, 6, 17, 10, 50, 15) + ]; + + GCFSValue *timestamp1 = [GCFSValue message]; + timestamp1.timestampValue.seconds = 1451730050; + timestamp1.timestampValue.nanos = 500000000; + + GCFSValue *timestamp2 = [GCFSValue message]; + timestamp2.timestampValue.seconds = 1466160615; + timestamp2.timestampValue.nanos = 0; + NSArray *expectedTimestamps = @[ timestamp1, timestamp2 ]; + + for (NSUInteger i = 0; i < [examples count]; i++) { + [self assertRoundTripForModel:FSTTestFieldValue(examples[i]) + proto:expectedTimestamps[i] + type:GCFSValue_ValueType_OneOfCase_TimestampValue]; + } +} + +- (void)testEncodesGeoPoints { + NSArray *examples = + @[ FSTTestGeoPoint(0, 0), FSTTestGeoPoint(1.24, 4.56), FSTTestGeoPoint(-90, 180) ]; + for (FIRGeoPoint *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.geoPointValue = [GTPLatLng message]; + proto.geoPointValue.latitude = example.latitude; + proto.geoPointValue.longitude = example.longitude; + + [self assertRoundTripForModel:model + proto:proto + type:GCFSValue_ValueType_OneOfCase_GeoPointValue]; + } +} + +- (void)testEncodesBlobs { + NSArray *examples = @[ + FSTTestData(-1), + FSTTestData(0, -1), + FSTTestData(0, 1, 2, -1), + FSTTestData(255, -1), + FSTTestData(0, 1, 255, -1), + ]; + for (NSData *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.bytesValue = example; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_BytesValue]; + } +} + +- (void)testEncodesResourceNames { + FSTDocumentKeyReference *reference = FSTTestRef(@"project", kDefaultDatabaseID, @"foo/bar"); + GCFSValue *proto = [GCFSValue message]; + proto.referenceValue = @"projects/project/databases/(default)/documents/foo/bar"; + + [self assertRoundTripForModel:FSTTestFieldValue(reference) + proto:proto + type:GCFSValue_ValueType_OneOfCase_ReferenceValue]; +} + +- (void)testEncodesArrays { + FSTFieldValue *model = FSTTestFieldValue(@[ @YES, @"foo" ]); + + GCFSValue *proto = [GCFSValue message]; + [proto.arrayValue.valuesArray addObjectsFromArray:@[ + [self.serializer encodedBool:YES], [self.serializer encodedString:@"foo"] + ]]; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_ArrayValue]; +} + +- (void)testEncodesEmptyMap { + FSTFieldValue *model = [FSTObjectValue objectValue]; + + GCFSValue *proto = [GCFSValue message]; + proto.mapValue = [GCFSMapValue message]; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_MapValue]; +} + +- (void)testEncodesNestedObjects { + FSTFieldValue *model = FSTTestFieldValue(@{ + @"b" : @YES, + @"d" : @(DBL_MAX), + @"i" : @1, + @"n" : [NSNull null], + @"s" : @"foo", + @"a" : @[ @2, @"bar", + @{ @"b" : @NO } ], + @"o" : @{ + @"d" : @100, + @"nested" : @{@"e" : @(LLONG_MIN)}, + }, + }); + + GCFSValue *innerObject = [GCFSValue message]; + innerObject.mapValue.fields[@"b"] = [self.serializer encodedBool:NO]; + + GCFSValue *middleArray = [GCFSValue message]; + [middleArray.arrayValue.valuesArray addObjectsFromArray:@[ + [self.serializer encodedInteger:2], [self.serializer encodedString:@"bar"], innerObject + ]]; + + innerObject = [GCFSValue message]; + innerObject.mapValue.fields[@"e"] = [self.serializer encodedInteger:LLONG_MIN]; + + GCFSValue *middleObject = [GCFSValue message]; + [middleObject.mapValue.fields addEntriesFromDictionary:@{ + @"d" : [self.serializer encodedInteger:100], + @"nested" : innerObject + }]; + + GCFSValue *proto = [GCFSValue message]; + [proto.mapValue.fields addEntriesFromDictionary:@{ + @"b" : [self.serializer encodedBool:YES], + @"d" : [self.serializer encodedDouble:DBL_MAX], + @"i" : [self.serializer encodedInteger:1], + @"n" : [self.serializer encodedNull], + @"s" : [self.serializer encodedString:@"foo"], + @"a" : middleArray, + @"o" : middleObject + }]; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_MapValue]; +} + +- (void)assertRoundTripForModel:(FSTFieldValue *)model + proto:(GCFSValue *)value + type:(GCFSValue_ValueType_OneOfCase)type { + GCFSValue *actualProto = [self.serializer encodedFieldValue:model]; + XCTAssertEqual(actualProto.valueTypeOneOfCase, type); + XCTAssertEqualObjects(actualProto, value); + + FSTFieldValue *actualModel = [self.serializer decodedFieldValue:value]; + XCTAssertEqualObjects(actualModel, model); +} + +- (void)testEncodesSetMutation { + FSTSetMutation *mutation = FSTTestSetMutation(@"docs/1", @{ @"a" : @"b", @"num" : @1 }); + GCFSWrite *proto = [GCFSWrite message]; + proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key]; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)testEncodesPatchMutation { + FSTPatchMutation *mutation = + FSTTestPatchMutation(@"docs/1", + @{ @"a" : @"b", + @"num" : @1, + @"some.de\\\\ep.th\\ing'" : @2 }, + nil); + GCFSWrite *proto = [GCFSWrite message]; + proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key]; + proto.updateMask = [self.serializer encodedFieldMask:mutation.fieldMask]; + proto.currentDocument.exists = YES; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)testEncodesDeleteMutation { + FSTDeleteMutation *mutation = FSTTestDeleteMutation(@"docs/1"); + GCFSWrite *proto = [GCFSWrite message]; + proto.delete_p = @"projects/p/databases/d/documents/docs/1"; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)testEncodesTransformMutation { + FSTTransformMutation *mutation = FSTTestTransformMutation(@"docs/1", @[ @"a", @"bar.baz" ]); + GCFSWrite *proto = [GCFSWrite message]; + proto.transform = [GCFSDocumentTransform message]; + proto.transform.document = [self.serializer encodedDocumentKey:mutation.key]; + proto.transform.fieldTransformsArray = + [self.serializer encodedFieldTransforms:mutation.fieldTransforms]; + proto.currentDocument.exists = YES; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)testEncodesSetMutationWithPrecondition { + FSTSetMutation *mutation = [[FSTSetMutation alloc] + initWithKey:FSTTestDocKey(@"foo/bar") + value:FSTTestObjectValue( + @{ @"a" : @"b", + @"num" : @1 }) + precondition:[FSTPrecondition preconditionWithUpdateTime:FSTTestVersion(4)]]; + GCFSWrite *proto = [GCFSWrite message]; + proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key]; + proto.currentDocument.updateTime = + [self.serializer encodedTimestamp:[[FSTTimestamp alloc] initWithSeconds:0 nanos:4000]]; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)assertRoundTripForMutation:(FSTMutation *)mutation proto:(GCFSWrite *)proto { + GCFSWrite *actualProto = [self.serializer encodedMutation:mutation]; + XCTAssertEqualObjects(actualProto, proto); + + FSTMutation *actualMutation = [self.serializer decodedMutation:proto]; + XCTAssertEqualObjects(actualMutation, mutation); +} + +- (void)testRoundTripSpecialFieldNames { + FSTMutation *set = FSTTestSetMutation(@"collection/key", @{ + @"field" : [NSString stringWithFormat:@"field %d", 1], + @"field.dot" : @2, + @"field\\slash" : @3 + }); + GCFSWrite *encoded = [self.serializer encodedMutation:set]; + FSTMutation *decoded = [self.serializer decodedMutation:encoded]; + XCTAssertEqualObjects(set, decoded); +} + +- (void)testEncodesListenRequestLabels { + FSTQuery *query = FSTTestQuery(@"collection/key"); + FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query + targetID:2 + listenSequenceNumber:3 + purpose:FSTQueryPurposeListen]; + + NSDictionary *result = + [self.serializer encodedListenRequestLabelsForQueryData:queryData]; + XCTAssertNil(result); + + queryData = [[FSTQueryData alloc] initWithQuery:query + targetID:2 + listenSequenceNumber:3 + purpose:FSTQueryPurposeLimboResolution]; + result = [self.serializer encodedListenRequestLabelsForQueryData:queryData]; + XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"limbo-document"}); + + queryData = [[FSTQueryData alloc] initWithQuery:query + targetID:2 + listenSequenceNumber:3 + purpose:FSTQueryPurposeExistenceFilterMismatch]; + result = [self.serializer encodedListenRequestLabelsForQueryData:queryData]; + XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"existence-filter-mismatch"}); +} + +- (void)testEncodesRelationFilter { + FSTRelationFilter *input = FSTTestFilter(@"item.part.top", @"==", @"food"); + GCFSStructuredQuery_Filter *actual = [self.serializer encodedRelationFilter:input]; + + GCFSStructuredQuery_Filter *expected = [GCFSStructuredQuery_Filter message]; + GCFSStructuredQuery_FieldFilter *prop = expected.fieldFilter; + prop.field.fieldPath = @"item.part.top"; + prop.op = GCFSStructuredQuery_FieldFilter_Operator_Equal; + prop.value.stringValue = @"food"; + XCTAssertEqualObjects(actual, expected); +} + +#pragma mark - encodedQuery + +- (void)testEncodesFirstLevelKeyQueries { + FSTQuery *q = FSTTestQuery(@"docs/1"); + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + [expected.documents.documentsArray addObject:@"projects/p/databases/d/documents/docs/1"]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesFirstLevelAncestorQueries { + FSTQuery *q = FSTTestQuery(@"messages"); + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"messages"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesNestedAncestorQueries { + FSTQuery *q = FSTTestQuery(@"rooms/1/messages/10/attachments"); + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"attachments"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesSingleFiltersAtFirstLevelCollections { + FSTQuery *q = [FSTTestQuery(@"docs") queryByAddingFilter:FSTTestFilter(@"prop", @"<", @(42))]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + + GCFSStructuredQuery_FieldFilter *filter = expected.query.structuredQuery.where.fieldFilter; + filter.field.fieldPath = @"prop"; + filter.op = GCFSStructuredQuery_FieldFilter_Operator_LessThan; + filter.value.integerValue = 42; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesMultipleFiltersOnDeeperCollections { + FSTQuery *q = [[FSTTestQuery(@"rooms/1/messages/10/attachments") + queryByAddingFilter:FSTTestFilter(@"prop", @">=", @(42))] + queryByAddingFilter:FSTTestFilter(@"author", @"==", @"dimond")]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"attachments"; + [expected.query.structuredQuery.fromArray addObject:from]; + + GCFSStructuredQuery_Filter *filter1 = [GCFSStructuredQuery_Filter message]; + GCFSStructuredQuery_FieldFilter *field1 = filter1.fieldFilter; + field1.field.fieldPath = @"prop"; + field1.op = GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual; + field1.value.integerValue = 42; + + GCFSStructuredQuery_Filter *filter2 = [GCFSStructuredQuery_Filter message]; + GCFSStructuredQuery_FieldFilter *field2 = filter2.fieldFilter; + field2.field.fieldPath = @"author"; + field2.op = GCFSStructuredQuery_FieldFilter_Operator_Equal; + field2.value.stringValue = @"dimond"; + + GCFSStructuredQuery_CompositeFilter *composite = + expected.query.structuredQuery.where.compositeFilter; + composite.op = GCFSStructuredQuery_CompositeFilter_Operator_And; + [composite.filtersArray addObject:filter1]; + [composite.filtersArray addObject:filter2]; + + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesNullFilter { + [self unaryFilterTestWithValue:[NSNull null] + expectedUnaryOperator:GCFSStructuredQuery_UnaryFilter_Operator_IsNull]; +} + +- (void)testEncodesNanFilter { + [self unaryFilterTestWithValue:@(NAN) + expectedUnaryOperator:GCFSStructuredQuery_UnaryFilter_Operator_IsNan]; +} + +- (void)unaryFilterTestWithValue:(id)value + expectedUnaryOperator:(GCFSStructuredQuery_UnaryFilter_Operator)op { + FSTQuery *q = [FSTTestQuery(@"docs") queryByAddingFilter:FSTTestFilter(@"prop", @"==", value)]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + + GCFSStructuredQuery_UnaryFilter *filter = expected.query.structuredQuery.where.unaryFilter; + filter.field.fieldPath = @"prop"; + filter.op = op; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesSortOrders { + FSTQuery *q = [FSTTestQuery(@"docs") + queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"prop") + ascending:YES]]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesSortOrdersDescending { + FSTQuery *q = [FSTTestQuery(@"rooms/1/messages/10/attachments") + queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"prop") + ascending:NO]]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"attachments"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:NO]]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:NO]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesLimits { + FSTQuery *q = [FSTTestQuery(@"docs") queryBySettingLimit:26]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.query.structuredQuery.limit.value = 26; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesResumeTokens { + FSTQuery *q = FSTTestQuery(@"docs"); + FSTQueryData *model = [[FSTQueryData alloc] initWithQuery:q + targetID:1 + listenSequenceNumber:0 + purpose:FSTQueryPurposeListen + snapshotVersion:[FSTSnapshotVersion noVersion] + resumeToken:FSTTestData(1, 2, 3, -1)]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + expected.resumeToken = FSTTestData(1, 2, 3, -1); + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (FSTQueryData *)queryDataForQuery:(FSTQuery *)query { + return [[FSTQueryData alloc] initWithQuery:query + targetID:1 + listenSequenceNumber:0 + purpose:FSTQueryPurposeListen + snapshotVersion:[FSTSnapshotVersion noVersion] + resumeToken:[NSData data]]; +} + +- (void)assertRoundTripForQueryData:(FSTQueryData *)queryData proto:(GCFSTarget *)proto { + // Verify that the encoded FSTQueryData matches the target. + GCFSTarget *actualProto = [self.serializer encodedTarget:queryData]; + XCTAssertEqualObjects(actualProto, proto); + + // We don't have deserialization logic for full targets since they're not used for RPC + // interaction, but the query deserialization only *is* used for the local store. + FSTQuery *actualModel; + if (proto.targetTypeOneOfCase == GCFSTarget_TargetType_OneOfCase_Query) { + actualModel = [self.serializer decodedQueryFromQueryTarget:proto.query]; + } else { + actualModel = [self.serializer decodedQueryFromDocumentsTarget:proto.documents]; + } + XCTAssertEqualObjects(actualModel, queryData.query); +} + +- (void)testConvertsTargetChangeWithAdded { + FSTWatchChange *expected = + [[FSTWatchTargetChange alloc] initWithState:FSTWatchTargetChangeStateAdded + targetIDs:@[ @1, @4 ] + resumeToken:[NSData data] + cause:nil]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_Add; + [listenResponse.targetChange.targetIdsArray addValue:1]; + [listenResponse.targetChange.targetIdsArray addValue:4]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsTargetChangeWithRemoved { + FSTWatchChange *expected = [[FSTWatchTargetChange alloc] + initWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @1, @4 ] + resumeToken:FSTTestData(0, 1, 2, -1) + cause:[NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodePermissionDenied + userInfo:@{ + NSLocalizedDescriptionKey : @"Error message", + }]]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_Remove; + listenResponse.targetChange.cause.code = FIRFirestoreErrorCodePermissionDenied; + listenResponse.targetChange.cause.message = @"Error message"; + listenResponse.targetChange.resumeToken = FSTTestData(0, 1, 2, -1); + [listenResponse.targetChange.targetIdsArray addValue:1]; + [listenResponse.targetChange.targetIdsArray addValue:4]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsTargetChangeWithNoChange { + FSTWatchChange *expected = + [[FSTWatchTargetChange alloc] initWithState:FSTWatchTargetChangeStateNoChange + targetIDs:@[ @1, @4 ] + resumeToken:[NSData data] + cause:nil]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_NoChange; + [listenResponse.targetChange.targetIdsArray addValue:1]; + [listenResponse.targetChange.targetIdsArray addValue:4]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsDocumentChangeWithTargetIds { + FSTWatchChange *expected = [[FSTDocumentWatchChange alloc] + initWithUpdatedTargetIDs:@[ @1, @2 ] + removedTargetIDs:@[] + documentKey:FSTTestDocKey(@"coll/1") + document:FSTTestDoc(@"coll/1", 5, @{@"foo" : @"bar"}, NO)]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.documentChange.document.name = @"projects/p/databases/d/documents/coll/1"; + listenResponse.documentChange.document.updateTime.nanos = 5000; + GCFSValue *fooValue = [GCFSValue message]; + fooValue.stringValue = @"bar"; + [listenResponse.documentChange.document.fields setObject:fooValue forKey:@"foo"]; + [listenResponse.documentChange.targetIdsArray addValue:1]; + [listenResponse.documentChange.targetIdsArray addValue:2]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsDocumentChangeWithRemovedTargetIds { + FSTWatchChange *expected = [[FSTDocumentWatchChange alloc] + initWithUpdatedTargetIDs:@[ @2 ] + removedTargetIDs:@[ @1 ] + documentKey:FSTTestDocKey(@"coll/1") + document:FSTTestDoc(@"coll/1", 5, @{@"foo" : @"bar"}, NO)]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.documentChange.document.name = @"projects/p/databases/d/documents/coll/1"; + listenResponse.documentChange.document.updateTime.nanos = 5000; + GCFSValue *fooValue = [GCFSValue message]; + fooValue.stringValue = @"bar"; + [listenResponse.documentChange.document.fields setObject:fooValue forKey:@"foo"]; + [listenResponse.documentChange.removedTargetIdsArray addValue:1]; + [listenResponse.documentChange.targetIdsArray addValue:2]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsDocumentChangeWithDeletions { + FSTWatchChange *expected = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:@[ @1, @2 ] + documentKey:FSTTestDocKey(@"coll/1") + document:FSTTestDeletedDoc(@"coll/1", 5)]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.documentDelete.document = @"projects/p/databases/d/documents/coll/1"; + listenResponse.documentDelete.readTime.nanos = 5000; + [listenResponse.documentDelete.removedTargetIdsArray addValue:1]; + [listenResponse.documentDelete.removedTargetIdsArray addValue:2]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsDocumentChangeWithRemoves { + FSTWatchChange *expected = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:@[ @1, @2 ] + documentKey:FSTTestDocKey(@"coll/1") + document:nil]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.documentRemove.document = @"projects/p/databases/d/documents/coll/1"; + [listenResponse.documentRemove.removedTargetIdsArray addValue:1]; + [listenResponse.documentRemove.removedTargetIdsArray addValue:2]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m deleted file mode 100644 index 6bb314d..0000000 --- a/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h" - -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Remote/FSTWatchChange.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTWatchTargetChange (Testing) - -+ (instancetype)changeWithState:(FSTWatchTargetChangeState)state - targetIDs:(NSArray *)targetIDs { - return [[FSTWatchTargetChange alloc] initWithState:state - targetIDs:targetIDs - resumeToken:[NSData data] - cause:nil]; -} - -+ (instancetype)changeWithState:(FSTWatchTargetChangeState)state - targetIDs:(NSArray *)targetIDs - cause:(nullable NSError *)cause { - return [[FSTWatchTargetChange alloc] initWithState:state - targetIDs:targetIDs - resumeToken:[NSData data] - cause:cause]; -} - -+ (instancetype)changeWithState:(FSTWatchTargetChangeState)state - targetIDs:(NSArray *)targetIDs - resumeToken:(nullable NSData *)resumeToken { - return [[FSTWatchTargetChange alloc] initWithState:state - targetIDs:targetIDs - resumeToken:resumeToken - cause:nil]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.mm b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.mm new file mode 100644 index 0000000..6bb314d --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.mm @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h" + +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Remote/FSTWatchChange.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTWatchTargetChange (Testing) + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray *)targetIDs { + return [[FSTWatchTargetChange alloc] initWithState:state + targetIDs:targetIDs + resumeToken:[NSData data] + cause:nil]; +} + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray *)targetIDs + cause:(nullable NSError *)cause { + return [[FSTWatchTargetChange alloc] initWithState:state + targetIDs:targetIDs + resumeToken:[NSData data] + cause:cause]; +} + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray *)targetIDs + resumeToken:(nullable NSData *)resumeToken { + return [[FSTWatchTargetChange alloc] initWithState:state + targetIDs:targetIDs + resumeToken:resumeToken + cause:nil]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTWatchChangeTests.m b/Firestore/Example/Tests/Remote/FSTWatchChangeTests.m deleted file mode 100644 index df2496b..0000000 --- a/Firestore/Example/Tests/Remote/FSTWatchChangeTests.m +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Remote/FSTWatchChange.h" - -#import - -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Remote/FSTExistenceFilter.h" - -#import "Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h" -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTWatchChangeTests : XCTestCase -@end - -@implementation FSTWatchChangeTests - -- (void)testDocumentChange { - FSTMaybeDocument *doc = FSTTestDoc(@"a/b", 1, @{}, NO); - FSTDocumentWatchChange *change = - [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2, @3 ] - removedTargetIDs:@[ @4, @5 ] - documentKey:doc.key - document:doc]; - XCTAssertEqual(change.updatedTargetIDs.count, 3); - XCTAssertEqual(change.removedTargetIDs.count, 2); - // Testing object identity here is fine. - XCTAssertEqual(change.document, doc); -} - -- (void)testExistenceFilterChange { - FSTExistenceFilter *filter = [FSTExistenceFilter filterWithCount:7]; - FSTExistenceFilterWatchChange *change = - [FSTExistenceFilterWatchChange changeWithFilter:filter targetID:5]; - XCTAssertEqual(change.filter.count, 7); - XCTAssertEqual(change.targetID, 5); -} - -- (void)testWatchTargetChange { - FSTWatchTargetChange *change = - [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset - targetIDs:@[ @1, @2 ] - cause:nil]; - XCTAssertEqual(change.state, FSTWatchTargetChangeStateReset); - XCTAssertEqual(change.targetIDs.count, 2); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTWatchChangeTests.mm b/Firestore/Example/Tests/Remote/FSTWatchChangeTests.mm new file mode 100644 index 0000000..df2496b --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTWatchChangeTests.mm @@ -0,0 +1,66 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Remote/FSTWatchChange.h" + +#import + +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Remote/FSTExistenceFilter.h" + +#import "Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h" +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTWatchChangeTests : XCTestCase +@end + +@implementation FSTWatchChangeTests + +- (void)testDocumentChange { + FSTMaybeDocument *doc = FSTTestDoc(@"a/b", 1, @{}, NO); + FSTDocumentWatchChange *change = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2, @3 ] + removedTargetIDs:@[ @4, @5 ] + documentKey:doc.key + document:doc]; + XCTAssertEqual(change.updatedTargetIDs.count, 3); + XCTAssertEqual(change.removedTargetIDs.count, 2); + // Testing object identity here is fine. + XCTAssertEqual(change.document, doc); +} + +- (void)testExistenceFilterChange { + FSTExistenceFilter *filter = [FSTExistenceFilter filterWithCount:7]; + FSTExistenceFilterWatchChange *change = + [FSTExistenceFilterWatchChange changeWithFilter:filter targetID:5]; + XCTAssertEqual(change.filter.count, 7); + XCTAssertEqual(change.targetID, 5); +} + +- (void)testWatchTargetChange { + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @1, @2 ] + cause:nil]; + XCTAssertEqual(change.state, FSTWatchTargetChangeStateReset); + XCTAssertEqual(change.targetIDs.count, 2); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.m b/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.m deleted file mode 100644 index a67f667..0000000 --- a/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.m +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Example/Tests/SpecTests/FSTSpecTests.h" - -#import "Firestore/Source/Local/FSTLevelDB.h" - -#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" -#import "Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h" - -NS_ASSUME_NONNULL_BEGIN - -/** - * An implementation of FSTSpecTests that uses the LevelDB implementation of local storage. - * - * See the FSTSpecTests class comments for more information about how this works. - */ -@interface FSTLevelDBSpecTests : FSTSpecTests -@end - -@implementation FSTLevelDBSpecTests - -/** Overrides -[FSTSpecTests persistence] */ -- (id)persistence { - return [FSTPersistenceTestHelpers levelDBPersistence]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.mm b/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.mm new file mode 100644 index 0000000..a67f667 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.mm @@ -0,0 +1,43 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Example/Tests/SpecTests/FSTSpecTests.h" + +#import "Firestore/Source/Local/FSTLevelDB.h" + +#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" +#import "Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * An implementation of FSTSpecTests that uses the LevelDB implementation of local storage. + * + * See the FSTSpecTests class comments for more information about how this works. + */ +@interface FSTLevelDBSpecTests : FSTSpecTests +@end + +@implementation FSTLevelDBSpecTests + +/** Overrides -[FSTSpecTests persistence] */ +- (id)persistence { + return [FSTPersistenceTestHelpers levelDBPersistence]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m b/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m deleted file mode 100644 index 3030ab5..0000000 --- a/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Example/Tests/SpecTests/FSTSpecTests.h" - -#import "Firestore/Source/Local/FSTMemoryPersistence.h" - -#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" -#import "Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h" - -NS_ASSUME_NONNULL_BEGIN - -/** - * An implementation of FSTSpecTests that uses the memory-only implementation of local storage. - * - * @see the FSTSpecTests class comments for more information about how this works. - */ -@interface FSTMemorySpecTests : FSTSpecTests -@end - -@implementation FSTMemorySpecTests - -/** Overrides -[FSTSpecTests persistence] */ -- (id)persistence { - return [FSTPersistenceTestHelpers memoryPersistence]; -} -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.mm b/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.mm new file mode 100644 index 0000000..3030ab5 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.mm @@ -0,0 +1,42 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Example/Tests/SpecTests/FSTSpecTests.h" + +#import "Firestore/Source/Local/FSTMemoryPersistence.h" + +#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" +#import "Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * An implementation of FSTSpecTests that uses the memory-only implementation of local storage. + * + * @see the FSTSpecTests class comments for more information about how this works. + */ +@interface FSTMemorySpecTests : FSTSpecTests +@end + +@implementation FSTMemorySpecTests + +/** Overrides -[FSTSpecTests persistence] */ +- (id)persistence { + return [FSTPersistenceTestHelpers memoryPersistence]; +} +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.m b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.m deleted file mode 100644 index 9a1d719..0000000 --- a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.m +++ /dev/null @@ -1,380 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Example/Tests/SpecTests/FSTMockDatastore.h" - -#import "Firestore/Source/Auth/FSTEmptyCredentialsProvider.h" -#import "Firestore/Source/Core/FSTDatabaseInfo.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTDatabaseID.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Remote/FSTSerializerBeta.h" -#import "Firestore/Source/Remote/FSTStream.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTLogger.h" - -#import "Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h" - -@class GRPCProtoCall; - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTMockWatchStream - -@interface FSTMockWatchStream : FSTWatchStream - -- (instancetype)initWithDatastore:(FSTMockDatastore *)datastore - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue - credentials:(id)credentials - serializer:(FSTSerializerBeta *)serializer NS_DESIGNATED_INITIALIZER; - -- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue - credentials:(id)credentials - serializer:(FSTSerializerBeta *)serializer NS_UNAVAILABLE; - -- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue - credentials:(id)credentials - responseMessageClass:(Class)responseMessageClass NS_UNAVAILABLE; - -@property(nonatomic, assign) BOOL open; -@property(nonatomic, strong, readonly) FSTMockDatastore *datastore; -@property(nonatomic, strong, readonly) - NSMutableDictionary *activeTargets; -@property(nonatomic, weak, readwrite, nullable) id delegate; - -@end - -@implementation FSTMockWatchStream - -- (instancetype)initWithDatastore:(FSTMockDatastore *)datastore - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue - credentials:(id)credentials - serializer:(FSTSerializerBeta *)serializer { - self = [super initWithDatabase:datastore.databaseInfo - workerDispatchQueue:workerDispatchQueue - credentials:credentials - serializer:serializer]; - if (self) { - FSTAssert(datastore, @"Datastore must not be nil"); - _datastore = datastore; - _activeTargets = [NSMutableDictionary dictionary]; - } - return self; -} - -#pragma mark - Overridden FSTWatchStream methods. - -- (void)startWithDelegate:(id)delegate { - FSTAssert(!self.open, @"Trying to start already started watch stream"); - self.open = YES; - self.delegate = delegate; - [self notifyStreamOpen]; -} - -- (void)stop { - [self.activeTargets removeAllObjects]; - self.delegate = nil; -} - -- (BOOL)isOpen { - return self.open; -} - -- (BOOL)isStarted { - return self.open; -} - -- (void)notifyStreamOpen { - [self.delegate watchStreamDidOpen]; -} - -- (void)notifyStreamInterruptedWithError:(nullable NSError *)error { - [self.delegate watchStreamWasInterruptedWithError:error]; -} - -- (void)watchQuery:(FSTQueryData *)query { - FSTLog(@"watchQuery: %d: %@", query.targetID, query.query); - self.datastore.watchStreamRequestCount += 1; - // Snapshot version is ignored on the wire - FSTQueryData *sentQueryData = - [query queryDataByReplacingSnapshotVersion:[FSTSnapshotVersion noVersion] - resumeToken:query.resumeToken]; - self.activeTargets[@(query.targetID)] = sentQueryData; -} - -- (void)unwatchTargetID:(FSTTargetID)targetID { - FSTLog(@"unwatchTargetID: %d", targetID); - [self.activeTargets removeObjectForKey:@(targetID)]; -} - -- (void)failStreamWithError:(NSError *)error { - self.open = NO; - [self notifyStreamInterruptedWithError:error]; -} - -#pragma mark - Helper methods. - -- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap { - if ([change isKindOfClass:[FSTWatchTargetChange class]]) { - FSTWatchTargetChange *targetChange = (FSTWatchTargetChange *)change; - if (targetChange.cause) { - for (NSNumber *targetID in targetChange.targetIDs) { - if (!self.activeTargets[targetID]) { - // Technically removing an unknown target is valid (e.g. it could race with a - // server-side removal), but we want to pay extra careful attention in tests - // that we only remove targets we listened too. - FSTFail(@"Removing a non-active target"); - } - [self.activeTargets removeObjectForKey:targetID]; - } - } - } - [self.delegate watchStreamDidChange:change snapshotVersion:snap]; -} - -@end - -#pragma mark - FSTMockWriteStream - -@interface FSTWriteStream () - -@property(nonatomic, weak, readwrite, nullable) id delegate; - -- (void)notifyStreamOpen; -- (void)notifyStreamInterruptedWithError:(nullable NSError *)error; - -@end - -@interface FSTMockWriteStream : FSTWriteStream - -- (instancetype)initWithDatastore:(FSTMockDatastore *)datastore - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue - credentials:(id)credentials - serializer:(FSTSerializerBeta *)serializer NS_DESIGNATED_INITIALIZER; - -- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue - credentials:(id)credentials - serializer:(FSTSerializerBeta *)serializer NS_UNAVAILABLE; - -- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue - credentials:(id)credentials - responseMessageClass:(Class)responseMessageClass NS_UNAVAILABLE; - -@property(nonatomic, strong, readonly) FSTMockDatastore *datastore; -@property(nonatomic, assign) BOOL open; -@property(nonatomic, strong, readonly) NSMutableArray *> *sentMutations; - -@end - -@implementation FSTMockWriteStream - -- (instancetype)initWithDatastore:(FSTMockDatastore *)datastore - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue - credentials:(id)credentials - serializer:(FSTSerializerBeta *)serializer { - self = [super initWithDatabase:datastore.databaseInfo - workerDispatchQueue:workerDispatchQueue - credentials:credentials - serializer:serializer]; - if (self) { - FSTAssert(datastore, @"Datastore must not be nil"); - _datastore = datastore; - _sentMutations = [NSMutableArray array]; - } - return self; -} - -#pragma mark - Overridden FSTWriteStream methods. - -- (void)startWithDelegate:(id)delegate { - FSTAssert(!self.open, @"Trying to start already started write stream"); - self.open = YES; - [self.sentMutations removeAllObjects]; - self.delegate = delegate; - [self notifyStreamOpen]; -} - -- (BOOL)isOpen { - return self.open; -} - -- (BOOL)isStarted { - return self.open; -} - -- (void)writeHandshake { - self.datastore.writeStreamRequestCount += 1; - self.handshakeComplete = YES; - [self.delegate writeStreamDidCompleteHandshake]; -} - -- (void)writeMutations:(NSArray *)mutations { - self.datastore.writeStreamRequestCount += 1; - [self.sentMutations addObject:mutations]; -} - -#pragma mark - Helper methods. - -/** Injects a write ack as though it had come from the backend in response to a write. */ -- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion - mutationResults:(NSArray *)results { - [self.delegate writeStreamDidReceiveResponseWithVersion:commitVersion mutationResults:results]; -} - -/** Injects a failed write response as though it had come from the backend. */ -- (void)failStreamWithError:(NSError *)error { - self.open = NO; - [self notifyStreamInterruptedWithError:error]; -} - -/** - * Returns the next write that was "sent to the backend", failing if there are no queued sent - */ -- (NSArray *)nextSentWrite { - FSTAssert(self.sentMutations.count > 0, - @"Writes need to happen before you can call nextSentWrite."); - NSArray *result = [self.sentMutations objectAtIndex:0]; - [self.sentMutations removeObjectAtIndex:0]; - return result; -} - -/** - * Returns the number of mutations that have been sent to the backend but not retrieved via - * nextSentWrite yet. - */ -- (int)sentMutationsCount { - return (int)self.sentMutations.count; -} - -@end - -#pragma mark - FSTMockDatastore - -@interface FSTMockDatastore () -@property(nonatomic, strong, nullable) FSTMockWatchStream *watchStream; -@property(nonatomic, strong, nullable) FSTMockWriteStream *writeStream; - -/** Properties implemented in FSTDatastore that are nonpublic. */ -@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue; -@property(nonatomic, strong, readonly) id credentials; - -@end - -@implementation FSTMockDatastore - -+ (instancetype)mockDatastoreWithWorkerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue { - FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"project" database:@"database"]; - FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID - persistenceKey:@"persistence" - host:@"host" - sslEnabled:NO]; - - FSTEmptyCredentialsProvider *credentials = [[FSTEmptyCredentialsProvider alloc] init]; - - return [[FSTMockDatastore alloc] initWithDatabaseInfo:databaseInfo - workerDispatchQueue:workerDispatchQueue - credentials:credentials]; -} - -#pragma mark - Overridden FSTDatastore methods. - -- (FSTWatchStream *)createWatchStream { - FSTAssert(self.databaseInfo, @"DatabaseInfo must not be nil"); - self.watchStream = [[FSTMockWatchStream alloc] - initWithDatastore:self - workerDispatchQueue:self.workerDispatchQueue - credentials:self.credentials - serializer:[[FSTSerializerBeta alloc] - initWithDatabaseID:self.databaseInfo.databaseID]]; - return self.watchStream; -} - -- (FSTWriteStream *)createWriteStream { - FSTAssert(self.databaseInfo, @"DatabaseInfo must not be nil"); - self.writeStream = [[FSTMockWriteStream alloc] - initWithDatastore:self - workerDispatchQueue:self.workerDispatchQueue - credentials:self.credentials - serializer:[[FSTSerializerBeta alloc] - initWithDatabaseID:self.databaseInfo.databaseID]]; - return self.writeStream; -} - -- (void)authorizeAndStartRPC:(GRPCProtoCall *)rpc completion:(FSTVoidErrorBlock)completion { - FSTFail(@"FSTMockDatastore shouldn't be starting any RPCs."); -} - -#pragma mark - Method exposed for tests to call. - -- (NSArray *)nextSentWrite { - return [self.writeStream nextSentWrite]; -} - -- (int)writesSent { - return [self.writeStream sentMutationsCount]; -} - -- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion - mutationResults:(NSArray *)results { - [self.writeStream ackWriteWithVersion:commitVersion mutationResults:results]; -} - -- (void)failWriteWithError:(NSError *_Nullable)error { - [self.writeStream failStreamWithError:error]; -} - -- (void)writeWatchTargetAddedWithTargetIDs:(NSArray *)targetIDs { - FSTWatchTargetChange *change = - [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded - targetIDs:targetIDs - cause:nil]; - [self writeWatchChange:change snapshotVersion:[FSTSnapshotVersion noVersion]]; -} - -- (void)writeWatchCurrentWithTargetIDs:(NSArray *)targetIDs - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion - resumeToken:(NSData *)resumeToken { - FSTWatchTargetChange *change = - [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent - targetIDs:targetIDs - resumeToken:resumeToken]; - [self writeWatchChange:change snapshotVersion:snapshotVersion]; -} - -- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap { - [self.watchStream writeWatchChange:change snapshotVersion:snap]; -} - -- (void)failWatchStreamWithError:(NSError *)error { - [self.watchStream failStreamWithError:error]; -} - -- (NSDictionary *)activeTargets { - return [self.watchStream.activeTargets copy]; -} - -- (BOOL)isWatchStreamOpen { - return self.watchStream.isOpen; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm new file mode 100644 index 0000000..9a1d719 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm @@ -0,0 +1,380 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Example/Tests/SpecTests/FSTMockDatastore.h" + +#import "Firestore/Source/Auth/FSTEmptyCredentialsProvider.h" +#import "Firestore/Source/Core/FSTDatabaseInfo.h" +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Local/FSTQueryData.h" +#import "Firestore/Source/Model/FSTDatabaseID.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Remote/FSTSerializerBeta.h" +#import "Firestore/Source/Remote/FSTStream.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTLogger.h" + +#import "Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h" + +@class GRPCProtoCall; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTMockWatchStream + +@interface FSTMockWatchStream : FSTWatchStream + +- (instancetype)initWithDatastore:(FSTMockDatastore *)datastore + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + serializer:(FSTSerializerBeta *)serializer NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + serializer:(FSTSerializerBeta *)serializer NS_UNAVAILABLE; + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + responseMessageClass:(Class)responseMessageClass NS_UNAVAILABLE; + +@property(nonatomic, assign) BOOL open; +@property(nonatomic, strong, readonly) FSTMockDatastore *datastore; +@property(nonatomic, strong, readonly) + NSMutableDictionary *activeTargets; +@property(nonatomic, weak, readwrite, nullable) id delegate; + +@end + +@implementation FSTMockWatchStream + +- (instancetype)initWithDatastore:(FSTMockDatastore *)datastore + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + serializer:(FSTSerializerBeta *)serializer { + self = [super initWithDatabase:datastore.databaseInfo + workerDispatchQueue:workerDispatchQueue + credentials:credentials + serializer:serializer]; + if (self) { + FSTAssert(datastore, @"Datastore must not be nil"); + _datastore = datastore; + _activeTargets = [NSMutableDictionary dictionary]; + } + return self; +} + +#pragma mark - Overridden FSTWatchStream methods. + +- (void)startWithDelegate:(id)delegate { + FSTAssert(!self.open, @"Trying to start already started watch stream"); + self.open = YES; + self.delegate = delegate; + [self notifyStreamOpen]; +} + +- (void)stop { + [self.activeTargets removeAllObjects]; + self.delegate = nil; +} + +- (BOOL)isOpen { + return self.open; +} + +- (BOOL)isStarted { + return self.open; +} + +- (void)notifyStreamOpen { + [self.delegate watchStreamDidOpen]; +} + +- (void)notifyStreamInterruptedWithError:(nullable NSError *)error { + [self.delegate watchStreamWasInterruptedWithError:error]; +} + +- (void)watchQuery:(FSTQueryData *)query { + FSTLog(@"watchQuery: %d: %@", query.targetID, query.query); + self.datastore.watchStreamRequestCount += 1; + // Snapshot version is ignored on the wire + FSTQueryData *sentQueryData = + [query queryDataByReplacingSnapshotVersion:[FSTSnapshotVersion noVersion] + resumeToken:query.resumeToken]; + self.activeTargets[@(query.targetID)] = sentQueryData; +} + +- (void)unwatchTargetID:(FSTTargetID)targetID { + FSTLog(@"unwatchTargetID: %d", targetID); + [self.activeTargets removeObjectForKey:@(targetID)]; +} + +- (void)failStreamWithError:(NSError *)error { + self.open = NO; + [self notifyStreamInterruptedWithError:error]; +} + +#pragma mark - Helper methods. + +- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap { + if ([change isKindOfClass:[FSTWatchTargetChange class]]) { + FSTWatchTargetChange *targetChange = (FSTWatchTargetChange *)change; + if (targetChange.cause) { + for (NSNumber *targetID in targetChange.targetIDs) { + if (!self.activeTargets[targetID]) { + // Technically removing an unknown target is valid (e.g. it could race with a + // server-side removal), but we want to pay extra careful attention in tests + // that we only remove targets we listened too. + FSTFail(@"Removing a non-active target"); + } + [self.activeTargets removeObjectForKey:targetID]; + } + } + } + [self.delegate watchStreamDidChange:change snapshotVersion:snap]; +} + +@end + +#pragma mark - FSTMockWriteStream + +@interface FSTWriteStream () + +@property(nonatomic, weak, readwrite, nullable) id delegate; + +- (void)notifyStreamOpen; +- (void)notifyStreamInterruptedWithError:(nullable NSError *)error; + +@end + +@interface FSTMockWriteStream : FSTWriteStream + +- (instancetype)initWithDatastore:(FSTMockDatastore *)datastore + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + serializer:(FSTSerializerBeta *)serializer NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + serializer:(FSTSerializerBeta *)serializer NS_UNAVAILABLE; + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + responseMessageClass:(Class)responseMessageClass NS_UNAVAILABLE; + +@property(nonatomic, strong, readonly) FSTMockDatastore *datastore; +@property(nonatomic, assign) BOOL open; +@property(nonatomic, strong, readonly) NSMutableArray *> *sentMutations; + +@end + +@implementation FSTMockWriteStream + +- (instancetype)initWithDatastore:(FSTMockDatastore *)datastore + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + serializer:(FSTSerializerBeta *)serializer { + self = [super initWithDatabase:datastore.databaseInfo + workerDispatchQueue:workerDispatchQueue + credentials:credentials + serializer:serializer]; + if (self) { + FSTAssert(datastore, @"Datastore must not be nil"); + _datastore = datastore; + _sentMutations = [NSMutableArray array]; + } + return self; +} + +#pragma mark - Overridden FSTWriteStream methods. + +- (void)startWithDelegate:(id)delegate { + FSTAssert(!self.open, @"Trying to start already started write stream"); + self.open = YES; + [self.sentMutations removeAllObjects]; + self.delegate = delegate; + [self notifyStreamOpen]; +} + +- (BOOL)isOpen { + return self.open; +} + +- (BOOL)isStarted { + return self.open; +} + +- (void)writeHandshake { + self.datastore.writeStreamRequestCount += 1; + self.handshakeComplete = YES; + [self.delegate writeStreamDidCompleteHandshake]; +} + +- (void)writeMutations:(NSArray *)mutations { + self.datastore.writeStreamRequestCount += 1; + [self.sentMutations addObject:mutations]; +} + +#pragma mark - Helper methods. + +/** Injects a write ack as though it had come from the backend in response to a write. */ +- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray *)results { + [self.delegate writeStreamDidReceiveResponseWithVersion:commitVersion mutationResults:results]; +} + +/** Injects a failed write response as though it had come from the backend. */ +- (void)failStreamWithError:(NSError *)error { + self.open = NO; + [self notifyStreamInterruptedWithError:error]; +} + +/** + * Returns the next write that was "sent to the backend", failing if there are no queued sent + */ +- (NSArray *)nextSentWrite { + FSTAssert(self.sentMutations.count > 0, + @"Writes need to happen before you can call nextSentWrite."); + NSArray *result = [self.sentMutations objectAtIndex:0]; + [self.sentMutations removeObjectAtIndex:0]; + return result; +} + +/** + * Returns the number of mutations that have been sent to the backend but not retrieved via + * nextSentWrite yet. + */ +- (int)sentMutationsCount { + return (int)self.sentMutations.count; +} + +@end + +#pragma mark - FSTMockDatastore + +@interface FSTMockDatastore () +@property(nonatomic, strong, nullable) FSTMockWatchStream *watchStream; +@property(nonatomic, strong, nullable) FSTMockWriteStream *writeStream; + +/** Properties implemented in FSTDatastore that are nonpublic. */ +@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue; +@property(nonatomic, strong, readonly) id credentials; + +@end + +@implementation FSTMockDatastore + ++ (instancetype)mockDatastoreWithWorkerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue { + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"project" database:@"database"]; + FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID + persistenceKey:@"persistence" + host:@"host" + sslEnabled:NO]; + + FSTEmptyCredentialsProvider *credentials = [[FSTEmptyCredentialsProvider alloc] init]; + + return [[FSTMockDatastore alloc] initWithDatabaseInfo:databaseInfo + workerDispatchQueue:workerDispatchQueue + credentials:credentials]; +} + +#pragma mark - Overridden FSTDatastore methods. + +- (FSTWatchStream *)createWatchStream { + FSTAssert(self.databaseInfo, @"DatabaseInfo must not be nil"); + self.watchStream = [[FSTMockWatchStream alloc] + initWithDatastore:self + workerDispatchQueue:self.workerDispatchQueue + credentials:self.credentials + serializer:[[FSTSerializerBeta alloc] + initWithDatabaseID:self.databaseInfo.databaseID]]; + return self.watchStream; +} + +- (FSTWriteStream *)createWriteStream { + FSTAssert(self.databaseInfo, @"DatabaseInfo must not be nil"); + self.writeStream = [[FSTMockWriteStream alloc] + initWithDatastore:self + workerDispatchQueue:self.workerDispatchQueue + credentials:self.credentials + serializer:[[FSTSerializerBeta alloc] + initWithDatabaseID:self.databaseInfo.databaseID]]; + return self.writeStream; +} + +- (void)authorizeAndStartRPC:(GRPCProtoCall *)rpc completion:(FSTVoidErrorBlock)completion { + FSTFail(@"FSTMockDatastore shouldn't be starting any RPCs."); +} + +#pragma mark - Method exposed for tests to call. + +- (NSArray *)nextSentWrite { + return [self.writeStream nextSentWrite]; +} + +- (int)writesSent { + return [self.writeStream sentMutationsCount]; +} + +- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray *)results { + [self.writeStream ackWriteWithVersion:commitVersion mutationResults:results]; +} + +- (void)failWriteWithError:(NSError *_Nullable)error { + [self.writeStream failStreamWithError:error]; +} + +- (void)writeWatchTargetAddedWithTargetIDs:(NSArray *)targetIDs { + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded + targetIDs:targetIDs + cause:nil]; + [self writeWatchChange:change snapshotVersion:[FSTSnapshotVersion noVersion]]; +} + +- (void)writeWatchCurrentWithTargetIDs:(NSArray *)targetIDs + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + resumeToken:(NSData *)resumeToken { + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:targetIDs + resumeToken:resumeToken]; + [self writeWatchChange:change snapshotVersion:snapshotVersion]; +} + +- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap { + [self.watchStream writeWatchChange:change snapshotVersion:snap]; +} + +- (void)failWatchStreamWithError:(NSError *)error { + [self.watchStream failStreamWithError:error]; +} + +- (NSDictionary *)activeTargets { + return [self.watchStream.activeTargets copy]; +} + +- (BOOL)isWatchStreamOpen { + return self.watchStream.isOpen; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.m b/Firestore/Example/Tests/SpecTests/FSTSpecTests.m deleted file mode 100644 index 3335990..0000000 --- a/Firestore/Example/Tests/SpecTests/FSTSpecTests.m +++ /dev/null @@ -1,665 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Example/Tests/SpecTests/FSTSpecTests.h" - -#import -#import - -#import "Firestore/Source/Auth/FSTUser.h" -#import "Firestore/Source/Core/FSTEventManager.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" -#import "Firestore/Source/Local/FSTEagerGarbageCollector.h" -#import "Firestore/Source/Local/FSTNoOpGarbageCollector.h" -#import "Firestore/Source/Local/FSTPersistence.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Remote/FSTExistenceFilter.h" -#import "Firestore/Source/Remote/FSTWatchChange.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTClasses.h" -#import "Firestore/Source/Util/FSTLogger.h" - -#import "Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h" -#import "Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h" -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -NS_ASSUME_NONNULL_BEGIN - -// Disables all other tests; useful for debugging. Multiple tests can have this tag and they'll all -// be run (but all others won't). -static NSString *const kExclusiveTag = @"exclusive"; - -// A tag for tests that should be excluded from execution (on iOS), useful to allow the platforms -// to temporarily diverge. -static NSString *const kNoIOSTag = @"no-ios"; - -@interface FSTSpecTests () -@property(nonatomic, strong) FSTSyncEngineTestDriver *driver; - -// Some config info for the currently running spec; used when restarting the driver (for doRestart). -@property(nonatomic, assign) BOOL GCEnabled; -@property(nonatomic, strong) id driverPersistence; -@end - -@implementation FSTSpecTests - -- (id)persistence { - @throw FSTAbstractMethodException(); // NOLINT -} - -- (void)setUpForSpecWithConfig:(NSDictionary *)config { - // Store persistence / GCEnabled so we can re-use it in doRestart. - self.driverPersistence = [self persistence]; - NSNumber *GCEnabled = config[@"useGarbageCollection"]; - self.GCEnabled = [GCEnabled boolValue]; - self.driver = [[FSTSyncEngineTestDriver alloc] initWithPersistence:self.driverPersistence - garbageCollector:self.garbageCollector]; - [self.driver start]; -} - -- (void)tearDownForSpec { - [self.driver shutdown]; - [self.driverPersistence shutdown]; -} - -/** - * Creates the appropriate garbage collector for the test configuration: an eager collector if - * GC is enabled or a no-op collector otherwise. - */ -- (id)garbageCollector { - return self.GCEnabled ? [[FSTEagerGarbageCollector alloc] init] - : [[FSTNoOpGarbageCollector alloc] init]; -} - -/** - * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for - * FSTSpecTests since it is incomplete without the implementations supplied by its subclasses. - */ -- (BOOL)isTestBaseClass { - return [self class] == [FSTSpecTests class]; -} - -#pragma mark - Methods for constructing objects from specs. - -- (nullable FSTQuery *)parseQuery:(id)querySpec { - if ([querySpec isKindOfClass:[NSString class]]) { - return FSTTestQuery(querySpec); - } else if ([querySpec isKindOfClass:[NSDictionary class]]) { - NSDictionary *queryDict = (NSDictionary *)querySpec; - NSString *path = queryDict[@"path"]; - __block FSTQuery *query = FSTTestQuery(path); - if (queryDict[@"limit"]) { - NSNumber *limit = queryDict[@"limit"]; - query = [query queryBySettingLimit:limit.integerValue]; - } - if (queryDict[@"filters"]) { - NSArray *filters = queryDict[@"filters"]; - [filters enumerateObjectsUsingBlock:^(NSArray *_Nonnull filter, NSUInteger idx, - BOOL *_Nonnull stop) { - query = [query queryByAddingFilter:FSTTestFilter(filter[0], filter[1], filter[2])]; - }]; - } - if (queryDict[@"orderBys"]) { - NSArray *orderBys = queryDict[@"orderBys"]; - [orderBys enumerateObjectsUsingBlock:^(NSArray *_Nonnull orderBy, NSUInteger idx, - BOOL *_Nonnull stop) { - query = [query queryByAddingSortOrder:FSTTestOrderBy(orderBy[0], orderBy[1])]; - }]; - } - return query; - } else { - XCTFail(@"Invalid query: %@", querySpec); - return nil; - } -} - -- (FSTSnapshotVersion *)parseVersion:(NSNumber *_Nullable)version { - return FSTTestVersion(version.longLongValue); -} - -- (FSTDocumentViewChange *)parseChange:(NSArray *)change ofType:(FSTDocumentViewChangeType)type { - BOOL hasMutations = NO; - for (NSUInteger i = 3; i < change.count; ++i) { - if ([change[i] isEqual:@"local"]) { - hasMutations = YES; - } - } - NSNumber *version = change[1]; - FSTDocument *doc = FSTTestDoc(change[0], version.longLongValue, change[2], hasMutations); - return [FSTDocumentViewChange changeWithDocument:doc type:type]; -} - -#pragma mark - Methods for doing the steps of the spec test. - -- (void)doListen:(NSArray *)listenSpec { - FSTQuery *query = [self parseQuery:listenSpec[1]]; - FSTTargetID actualID = [self.driver addUserListenerWithQuery:query]; - - FSTTargetID expectedID = [listenSpec[0] intValue]; - XCTAssertEqual(actualID, expectedID, @"targetID assigned to listen"); -} - -- (void)doUnlisten:(NSArray *)unlistenSpec { - FSTQuery *query = [self parseQuery:unlistenSpec[1]]; - [self.driver removeUserListenerWithQuery:query]; -} - -- (void)doSet:(NSArray *)setSpec { - [self.driver writeUserMutation:FSTTestSetMutation(setSpec[0], setSpec[1])]; -} - -- (void)doPatch:(NSArray *)patchSpec { - [self.driver writeUserMutation:FSTTestPatchMutation(patchSpec[0], patchSpec[1], nil)]; -} - -- (void)doDelete:(NSString *)key { - [self.driver writeUserMutation:FSTTestDeleteMutation(key)]; -} - -- (void)doWatchAck:(NSArray *)ackedTargets snapshot:(NSNumber *)watchSnapshot { - FSTWatchTargetChange *change = - [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded - targetIDs:ackedTargets - cause:nil]; - [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; -} - -- (void)doWatchCurrent:(NSArray *)currentSpec snapshot:(NSNumber *)watchSnapshot { - NSArray *currentTargets = currentSpec[0]; - NSData *resumeToken = [currentSpec[1] dataUsingEncoding:NSUTF8StringEncoding]; - FSTWatchTargetChange *change = - [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent - targetIDs:currentTargets - resumeToken:resumeToken]; - [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; -} - -- (void)doWatchRemove:(NSDictionary *)watchRemoveSpec snapshot:(NSNumber *)watchSnapshot { - NSError *error = nil; - NSDictionary *cause = watchRemoveSpec[@"cause"]; - if (cause) { - int code = ((NSNumber *)cause[@"code"]).intValue; - NSDictionary *userInfo = @{ - NSLocalizedDescriptionKey : @"Error from watchRemove.", - }; - error = [NSError errorWithDomain:FIRFirestoreErrorDomain code:code userInfo:userInfo]; - } - FSTWatchTargetChange *change = - [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved - targetIDs:watchRemoveSpec[@"targetIds"] - cause:error]; - [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; - // Unlike web, the FSTMockDatastore detects a watch removal with cause and will remove active - // targets -} - -- (void)doWatchEntity:(NSDictionary *)watchEntity snapshot:(NSNumber *_Nullable)watchSnapshot { - if (watchEntity[@"docs"]) { - FSTAssert(!watchEntity[@"doc"], @"Exactly one of |doc| or |docs| needs to be set."); - int count = 0; - NSArray *docs = watchEntity[@"docs"]; - for (NSDictionary *doc in docs) { - count++; - bool isLast = (count == docs.count); - NSMutableDictionary *watchSpec = [NSMutableDictionary dictionary]; - watchSpec[@"doc"] = doc; - if (watchEntity[@"targets"]) { - watchSpec[@"targets"] = watchEntity[@"targets"]; - } - if (watchEntity[@"removedTargets"]) { - watchSpec[@"removedTargets"] = watchEntity[@"removedTargets"]; - } - NSNumber *_Nullable version = nil; - if (isLast) { - version = watchSnapshot; - } - [self doWatchEntity:watchSpec snapshot:version]; - } - } else if (watchEntity[@"doc"]) { - NSArray *docSpec = watchEntity[@"doc"]; - FSTDocumentKey *key = FSTTestDocKey(docSpec[0]); - FSTObjectValue *value = FSTTestObjectValue(docSpec[2]); - FSTSnapshotVersion *version = [self parseVersion:docSpec[1]]; - FSTMaybeDocument *doc = - [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; - FSTWatchChange *change = - [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:watchEntity[@"targets"] - removedTargetIDs:watchEntity[@"removedTargets"] - documentKey:doc.key - document:doc]; - [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; - } else if (watchEntity[@"key"]) { - FSTDocumentKey *docKey = FSTTestDocKey(watchEntity[@"key"]); - FSTWatchChange *change = - [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] - removedTargetIDs:watchEntity[@"removedTargets"] - documentKey:docKey - document:nil]; - [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; - } else { - FSTFail(@"Either key, doc or docs must be set."); - } -} - -- (void)doWatchFilter:(NSArray *)watchFilter snapshot:(NSNumber *_Nullable)watchSnapshot { - NSArray *targets = watchFilter[0]; - FSTAssert(targets.count == 1, @"ExistenceFilters currently support exactly one target only."); - - int keyCount = watchFilter.count == 0 ? 0 : (int)watchFilter.count - 1; - - // TODO(dimond): extend this with different existence filters over time. - FSTExistenceFilter *filter = [FSTExistenceFilter filterWithCount:keyCount]; - FSTExistenceFilterWatchChange *change = - [FSTExistenceFilterWatchChange changeWithFilter:filter targetID:targets[0].intValue]; - [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; -} - -- (void)doWatchReset:(NSArray *)watchReset snapshot:(NSNumber *_Nullable)watchSnapshot { - FSTWatchTargetChange *change = - [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset - targetIDs:watchReset - cause:nil]; - [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; -} - -- (void)doWatchStreamClose:(NSDictionary *)closeSpec { - NSDictionary *errorSpec = closeSpec[@"error"]; - int code = ((NSNumber *)(errorSpec[@"code"])).intValue; - [self.driver receiveWatchStreamError:code userInfo:errorSpec]; -} - -- (void)doWriteAck:(NSDictionary *)spec { - FSTSnapshotVersion *version = [self parseVersion:spec[@"version"]]; - NSNumber *expectUserCallback = spec[@"expectUserCallback"]; - - FSTMutationResult *mutationResult = - [[FSTMutationResult alloc] initWithVersion:version transformResults:nil]; - FSTOutstandingWrite *write = - [self.driver receiveWriteAckWithVersion:version mutationResults:@[ mutationResult ]]; - - if (expectUserCallback.boolValue) { - FSTAssert(write.done, @"Write should be done"); - FSTAssert(!write.error, @"Ack should not fail"); - } -} - -- (void)doFailWrite:(NSDictionary *)spec { - NSDictionary *errorSpec = spec[@"error"]; - NSNumber *expectUserCallback = spec[@"expectUserCallback"]; - - int code = ((NSNumber *)(errorSpec[@"code"])).intValue; - FSTOutstandingWrite *write = [self.driver receiveWriteError:code userInfo:errorSpec]; - - if (expectUserCallback.boolValue) { - FSTAssert(write.done, @"Write should be done"); - XCTAssertNotNil(write.error, @"Write should have failed"); - XCTAssertEqualObjects(write.error.domain, FIRFirestoreErrorDomain); - XCTAssertEqual(write.error.code, code); - } -} - -- (void)doDisableNetwork { - [self.driver disableNetwork]; -} - -- (void)doEnableNetwork { - [self.driver enableNetwork]; -} - -- (void)doChangeUser:(id)UID { - FSTUser *user = [UID isEqual:[NSNull null]] ? [FSTUser unauthenticatedUser] - : [[FSTUser alloc] initWithUID:UID]; - [self.driver changeUser:user]; -} - -- (void)doRestart { - // Any outstanding user writes should be automatically re-sent, so we want to preserve them - // when re-creating the driver. - FSTOutstandingWriteQueues *outstandingWrites = self.driver.outstandingWrites; - - [self.driver shutdown]; - - // NOTE: We intentionally don't shutdown / re-create driverPersistence, since we want to - // preserve the persisted state. This is a bit of a cheat since it means we're not exercising - // the initialization / start logic that would normally be hit, but simplifies the plumbing and - // allows us to run these tests against FSTMemoryPersistence as well (there would be no way to - // re-create FSTMemoryPersistence without losing all persisted state). - - self.driver = [[FSTSyncEngineTestDriver alloc] initWithPersistence:self.driverPersistence - garbageCollector:self.garbageCollector - initialUser:self.driver.currentUser - outstandingWrites:outstandingWrites]; - [self.driver start]; -} - -- (void)doStep:(NSDictionary *)step { - if (step[@"userListen"]) { - [self doListen:step[@"userListen"]]; - } else if (step[@"userUnlisten"]) { - [self doUnlisten:step[@"userUnlisten"]]; - } else if (step[@"userSet"]) { - [self doSet:step[@"userSet"]]; - } else if (step[@"userPatch"]) { - [self doPatch:step[@"userPatch"]]; - } else if (step[@"userDelete"]) { - [self doDelete:step[@"userDelete"]]; - } else if (step[@"watchAck"]) { - [self doWatchAck:step[@"watchAck"] snapshot:step[@"watchSnapshot"]]; - } else if (step[@"watchCurrent"]) { - [self doWatchCurrent:step[@"watchCurrent"] snapshot:step[@"watchSnapshot"]]; - } else if (step[@"watchRemove"]) { - [self doWatchRemove:step[@"watchRemove"] snapshot:step[@"watchSnapshot"]]; - } else if (step[@"watchEntity"]) { - [self doWatchEntity:step[@"watchEntity"] snapshot:step[@"watchSnapshot"]]; - } else if (step[@"watchFilter"]) { - [self doWatchFilter:step[@"watchFilter"] snapshot:step[@"watchSnapshot"]]; - } else if (step[@"watchReset"]) { - [self doWatchReset:step[@"watchReset"] snapshot:step[@"watchSnapshot"]]; - } else if (step[@"watchStreamClose"]) { - [self doWatchStreamClose:step[@"watchStreamClose"]]; - } else if (step[@"watchProto"]) { - // watchProto isn't yet used, and it's unclear how to create arbitrary protos from JSON. - FSTFail(@"watchProto is not yet supported."); - } else if (step[@"writeAck"]) { - [self doWriteAck:step[@"writeAck"]]; - } else if (step[@"failWrite"]) { - [self doFailWrite:step[@"failWrite"]]; - } else if (step[@"enableNetwork"]) { - if ([step[@"enableNetwork"] boolValue]) { - [self doEnableNetwork]; - } else { - [self doDisableNetwork]; - } - } else if (step[@"changeUser"]) { - [self doChangeUser:step[@"changeUser"]]; - } else if (step[@"restart"]) { - [self doRestart]; - } else { - XCTFail(@"Unknown step: %@", step); - } -} - -- (void)validateEvent:(FSTQueryEvent *)actual matches:(NSDictionary *)expected { - FSTQuery *expectedQuery = [self parseQuery:expected[@"query"]]; - XCTAssertEqualObjects(actual.query, expectedQuery); - if ([expected[@"errorCode"] integerValue] != 0) { - XCTAssertNotNil(actual.error); - XCTAssertEqual(actual.error.code, [expected[@"errorCode"] integerValue]); - } else { - NSMutableArray *expectedChanges = [NSMutableArray array]; - NSMutableArray *removed = expected[@"removed"]; - for (NSArray *changeSpec in removed) { - [expectedChanges - addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeRemoved]]; - } - NSMutableArray *added = expected[@"added"]; - for (NSArray *changeSpec in added) { - [expectedChanges - addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeAdded]]; - } - NSMutableArray *modified = expected[@"modified"]; - for (NSArray *changeSpec in modified) { - [expectedChanges - addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeModified]]; - } - NSMutableArray *metadata = expected[@"metadata"]; - for (NSArray *changeSpec in metadata) { - [expectedChanges - addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeMetadata]]; - } - XCTAssertEqualObjects(actual.viewSnapshot.documentChanges, expectedChanges); - - BOOL expectedHasPendingWrites = - expected[@"hasPendingWrites"] ? [expected[@"hasPendingWrites"] boolValue] : NO; - BOOL expectedIsFromCache = expected[@"fromCache"] ? [expected[@"fromCache"] boolValue] : NO; - XCTAssertEqual(actual.viewSnapshot.hasPendingWrites, expectedHasPendingWrites, - @"hasPendingWrites"); - XCTAssertEqual(actual.viewSnapshot.isFromCache, expectedIsFromCache, @"isFromCache"); - } -} - -- (void)validateStepExpectations:(NSMutableArray *_Nullable)stepExpectations { - NSArray *events = self.driver.capturedEventsSinceLastCall; - - if (!stepExpectations) { - XCTAssertEqual(events.count, 0); - for (FSTQueryEvent *event in events) { - XCTFail(@"Unexpected event: %@", event); - } - return; - } - - events = - [events sortedArrayUsingComparator:^NSComparisonResult(FSTQueryEvent *q1, FSTQueryEvent *q2) { - return [q1.query.canonicalID compare:q2.query.canonicalID]; - }]; - - XCTAssertEqual(events.count, stepExpectations.count); - NSUInteger i = 0; - for (; i < stepExpectations.count && i < events.count; ++i) { - [self validateEvent:events[i] matches:stepExpectations[i]]; - } - for (; i < stepExpectations.count; ++i) { - XCTFail(@"Missing event: %@", stepExpectations[i]); - } - for (; i < events.count; ++i) { - XCTFail(@"Unexpected event: %@", events[i]); - } -} - -- (void)validateStateExpectations:(nullable NSDictionary *)expected { - if (expected) { - if (expected[@"numOutstandingWrites"]) { - XCTAssertEqual([self.driver sentWritesCount], [expected[@"numOutstandingWrites"] intValue]); - } - if (expected[@"writeStreamRequestCount"]) { - XCTAssertEqual([self.driver writeStreamRequestCount], - [expected[@"writeStreamRequestCount"] intValue]); - } - if (expected[@"watchStreamRequestCount"]) { - XCTAssertEqual([self.driver watchStreamRequestCount], - [expected[@"watchStreamRequestCount"] intValue]); - } - if (expected[@"limboDocs"]) { - NSMutableSet *expectedLimboDocuments = [NSMutableSet set]; - NSArray *docNames = expected[@"limboDocs"]; - for (NSString *name in docNames) { - [expectedLimboDocuments addObject:FSTTestDocKey(name)]; - } - // Update the expected limbo documents - self.driver.expectedLimboDocuments = expectedLimboDocuments; - } - if (expected[@"activeTargets"]) { - NSMutableDictionary *expectedActiveTargets = [NSMutableDictionary dictionary]; - [expected[@"activeTargets"] enumerateKeysAndObjectsUsingBlock:^(NSString *targetIDString, - NSDictionary *queryData, - BOOL *stop) { - FSTTargetID targetID = [targetIDString intValue]; - FSTQuery *query = [self parseQuery:queryData[@"query"]]; - NSData *resumeToken = [queryData[@"resumeToken"] dataUsingEncoding:NSUTF8StringEncoding]; - // TODO(mcg): populate the purpose of the target once it's possible to encode that in the - // spec tests. For now, hard-code that it's a listen despite the fact that it's not always - // the right value. - expectedActiveTargets[@(targetID)] = - [[FSTQueryData alloc] initWithQuery:query - targetID:targetID - listenSequenceNumber:0 - purpose:FSTQueryPurposeListen - snapshotVersion:[FSTSnapshotVersion noVersion] - resumeToken:resumeToken]; - }]; - self.driver.expectedActiveTargets = expectedActiveTargets; - } - } - - // Always validate that the expected limbo docs match the actual limbo docs. - [self validateLimboDocuments]; - // Always validate that the expected active targets match the actual active targets. - [self validateActiveTargets]; -} - -- (void)validateLimboDocuments { - // Make a copy so it can modified while checking against the expected limbo docs. - NSMutableDictionary *actualLimboDocs = - [NSMutableDictionary dictionaryWithDictionary:self.driver.currentLimboDocuments]; - - // Validate that each limbo doc has an expected active target - [actualLimboDocs enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, - FSTBoxedTargetID *targetID, BOOL *stop) { - XCTAssertNotNil(self.driver.expectedActiveTargets[targetID], - @"Found limbo doc without an expected active target"); - }]; - - for (FSTDocumentKey *expectedLimboDoc in self.driver.expectedLimboDocuments) { - XCTAssertNotNil(actualLimboDocs[expectedLimboDoc], - @"Expected doc to be in limbo, but was not: %@", expectedLimboDoc); - [actualLimboDocs removeObjectForKey:expectedLimboDoc]; - } - XCTAssertTrue(actualLimboDocs.count == 0, "Unexpected docs in limbo: %@", actualLimboDocs); -} - -- (void)validateActiveTargets { - // Create a copy so we can modify it in tests - NSMutableDictionary *actualTargets = - [NSMutableDictionary dictionaryWithDictionary:self.driver.activeTargets]; - - [self.driver.expectedActiveTargets enumerateKeysAndObjectsUsingBlock:^(FSTBoxedTargetID *targetID, - FSTQueryData *queryData, - BOOL *stop) { - XCTAssertNotNil(actualTargets[targetID], @"Expected active target not found: %@", queryData); - - // TODO(mcg): validate the purpose of the target once it's possible to encode that in the - // spec tests. For now, only validate properties that can be validated. - // XCTAssertEqualObjects(actualTargets[targetID], queryData); - - FSTQueryData *actual = actualTargets[targetID]; - XCTAssertEqualObjects(actual.query, queryData.query); - XCTAssertEqual(actual.targetID, queryData.targetID); - XCTAssertEqualObjects(actual.snapshotVersion, queryData.snapshotVersion); - XCTAssertEqualObjects(actual.resumeToken, queryData.resumeToken); - - [actualTargets removeObjectForKey:targetID]; - }]; - XCTAssertTrue(actualTargets.count == 0, "Unexpected active targets: %@", actualTargets); -} - -- (void)runSpecTestSteps:(NSArray *)steps config:(NSDictionary *)config { - @try { - [self setUpForSpecWithConfig:config]; - for (NSDictionary *step in steps) { - FSTLog(@"Doing step %@", step); - [self doStep:step]; - [self validateStepExpectations:step[@"expect"]]; - [self validateStateExpectations:step[@"stateExpect"]]; - } - [self.driver validateUsage]; - } @finally { - // Ensure that the driver is torn down even if the test is failing due to a thrown exception so - // that any resources held by the driver are released. This is important when the driver is - // backed by LevelDB because LevelDB locks its database. If -tearDownForSpec were not called - // after an exception then subsequent attempts to open the LevelDB will fail, making it harder - // to zero in on the spec tests as a culprit. - [self tearDownForSpec]; - } -} - -#pragma mark - The actual test methods. - -- (void)testSpecTests { - if ([self isTestBaseClass]) return; - - // Enumerate the .json files containing the spec tests. - NSMutableArray *specFiles = [NSMutableArray array]; - NSMutableArray *parsedSpecs = [NSMutableArray array]; - NSBundle *bundle = [NSBundle bundleForClass:[self class]]; - NSFileManager *fs = [NSFileManager defaultManager]; - BOOL exclusiveMode = NO; - for (NSString *file in [fs enumeratorAtPath:[bundle bundlePath]]) { - if (![@"json" isEqual:[file pathExtension]]) { - continue; - } - - // Read and parse the JSON from the file. - NSString *fileName = [file stringByDeletingPathExtension]; - NSString *path = [bundle pathForResource:fileName ofType:@"json"]; - NSData *json = [NSData dataWithContentsOfFile:path]; - XCTAssertNotNil(json); - NSError *error = nil; - id _Nullable parsed = [NSJSONSerialization JSONObjectWithData:json options:0 error:&error]; - XCTAssertNil(error, @"%@", error); - XCTAssertTrue([parsed isKindOfClass:[NSDictionary class]]); - NSDictionary *testDict = (NSDictionary *)parsed; - - exclusiveMode = exclusiveMode || [self anyTestsAreMarkedExclusive:testDict]; - [specFiles addObject:fileName]; - [parsedSpecs addObject:testDict]; - } - - // Now iterate over them and run them. - __block bool ranAtLeastOneTest = NO; - for (NSUInteger i = 0; i < specFiles.count; i++) { - NSLog(@"Spec test file: %@", specFiles[i]); - // Iterate over the tests in the file and run them. - [parsedSpecs[i] enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { - XCTAssertTrue([obj isKindOfClass:[NSDictionary class]]); - NSDictionary *testDescription = (NSDictionary *)obj; - NSString *describeName = testDescription[@"describeName"]; - NSString *itName = testDescription[@"itName"]; - NSString *name = [NSString stringWithFormat:@"%@ %@", describeName, itName]; - NSDictionary *config = testDescription[@"config"]; - NSArray *steps = testDescription[@"steps"]; - NSArray *tags = testDescription[@"tags"]; - - BOOL runTest = !exclusiveMode || [tags indexOfObject:kExclusiveTag] != NSNotFound; - if ([tags indexOfObject:kNoIOSTag] != NSNotFound) { - runTest = NO; - } - if (runTest) { - NSLog(@" Spec test: %@", name); - [self runSpecTestSteps:steps config:config]; - ranAtLeastOneTest = YES; - } else { - NSLog(@" [SKIPPED] Spec test: %@", name); - } - }]; - } - XCTAssertTrue(ranAtLeastOneTest); -} - -- (BOOL)anyTestsAreMarkedExclusive:(NSDictionary *)tests { - __block BOOL found = NO; - [tests enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { - XCTAssertTrue([obj isKindOfClass:[NSDictionary class]]); - NSDictionary *testDescription = (NSDictionary *)obj; - NSArray *tags = testDescription[@"tags"]; - if ([tags indexOfObject:kExclusiveTag] != NSNotFound) { - found = YES; - *stop = YES; - } - }]; - return found; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm new file mode 100644 index 0000000..3335990 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm @@ -0,0 +1,665 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Example/Tests/SpecTests/FSTSpecTests.h" + +#import +#import + +#import "Firestore/Source/Auth/FSTUser.h" +#import "Firestore/Source/Core/FSTEventManager.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Core/FSTViewSnapshot.h" +#import "Firestore/Source/Local/FSTEagerGarbageCollector.h" +#import "Firestore/Source/Local/FSTNoOpGarbageCollector.h" +#import "Firestore/Source/Local/FSTPersistence.h" +#import "Firestore/Source/Local/FSTQueryData.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Remote/FSTExistenceFilter.h" +#import "Firestore/Source/Remote/FSTWatchChange.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTClasses.h" +#import "Firestore/Source/Util/FSTLogger.h" + +#import "Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h" +#import "Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h" +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +// Disables all other tests; useful for debugging. Multiple tests can have this tag and they'll all +// be run (but all others won't). +static NSString *const kExclusiveTag = @"exclusive"; + +// A tag for tests that should be excluded from execution (on iOS), useful to allow the platforms +// to temporarily diverge. +static NSString *const kNoIOSTag = @"no-ios"; + +@interface FSTSpecTests () +@property(nonatomic, strong) FSTSyncEngineTestDriver *driver; + +// Some config info for the currently running spec; used when restarting the driver (for doRestart). +@property(nonatomic, assign) BOOL GCEnabled; +@property(nonatomic, strong) id driverPersistence; +@end + +@implementation FSTSpecTests + +- (id)persistence { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (void)setUpForSpecWithConfig:(NSDictionary *)config { + // Store persistence / GCEnabled so we can re-use it in doRestart. + self.driverPersistence = [self persistence]; + NSNumber *GCEnabled = config[@"useGarbageCollection"]; + self.GCEnabled = [GCEnabled boolValue]; + self.driver = [[FSTSyncEngineTestDriver alloc] initWithPersistence:self.driverPersistence + garbageCollector:self.garbageCollector]; + [self.driver start]; +} + +- (void)tearDownForSpec { + [self.driver shutdown]; + [self.driverPersistence shutdown]; +} + +/** + * Creates the appropriate garbage collector for the test configuration: an eager collector if + * GC is enabled or a no-op collector otherwise. + */ +- (id)garbageCollector { + return self.GCEnabled ? [[FSTEagerGarbageCollector alloc] init] + : [[FSTNoOpGarbageCollector alloc] init]; +} + +/** + * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for + * FSTSpecTests since it is incomplete without the implementations supplied by its subclasses. + */ +- (BOOL)isTestBaseClass { + return [self class] == [FSTSpecTests class]; +} + +#pragma mark - Methods for constructing objects from specs. + +- (nullable FSTQuery *)parseQuery:(id)querySpec { + if ([querySpec isKindOfClass:[NSString class]]) { + return FSTTestQuery(querySpec); + } else if ([querySpec isKindOfClass:[NSDictionary class]]) { + NSDictionary *queryDict = (NSDictionary *)querySpec; + NSString *path = queryDict[@"path"]; + __block FSTQuery *query = FSTTestQuery(path); + if (queryDict[@"limit"]) { + NSNumber *limit = queryDict[@"limit"]; + query = [query queryBySettingLimit:limit.integerValue]; + } + if (queryDict[@"filters"]) { + NSArray *filters = queryDict[@"filters"]; + [filters enumerateObjectsUsingBlock:^(NSArray *_Nonnull filter, NSUInteger idx, + BOOL *_Nonnull stop) { + query = [query queryByAddingFilter:FSTTestFilter(filter[0], filter[1], filter[2])]; + }]; + } + if (queryDict[@"orderBys"]) { + NSArray *orderBys = queryDict[@"orderBys"]; + [orderBys enumerateObjectsUsingBlock:^(NSArray *_Nonnull orderBy, NSUInteger idx, + BOOL *_Nonnull stop) { + query = [query queryByAddingSortOrder:FSTTestOrderBy(orderBy[0], orderBy[1])]; + }]; + } + return query; + } else { + XCTFail(@"Invalid query: %@", querySpec); + return nil; + } +} + +- (FSTSnapshotVersion *)parseVersion:(NSNumber *_Nullable)version { + return FSTTestVersion(version.longLongValue); +} + +- (FSTDocumentViewChange *)parseChange:(NSArray *)change ofType:(FSTDocumentViewChangeType)type { + BOOL hasMutations = NO; + for (NSUInteger i = 3; i < change.count; ++i) { + if ([change[i] isEqual:@"local"]) { + hasMutations = YES; + } + } + NSNumber *version = change[1]; + FSTDocument *doc = FSTTestDoc(change[0], version.longLongValue, change[2], hasMutations); + return [FSTDocumentViewChange changeWithDocument:doc type:type]; +} + +#pragma mark - Methods for doing the steps of the spec test. + +- (void)doListen:(NSArray *)listenSpec { + FSTQuery *query = [self parseQuery:listenSpec[1]]; + FSTTargetID actualID = [self.driver addUserListenerWithQuery:query]; + + FSTTargetID expectedID = [listenSpec[0] intValue]; + XCTAssertEqual(actualID, expectedID, @"targetID assigned to listen"); +} + +- (void)doUnlisten:(NSArray *)unlistenSpec { + FSTQuery *query = [self parseQuery:unlistenSpec[1]]; + [self.driver removeUserListenerWithQuery:query]; +} + +- (void)doSet:(NSArray *)setSpec { + [self.driver writeUserMutation:FSTTestSetMutation(setSpec[0], setSpec[1])]; +} + +- (void)doPatch:(NSArray *)patchSpec { + [self.driver writeUserMutation:FSTTestPatchMutation(patchSpec[0], patchSpec[1], nil)]; +} + +- (void)doDelete:(NSString *)key { + [self.driver writeUserMutation:FSTTestDeleteMutation(key)]; +} + +- (void)doWatchAck:(NSArray *)ackedTargets snapshot:(NSNumber *)watchSnapshot { + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded + targetIDs:ackedTargets + cause:nil]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; +} + +- (void)doWatchCurrent:(NSArray *)currentSpec snapshot:(NSNumber *)watchSnapshot { + NSArray *currentTargets = currentSpec[0]; + NSData *resumeToken = [currentSpec[1] dataUsingEncoding:NSUTF8StringEncoding]; + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:currentTargets + resumeToken:resumeToken]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; +} + +- (void)doWatchRemove:(NSDictionary *)watchRemoveSpec snapshot:(NSNumber *)watchSnapshot { + NSError *error = nil; + NSDictionary *cause = watchRemoveSpec[@"cause"]; + if (cause) { + int code = ((NSNumber *)cause[@"code"]).intValue; + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey : @"Error from watchRemove.", + }; + error = [NSError errorWithDomain:FIRFirestoreErrorDomain code:code userInfo:userInfo]; + } + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved + targetIDs:watchRemoveSpec[@"targetIds"] + cause:error]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; + // Unlike web, the FSTMockDatastore detects a watch removal with cause and will remove active + // targets +} + +- (void)doWatchEntity:(NSDictionary *)watchEntity snapshot:(NSNumber *_Nullable)watchSnapshot { + if (watchEntity[@"docs"]) { + FSTAssert(!watchEntity[@"doc"], @"Exactly one of |doc| or |docs| needs to be set."); + int count = 0; + NSArray *docs = watchEntity[@"docs"]; + for (NSDictionary *doc in docs) { + count++; + bool isLast = (count == docs.count); + NSMutableDictionary *watchSpec = [NSMutableDictionary dictionary]; + watchSpec[@"doc"] = doc; + if (watchEntity[@"targets"]) { + watchSpec[@"targets"] = watchEntity[@"targets"]; + } + if (watchEntity[@"removedTargets"]) { + watchSpec[@"removedTargets"] = watchEntity[@"removedTargets"]; + } + NSNumber *_Nullable version = nil; + if (isLast) { + version = watchSnapshot; + } + [self doWatchEntity:watchSpec snapshot:version]; + } + } else if (watchEntity[@"doc"]) { + NSArray *docSpec = watchEntity[@"doc"]; + FSTDocumentKey *key = FSTTestDocKey(docSpec[0]); + FSTObjectValue *value = FSTTestObjectValue(docSpec[2]); + FSTSnapshotVersion *version = [self parseVersion:docSpec[1]]; + FSTMaybeDocument *doc = + [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; + FSTWatchChange *change = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:watchEntity[@"targets"] + removedTargetIDs:watchEntity[@"removedTargets"] + documentKey:doc.key + document:doc]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; + } else if (watchEntity[@"key"]) { + FSTDocumentKey *docKey = FSTTestDocKey(watchEntity[@"key"]); + FSTWatchChange *change = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:watchEntity[@"removedTargets"] + documentKey:docKey + document:nil]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; + } else { + FSTFail(@"Either key, doc or docs must be set."); + } +} + +- (void)doWatchFilter:(NSArray *)watchFilter snapshot:(NSNumber *_Nullable)watchSnapshot { + NSArray *targets = watchFilter[0]; + FSTAssert(targets.count == 1, @"ExistenceFilters currently support exactly one target only."); + + int keyCount = watchFilter.count == 0 ? 0 : (int)watchFilter.count - 1; + + // TODO(dimond): extend this with different existence filters over time. + FSTExistenceFilter *filter = [FSTExistenceFilter filterWithCount:keyCount]; + FSTExistenceFilterWatchChange *change = + [FSTExistenceFilterWatchChange changeWithFilter:filter targetID:targets[0].intValue]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; +} + +- (void)doWatchReset:(NSArray *)watchReset snapshot:(NSNumber *_Nullable)watchSnapshot { + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:watchReset + cause:nil]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; +} + +- (void)doWatchStreamClose:(NSDictionary *)closeSpec { + NSDictionary *errorSpec = closeSpec[@"error"]; + int code = ((NSNumber *)(errorSpec[@"code"])).intValue; + [self.driver receiveWatchStreamError:code userInfo:errorSpec]; +} + +- (void)doWriteAck:(NSDictionary *)spec { + FSTSnapshotVersion *version = [self parseVersion:spec[@"version"]]; + NSNumber *expectUserCallback = spec[@"expectUserCallback"]; + + FSTMutationResult *mutationResult = + [[FSTMutationResult alloc] initWithVersion:version transformResults:nil]; + FSTOutstandingWrite *write = + [self.driver receiveWriteAckWithVersion:version mutationResults:@[ mutationResult ]]; + + if (expectUserCallback.boolValue) { + FSTAssert(write.done, @"Write should be done"); + FSTAssert(!write.error, @"Ack should not fail"); + } +} + +- (void)doFailWrite:(NSDictionary *)spec { + NSDictionary *errorSpec = spec[@"error"]; + NSNumber *expectUserCallback = spec[@"expectUserCallback"]; + + int code = ((NSNumber *)(errorSpec[@"code"])).intValue; + FSTOutstandingWrite *write = [self.driver receiveWriteError:code userInfo:errorSpec]; + + if (expectUserCallback.boolValue) { + FSTAssert(write.done, @"Write should be done"); + XCTAssertNotNil(write.error, @"Write should have failed"); + XCTAssertEqualObjects(write.error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(write.error.code, code); + } +} + +- (void)doDisableNetwork { + [self.driver disableNetwork]; +} + +- (void)doEnableNetwork { + [self.driver enableNetwork]; +} + +- (void)doChangeUser:(id)UID { + FSTUser *user = [UID isEqual:[NSNull null]] ? [FSTUser unauthenticatedUser] + : [[FSTUser alloc] initWithUID:UID]; + [self.driver changeUser:user]; +} + +- (void)doRestart { + // Any outstanding user writes should be automatically re-sent, so we want to preserve them + // when re-creating the driver. + FSTOutstandingWriteQueues *outstandingWrites = self.driver.outstandingWrites; + + [self.driver shutdown]; + + // NOTE: We intentionally don't shutdown / re-create driverPersistence, since we want to + // preserve the persisted state. This is a bit of a cheat since it means we're not exercising + // the initialization / start logic that would normally be hit, but simplifies the plumbing and + // allows us to run these tests against FSTMemoryPersistence as well (there would be no way to + // re-create FSTMemoryPersistence without losing all persisted state). + + self.driver = [[FSTSyncEngineTestDriver alloc] initWithPersistence:self.driverPersistence + garbageCollector:self.garbageCollector + initialUser:self.driver.currentUser + outstandingWrites:outstandingWrites]; + [self.driver start]; +} + +- (void)doStep:(NSDictionary *)step { + if (step[@"userListen"]) { + [self doListen:step[@"userListen"]]; + } else if (step[@"userUnlisten"]) { + [self doUnlisten:step[@"userUnlisten"]]; + } else if (step[@"userSet"]) { + [self doSet:step[@"userSet"]]; + } else if (step[@"userPatch"]) { + [self doPatch:step[@"userPatch"]]; + } else if (step[@"userDelete"]) { + [self doDelete:step[@"userDelete"]]; + } else if (step[@"watchAck"]) { + [self doWatchAck:step[@"watchAck"] snapshot:step[@"watchSnapshot"]]; + } else if (step[@"watchCurrent"]) { + [self doWatchCurrent:step[@"watchCurrent"] snapshot:step[@"watchSnapshot"]]; + } else if (step[@"watchRemove"]) { + [self doWatchRemove:step[@"watchRemove"] snapshot:step[@"watchSnapshot"]]; + } else if (step[@"watchEntity"]) { + [self doWatchEntity:step[@"watchEntity"] snapshot:step[@"watchSnapshot"]]; + } else if (step[@"watchFilter"]) { + [self doWatchFilter:step[@"watchFilter"] snapshot:step[@"watchSnapshot"]]; + } else if (step[@"watchReset"]) { + [self doWatchReset:step[@"watchReset"] snapshot:step[@"watchSnapshot"]]; + } else if (step[@"watchStreamClose"]) { + [self doWatchStreamClose:step[@"watchStreamClose"]]; + } else if (step[@"watchProto"]) { + // watchProto isn't yet used, and it's unclear how to create arbitrary protos from JSON. + FSTFail(@"watchProto is not yet supported."); + } else if (step[@"writeAck"]) { + [self doWriteAck:step[@"writeAck"]]; + } else if (step[@"failWrite"]) { + [self doFailWrite:step[@"failWrite"]]; + } else if (step[@"enableNetwork"]) { + if ([step[@"enableNetwork"] boolValue]) { + [self doEnableNetwork]; + } else { + [self doDisableNetwork]; + } + } else if (step[@"changeUser"]) { + [self doChangeUser:step[@"changeUser"]]; + } else if (step[@"restart"]) { + [self doRestart]; + } else { + XCTFail(@"Unknown step: %@", step); + } +} + +- (void)validateEvent:(FSTQueryEvent *)actual matches:(NSDictionary *)expected { + FSTQuery *expectedQuery = [self parseQuery:expected[@"query"]]; + XCTAssertEqualObjects(actual.query, expectedQuery); + if ([expected[@"errorCode"] integerValue] != 0) { + XCTAssertNotNil(actual.error); + XCTAssertEqual(actual.error.code, [expected[@"errorCode"] integerValue]); + } else { + NSMutableArray *expectedChanges = [NSMutableArray array]; + NSMutableArray *removed = expected[@"removed"]; + for (NSArray *changeSpec in removed) { + [expectedChanges + addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeRemoved]]; + } + NSMutableArray *added = expected[@"added"]; + for (NSArray *changeSpec in added) { + [expectedChanges + addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeAdded]]; + } + NSMutableArray *modified = expected[@"modified"]; + for (NSArray *changeSpec in modified) { + [expectedChanges + addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeModified]]; + } + NSMutableArray *metadata = expected[@"metadata"]; + for (NSArray *changeSpec in metadata) { + [expectedChanges + addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeMetadata]]; + } + XCTAssertEqualObjects(actual.viewSnapshot.documentChanges, expectedChanges); + + BOOL expectedHasPendingWrites = + expected[@"hasPendingWrites"] ? [expected[@"hasPendingWrites"] boolValue] : NO; + BOOL expectedIsFromCache = expected[@"fromCache"] ? [expected[@"fromCache"] boolValue] : NO; + XCTAssertEqual(actual.viewSnapshot.hasPendingWrites, expectedHasPendingWrites, + @"hasPendingWrites"); + XCTAssertEqual(actual.viewSnapshot.isFromCache, expectedIsFromCache, @"isFromCache"); + } +} + +- (void)validateStepExpectations:(NSMutableArray *_Nullable)stepExpectations { + NSArray *events = self.driver.capturedEventsSinceLastCall; + + if (!stepExpectations) { + XCTAssertEqual(events.count, 0); + for (FSTQueryEvent *event in events) { + XCTFail(@"Unexpected event: %@", event); + } + return; + } + + events = + [events sortedArrayUsingComparator:^NSComparisonResult(FSTQueryEvent *q1, FSTQueryEvent *q2) { + return [q1.query.canonicalID compare:q2.query.canonicalID]; + }]; + + XCTAssertEqual(events.count, stepExpectations.count); + NSUInteger i = 0; + for (; i < stepExpectations.count && i < events.count; ++i) { + [self validateEvent:events[i] matches:stepExpectations[i]]; + } + for (; i < stepExpectations.count; ++i) { + XCTFail(@"Missing event: %@", stepExpectations[i]); + } + for (; i < events.count; ++i) { + XCTFail(@"Unexpected event: %@", events[i]); + } +} + +- (void)validateStateExpectations:(nullable NSDictionary *)expected { + if (expected) { + if (expected[@"numOutstandingWrites"]) { + XCTAssertEqual([self.driver sentWritesCount], [expected[@"numOutstandingWrites"] intValue]); + } + if (expected[@"writeStreamRequestCount"]) { + XCTAssertEqual([self.driver writeStreamRequestCount], + [expected[@"writeStreamRequestCount"] intValue]); + } + if (expected[@"watchStreamRequestCount"]) { + XCTAssertEqual([self.driver watchStreamRequestCount], + [expected[@"watchStreamRequestCount"] intValue]); + } + if (expected[@"limboDocs"]) { + NSMutableSet *expectedLimboDocuments = [NSMutableSet set]; + NSArray *docNames = expected[@"limboDocs"]; + for (NSString *name in docNames) { + [expectedLimboDocuments addObject:FSTTestDocKey(name)]; + } + // Update the expected limbo documents + self.driver.expectedLimboDocuments = expectedLimboDocuments; + } + if (expected[@"activeTargets"]) { + NSMutableDictionary *expectedActiveTargets = [NSMutableDictionary dictionary]; + [expected[@"activeTargets"] enumerateKeysAndObjectsUsingBlock:^(NSString *targetIDString, + NSDictionary *queryData, + BOOL *stop) { + FSTTargetID targetID = [targetIDString intValue]; + FSTQuery *query = [self parseQuery:queryData[@"query"]]; + NSData *resumeToken = [queryData[@"resumeToken"] dataUsingEncoding:NSUTF8StringEncoding]; + // TODO(mcg): populate the purpose of the target once it's possible to encode that in the + // spec tests. For now, hard-code that it's a listen despite the fact that it's not always + // the right value. + expectedActiveTargets[@(targetID)] = + [[FSTQueryData alloc] initWithQuery:query + targetID:targetID + listenSequenceNumber:0 + purpose:FSTQueryPurposeListen + snapshotVersion:[FSTSnapshotVersion noVersion] + resumeToken:resumeToken]; + }]; + self.driver.expectedActiveTargets = expectedActiveTargets; + } + } + + // Always validate that the expected limbo docs match the actual limbo docs. + [self validateLimboDocuments]; + // Always validate that the expected active targets match the actual active targets. + [self validateActiveTargets]; +} + +- (void)validateLimboDocuments { + // Make a copy so it can modified while checking against the expected limbo docs. + NSMutableDictionary *actualLimboDocs = + [NSMutableDictionary dictionaryWithDictionary:self.driver.currentLimboDocuments]; + + // Validate that each limbo doc has an expected active target + [actualLimboDocs enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, + FSTBoxedTargetID *targetID, BOOL *stop) { + XCTAssertNotNil(self.driver.expectedActiveTargets[targetID], + @"Found limbo doc without an expected active target"); + }]; + + for (FSTDocumentKey *expectedLimboDoc in self.driver.expectedLimboDocuments) { + XCTAssertNotNil(actualLimboDocs[expectedLimboDoc], + @"Expected doc to be in limbo, but was not: %@", expectedLimboDoc); + [actualLimboDocs removeObjectForKey:expectedLimboDoc]; + } + XCTAssertTrue(actualLimboDocs.count == 0, "Unexpected docs in limbo: %@", actualLimboDocs); +} + +- (void)validateActiveTargets { + // Create a copy so we can modify it in tests + NSMutableDictionary *actualTargets = + [NSMutableDictionary dictionaryWithDictionary:self.driver.activeTargets]; + + [self.driver.expectedActiveTargets enumerateKeysAndObjectsUsingBlock:^(FSTBoxedTargetID *targetID, + FSTQueryData *queryData, + BOOL *stop) { + XCTAssertNotNil(actualTargets[targetID], @"Expected active target not found: %@", queryData); + + // TODO(mcg): validate the purpose of the target once it's possible to encode that in the + // spec tests. For now, only validate properties that can be validated. + // XCTAssertEqualObjects(actualTargets[targetID], queryData); + + FSTQueryData *actual = actualTargets[targetID]; + XCTAssertEqualObjects(actual.query, queryData.query); + XCTAssertEqual(actual.targetID, queryData.targetID); + XCTAssertEqualObjects(actual.snapshotVersion, queryData.snapshotVersion); + XCTAssertEqualObjects(actual.resumeToken, queryData.resumeToken); + + [actualTargets removeObjectForKey:targetID]; + }]; + XCTAssertTrue(actualTargets.count == 0, "Unexpected active targets: %@", actualTargets); +} + +- (void)runSpecTestSteps:(NSArray *)steps config:(NSDictionary *)config { + @try { + [self setUpForSpecWithConfig:config]; + for (NSDictionary *step in steps) { + FSTLog(@"Doing step %@", step); + [self doStep:step]; + [self validateStepExpectations:step[@"expect"]]; + [self validateStateExpectations:step[@"stateExpect"]]; + } + [self.driver validateUsage]; + } @finally { + // Ensure that the driver is torn down even if the test is failing due to a thrown exception so + // that any resources held by the driver are released. This is important when the driver is + // backed by LevelDB because LevelDB locks its database. If -tearDownForSpec were not called + // after an exception then subsequent attempts to open the LevelDB will fail, making it harder + // to zero in on the spec tests as a culprit. + [self tearDownForSpec]; + } +} + +#pragma mark - The actual test methods. + +- (void)testSpecTests { + if ([self isTestBaseClass]) return; + + // Enumerate the .json files containing the spec tests. + NSMutableArray *specFiles = [NSMutableArray array]; + NSMutableArray *parsedSpecs = [NSMutableArray array]; + NSBundle *bundle = [NSBundle bundleForClass:[self class]]; + NSFileManager *fs = [NSFileManager defaultManager]; + BOOL exclusiveMode = NO; + for (NSString *file in [fs enumeratorAtPath:[bundle bundlePath]]) { + if (![@"json" isEqual:[file pathExtension]]) { + continue; + } + + // Read and parse the JSON from the file. + NSString *fileName = [file stringByDeletingPathExtension]; + NSString *path = [bundle pathForResource:fileName ofType:@"json"]; + NSData *json = [NSData dataWithContentsOfFile:path]; + XCTAssertNotNil(json); + NSError *error = nil; + id _Nullable parsed = [NSJSONSerialization JSONObjectWithData:json options:0 error:&error]; + XCTAssertNil(error, @"%@", error); + XCTAssertTrue([parsed isKindOfClass:[NSDictionary class]]); + NSDictionary *testDict = (NSDictionary *)parsed; + + exclusiveMode = exclusiveMode || [self anyTestsAreMarkedExclusive:testDict]; + [specFiles addObject:fileName]; + [parsedSpecs addObject:testDict]; + } + + // Now iterate over them and run them. + __block bool ranAtLeastOneTest = NO; + for (NSUInteger i = 0; i < specFiles.count; i++) { + NSLog(@"Spec test file: %@", specFiles[i]); + // Iterate over the tests in the file and run them. + [parsedSpecs[i] enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + XCTAssertTrue([obj isKindOfClass:[NSDictionary class]]); + NSDictionary *testDescription = (NSDictionary *)obj; + NSString *describeName = testDescription[@"describeName"]; + NSString *itName = testDescription[@"itName"]; + NSString *name = [NSString stringWithFormat:@"%@ %@", describeName, itName]; + NSDictionary *config = testDescription[@"config"]; + NSArray *steps = testDescription[@"steps"]; + NSArray *tags = testDescription[@"tags"]; + + BOOL runTest = !exclusiveMode || [tags indexOfObject:kExclusiveTag] != NSNotFound; + if ([tags indexOfObject:kNoIOSTag] != NSNotFound) { + runTest = NO; + } + if (runTest) { + NSLog(@" Spec test: %@", name); + [self runSpecTestSteps:steps config:config]; + ranAtLeastOneTest = YES; + } else { + NSLog(@" [SKIPPED] Spec test: %@", name); + } + }]; + } + XCTAssertTrue(ranAtLeastOneTest); +} + +- (BOOL)anyTestsAreMarkedExclusive:(NSDictionary *)tests { + __block BOOL found = NO; + [tests enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + XCTAssertTrue([obj isKindOfClass:[NSDictionary class]]); + NSDictionary *testDescription = (NSDictionary *)obj; + NSArray *tags = testDescription[@"tags"]; + if ([tags indexOfObject:kExclusiveTag] != NSNotFound) { + found = YES; + *stop = YES; + } + }]; + return found; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m deleted file mode 100644 index c23fa28..0000000 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m +++ /dev/null @@ -1,317 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h" - -#import -#import - -#import "Firestore/Source/Auth/FSTUser.h" -#import "Firestore/Source/Core/FSTEventManager.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Core/FSTSyncEngine.h" -#import "Firestore/Source/Local/FSTLocalStore.h" -#import "Firestore/Source/Local/FSTPersistence.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Remote/FSTDatastore.h" -#import "Firestore/Source/Remote/FSTWatchChange.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTDispatchQueue.h" -#import "Firestore/Source/Util/FSTLogger.h" - -#import "Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h" -#import "Firestore/Example/Tests/SpecTests/FSTMockDatastore.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTQueryEvent - -- (NSString *)description { - // The Query is also included in the view, so we skip it. - return [NSString stringWithFormat:@"", - self.viewSnapshot, self.error]; -} - -@end - -@implementation FSTOutstandingWrite -@end - -@interface FSTSyncEngineTestDriver () - -#pragma mark - Parts of the Firestore system that the spec tests need to control. - -@property(nonatomic, strong, readonly) FSTMockDatastore *datastore; -@property(nonatomic, strong, readonly) FSTEventManager *eventManager; -@property(nonatomic, strong, readonly) FSTRemoteStore *remoteStore; -@property(nonatomic, strong, readonly) FSTLocalStore *localStore; -@property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine; - -#pragma mark - Data structures for holding events sent by the watch stream. - -/** A block for the FSTEventAggregator to use to report events to the test. */ -@property(nonatomic, strong, readonly) void (^eventHandler)(FSTQueryEvent *); -/** The events received by our eventHandler and not yet retrieved via capturedEventsSinceLastCall */ -@property(nonatomic, strong, readonly) NSMutableArray *events; -/** A dictionary for tracking the listens on queries. */ -@property(nonatomic, strong, readonly) - NSMutableDictionary *queryListeners; - -#pragma mark - Other data structures. -@property(nonatomic, strong, readwrite) FSTUser *currentUser; - -@end - -@implementation FSTSyncEngineTestDriver { - // ivar is declared as mutable. - NSMutableDictionary *> *_outstandingWrites; -} - -- (instancetype)initWithPersistence:(id)persistence - garbageCollector:(id)garbageCollector { - return [self initWithPersistence:persistence - garbageCollector:garbageCollector - initialUser:[FSTUser unauthenticatedUser] - outstandingWrites:@{}]; -} - -- (instancetype)initWithPersistence:(id)persistence - garbageCollector:(id)garbageCollector - initialUser:(FSTUser *)initialUser - outstandingWrites:(FSTOutstandingWriteQueues *)outstandingWrites { - if (self = [super init]) { - // Create mutable copy of outstandingWrites. - _outstandingWrites = [NSMutableDictionary dictionary]; - [outstandingWrites enumerateKeysAndObjectsUsingBlock:^( - FSTUser *user, NSArray *writes, BOOL *stop) { - _outstandingWrites[user] = [writes mutableCopy]; - }]; - - _events = [NSMutableArray array]; - - // Set up the sync engine and various stores. - dispatch_queue_t mainQueue = dispatch_get_main_queue(); - FSTDispatchQueue *dispatchQueue = [FSTDispatchQueue queueWith:mainQueue]; - _localStore = [[FSTLocalStore alloc] initWithPersistence:persistence - garbageCollector:garbageCollector - initialUser:initialUser]; - _datastore = [FSTMockDatastore mockDatastoreWithWorkerDispatchQueue:dispatchQueue]; - - _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:_datastore]; - - _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore - remoteStore:_remoteStore - initialUser:initialUser]; - _remoteStore.syncEngine = _syncEngine; - _eventManager = [FSTEventManager eventManagerWithSyncEngine:_syncEngine]; - - _remoteStore.onlineStateDelegate = self; - - // Set up internal event tracking for the spec tests. - NSMutableArray *events = [NSMutableArray array]; - _eventHandler = ^(FSTQueryEvent *e) { - [events addObject:e]; - }; - _events = events; - - _queryListeners = [NSMutableDictionary dictionary]; - - _expectedLimboDocuments = [NSSet set]; - - _expectedActiveTargets = [NSDictionary dictionary]; - - _currentUser = initialUser; - } - return self; -} - -- (void)applyChangedOnlineState:(FSTOnlineState)onlineState { - [self.syncEngine applyChangedOnlineState:onlineState]; - [self.eventManager applyChangedOnlineState:onlineState]; -} - -- (void)start { - [self.localStore start]; - [self.remoteStore start]; -} - -- (void)validateUsage { - // We could relax this if we found a reason to. - FSTAssert(self.events.count == 0, - @"You must clear all pending events by calling" - " capturedEventsSinceLastCall before calling shutdown."); -} - -- (void)shutdown { - [self.remoteStore shutdown]; - [self.localStore shutdown]; -} - -- (void)validateNextWriteSent:(FSTMutation *)expectedWrite { - NSArray *request = [self.datastore nextSentWrite]; - // Make sure the write went through the pipe like we expected it to. - FSTAssert(request.count == 1, @"Only single mutation requests are supported at the moment"); - FSTMutation *actualWrite = request[0]; - FSTAssert([actualWrite isEqual:expectedWrite], - @"Mock datastore received write %@ but first outstanding mutation was %@", actualWrite, - expectedWrite); - FSTLog(@"A write was sent: %@", actualWrite); -} - -- (int)sentWritesCount { - return [self.datastore writesSent]; -} - -- (int)writeStreamRequestCount { - return [self.datastore writeStreamRequestCount]; -} - -- (int)watchStreamRequestCount { - return [self.datastore watchStreamRequestCount]; -} - -- (void)disableNetwork { - // Make sure to execute all writes that are currently queued. This allows us - // to assert on the total number of requests sent before shutdown. - [self.remoteStore fillWritePipeline]; - [self.remoteStore disableNetwork]; -} - -- (void)enableNetwork { - [self.remoteStore enableNetwork]; -} - -- (void)changeUser:(FSTUser *)user { - self.currentUser = user; - [self.syncEngine userDidChange:user]; -} - -- (FSTOutstandingWrite *)receiveWriteAckWithVersion:(FSTSnapshotVersion *)commitVersion - mutationResults: - (NSArray *)mutationResults { - FSTOutstandingWrite *write = [self currentOutstandingWrites].firstObject; - [[self currentOutstandingWrites] removeObjectAtIndex:0]; - [self validateNextWriteSent:write.write]; - - [self.datastore ackWriteWithVersion:commitVersion mutationResults:mutationResults]; - - return write; -} - -- (FSTOutstandingWrite *)receiveWriteError:(int)errorCode - userInfo:(NSDictionary *)userInfo { - NSError *error = - [NSError errorWithDomain:FIRFirestoreErrorDomain code:errorCode userInfo:userInfo]; - - FSTOutstandingWrite *write = [self currentOutstandingWrites].firstObject; - [self validateNextWriteSent:write.write]; - - // If this is a permanent error, the mutation is not expected to be sent again so we remove it - // from currentOutstandingWrites. - if ([FSTDatastore isPermanentWriteError:error]) { - [[self currentOutstandingWrites] removeObjectAtIndex:0]; - } - - FSTLog(@"Failing a write."); - [self.datastore failWriteWithError:error]; - - return write; -} - -- (NSArray *)capturedEventsSinceLastCall { - NSArray *result = [self.events copy]; - [self.events removeAllObjects]; - return result; -} - -- (FSTTargetID)addUserListenerWithQuery:(FSTQuery *)query { - // TODO(dimond): Allow customizing listen options in spec tests - // TODO(dimond): Change spec tests to verify isFromCache on snapshots - FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES - includeDocumentMetadataChanges:YES - waitForSyncWhenOnline:NO]; - FSTQueryListener *listener = [[FSTQueryListener alloc] - initWithQuery:query - options:options - viewSnapshotHandler:^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error) { - FSTQueryEvent *event = [[FSTQueryEvent alloc] init]; - event.query = query; - event.viewSnapshot = snapshot; - event.error = error; - [self.events addObject:event]; - }]; - self.queryListeners[query] = listener; - return [self.eventManager addListener:listener]; -} - -- (void)removeUserListenerWithQuery:(FSTQuery *)query { - FSTQueryListener *listener = self.queryListeners[query]; - [self.queryListeners removeObjectForKey:query]; - [self.eventManager removeListener:listener]; -} - -- (void)writeUserMutation:(FSTMutation *)mutation { - FSTOutstandingWrite *write = [[FSTOutstandingWrite alloc] init]; - write.write = mutation; - [[self currentOutstandingWrites] addObject:write]; - FSTLog(@"sending a user write."); - [self.syncEngine writeMutations:@[ mutation ] - completion:^(NSError *_Nullable error) { - FSTLog(@"A callback was called with error: %@", error); - write.done = YES; - write.error = error; - }]; -} - -- (void)receiveWatchChange:(FSTWatchChange *)change - snapshotVersion:(FSTSnapshotVersion *_Nullable)snapshot { - [self.datastore writeWatchChange:change snapshotVersion:snapshot]; -} - -- (void)receiveWatchStreamError:(int)errorCode userInfo:(NSDictionary *)userInfo { - NSError *error = - [NSError errorWithDomain:FIRFirestoreErrorDomain code:errorCode userInfo:userInfo]; - - [self.datastore failWatchStreamWithError:error]; - // Unlike web, stream should re-open synchronously (if we have any listeners) - if (self.queryListeners.count > 0) { - FSTAssert(self.datastore.isWatchStreamOpen, @"Watch stream is open"); - } -} - -- (NSDictionary *)currentLimboDocuments { - return [self.syncEngine currentLimboDocuments]; -} - -- (NSDictionary *)activeTargets { - return [[self.datastore activeTargets] copy]; -} - -#pragma mark - Helper Methods - -- (NSMutableArray *)currentOutstandingWrites { - NSMutableArray *writes = _outstandingWrites[self.currentUser]; - if (!writes) { - writes = [NSMutableArray array]; - _outstandingWrites[self.currentUser] = writes; - } - return writes; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm new file mode 100644 index 0000000..a4de615 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm @@ -0,0 +1,322 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h" + +#import +#import + +#import "Firestore/Source/Auth/FSTUser.h" +#import "Firestore/Source/Core/FSTEventManager.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Core/FSTSyncEngine.h" +#import "Firestore/Source/Local/FSTLocalStore.h" +#import "Firestore/Source/Local/FSTPersistence.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Remote/FSTDatastore.h" +#import "Firestore/Source/Remote/FSTWatchChange.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTDispatchQueue.h" +#import "Firestore/Source/Util/FSTLogger.h" + +#import "Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h" +#import "Firestore/Example/Tests/SpecTests/FSTMockDatastore.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTQueryEvent + +- (NSString *)description { + // The Query is also included in the view, so we skip it. + return [NSString stringWithFormat:@"", + self.viewSnapshot, self.error]; +} + +@end + +@implementation FSTOutstandingWrite +@end + +@interface FSTSyncEngineTestDriver () + +#pragma mark - Parts of the Firestore system that the spec tests need to control. + +@property(nonatomic, strong, readonly) FSTMockDatastore *datastore; +@property(nonatomic, strong, readonly) FSTEventManager *eventManager; +@property(nonatomic, strong, readonly) FSTRemoteStore *remoteStore; +@property(nonatomic, strong, readonly) FSTLocalStore *localStore; +@property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine; + +#pragma mark - Data structures for holding events sent by the watch stream. + +/** A block for the FSTEventAggregator to use to report events to the test. */ +@property(nonatomic, strong, readonly) void (^eventHandler)(FSTQueryEvent *); +/** The events received by our eventHandler and not yet retrieved via capturedEventsSinceLastCall */ +@property(nonatomic, strong, readonly) NSMutableArray *events; +/** A dictionary for tracking the listens on queries. */ +@property(nonatomic, strong, readonly) + NSMutableDictionary *queryListeners; + +#pragma mark - Other data structures. +@property(nonatomic, strong, readwrite) FSTUser *currentUser; + +@end + +@implementation FSTSyncEngineTestDriver { + // ivar is declared as mutable. + NSMutableDictionary *> *_outstandingWrites; +} + +- (instancetype)initWithPersistence:(id)persistence + garbageCollector:(id)garbageCollector { + return [self initWithPersistence:persistence + garbageCollector:garbageCollector + initialUser:[FSTUser unauthenticatedUser] + outstandingWrites:@{}]; +} + +- (instancetype)initWithPersistence:(id)persistence + garbageCollector:(id)garbageCollector + initialUser:(FSTUser *)initialUser + outstandingWrites:(FSTOutstandingWriteQueues *)outstandingWrites { + if (self = [super init]) { + // Create mutable copy of outstandingWrites. + _outstandingWrites = [NSMutableDictionary dictionary]; + [outstandingWrites enumerateKeysAndObjectsUsingBlock:^( + FSTUser *user, NSArray *writes, BOOL *stop) { + _outstandingWrites[user] = [writes mutableCopy]; + }]; + + _events = [NSMutableArray array]; + + // Set up the sync engine and various stores. + dispatch_queue_t mainQueue = dispatch_get_main_queue(); + FSTDispatchQueue *dispatchQueue = [FSTDispatchQueue queueWith:mainQueue]; + _localStore = [[FSTLocalStore alloc] initWithPersistence:persistence + garbageCollector:garbageCollector + initialUser:initialUser]; + _datastore = [FSTMockDatastore mockDatastoreWithWorkerDispatchQueue:dispatchQueue]; + + _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:_datastore]; + + _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore + remoteStore:_remoteStore + initialUser:initialUser]; + _remoteStore.syncEngine = _syncEngine; + _eventManager = [FSTEventManager eventManagerWithSyncEngine:_syncEngine]; + + _remoteStore.onlineStateDelegate = self; + + // Set up internal event tracking for the spec tests. + NSMutableArray *events = [NSMutableArray array]; + _eventHandler = ^(FSTQueryEvent *e) { + [events addObject:e]; + }; + _events = events; + + _queryListeners = [NSMutableDictionary dictionary]; + + _expectedLimboDocuments = [NSSet set]; + + _expectedActiveTargets = [NSDictionary dictionary]; + + _currentUser = initialUser; + } + return self; +} + +- (NSDictionary *> *)outstandingWrites { + return static_cast *> *>( + _outstandingWrites); +} + +- (void)applyChangedOnlineState:(FSTOnlineState)onlineState { + [self.syncEngine applyChangedOnlineState:onlineState]; + [self.eventManager applyChangedOnlineState:onlineState]; +} + +- (void)start { + [self.localStore start]; + [self.remoteStore start]; +} + +- (void)validateUsage { + // We could relax this if we found a reason to. + FSTAssert(self.events.count == 0, + @"You must clear all pending events by calling" + " capturedEventsSinceLastCall before calling shutdown."); +} + +- (void)shutdown { + [self.remoteStore shutdown]; + [self.localStore shutdown]; +} + +- (void)validateNextWriteSent:(FSTMutation *)expectedWrite { + NSArray *request = [self.datastore nextSentWrite]; + // Make sure the write went through the pipe like we expected it to. + FSTAssert(request.count == 1, @"Only single mutation requests are supported at the moment"); + FSTMutation *actualWrite = request[0]; + FSTAssert([actualWrite isEqual:expectedWrite], + @"Mock datastore received write %@ but first outstanding mutation was %@", actualWrite, + expectedWrite); + FSTLog(@"A write was sent: %@", actualWrite); +} + +- (int)sentWritesCount { + return [self.datastore writesSent]; +} + +- (int)writeStreamRequestCount { + return [self.datastore writeStreamRequestCount]; +} + +- (int)watchStreamRequestCount { + return [self.datastore watchStreamRequestCount]; +} + +- (void)disableNetwork { + // Make sure to execute all writes that are currently queued. This allows us + // to assert on the total number of requests sent before shutdown. + [self.remoteStore fillWritePipeline]; + [self.remoteStore disableNetwork]; +} + +- (void)enableNetwork { + [self.remoteStore enableNetwork]; +} + +- (void)changeUser:(FSTUser *)user { + self.currentUser = user; + [self.syncEngine userDidChange:user]; +} + +- (FSTOutstandingWrite *)receiveWriteAckWithVersion:(FSTSnapshotVersion *)commitVersion + mutationResults: + (NSArray *)mutationResults { + FSTOutstandingWrite *write = [self currentOutstandingWrites].firstObject; + [[self currentOutstandingWrites] removeObjectAtIndex:0]; + [self validateNextWriteSent:write.write]; + + [self.datastore ackWriteWithVersion:commitVersion mutationResults:mutationResults]; + + return write; +} + +- (FSTOutstandingWrite *)receiveWriteError:(int)errorCode + userInfo:(NSDictionary *)userInfo { + NSError *error = + [NSError errorWithDomain:FIRFirestoreErrorDomain code:errorCode userInfo:userInfo]; + + FSTOutstandingWrite *write = [self currentOutstandingWrites].firstObject; + [self validateNextWriteSent:write.write]; + + // If this is a permanent error, the mutation is not expected to be sent again so we remove it + // from currentOutstandingWrites. + if ([FSTDatastore isPermanentWriteError:error]) { + [[self currentOutstandingWrites] removeObjectAtIndex:0]; + } + + FSTLog(@"Failing a write."); + [self.datastore failWriteWithError:error]; + + return write; +} + +- (NSArray *)capturedEventsSinceLastCall { + NSArray *result = [self.events copy]; + [self.events removeAllObjects]; + return result; +} + +- (FSTTargetID)addUserListenerWithQuery:(FSTQuery *)query { + // TODO(dimond): Allow customizing listen options in spec tests + // TODO(dimond): Change spec tests to verify isFromCache on snapshots + FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES + includeDocumentMetadataChanges:YES + waitForSyncWhenOnline:NO]; + FSTQueryListener *listener = [[FSTQueryListener alloc] + initWithQuery:query + options:options + viewSnapshotHandler:^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error) { + FSTQueryEvent *event = [[FSTQueryEvent alloc] init]; + event.query = query; + event.viewSnapshot = snapshot; + event.error = error; + [self.events addObject:event]; + }]; + self.queryListeners[query] = listener; + return [self.eventManager addListener:listener]; +} + +- (void)removeUserListenerWithQuery:(FSTQuery *)query { + FSTQueryListener *listener = self.queryListeners[query]; + [self.queryListeners removeObjectForKey:query]; + [self.eventManager removeListener:listener]; +} + +- (void)writeUserMutation:(FSTMutation *)mutation { + FSTOutstandingWrite *write = [[FSTOutstandingWrite alloc] init]; + write.write = mutation; + [[self currentOutstandingWrites] addObject:write]; + FSTLog(@"sending a user write."); + [self.syncEngine writeMutations:@[ mutation ] + completion:^(NSError *_Nullable error) { + FSTLog(@"A callback was called with error: %@", error); + write.done = YES; + write.error = error; + }]; +} + +- (void)receiveWatchChange:(FSTWatchChange *)change + snapshotVersion:(FSTSnapshotVersion *_Nullable)snapshot { + [self.datastore writeWatchChange:change snapshotVersion:snapshot]; +} + +- (void)receiveWatchStreamError:(int)errorCode userInfo:(NSDictionary *)userInfo { + NSError *error = + [NSError errorWithDomain:FIRFirestoreErrorDomain code:errorCode userInfo:userInfo]; + + [self.datastore failWatchStreamWithError:error]; + // Unlike web, stream should re-open synchronously (if we have any listeners) + if (self.queryListeners.count > 0) { + FSTAssert(self.datastore.isWatchStreamOpen, @"Watch stream is open"); + } +} + +- (NSDictionary *)currentLimboDocuments { + return [self.syncEngine currentLimboDocuments]; +} + +- (NSDictionary *)activeTargets { + return [[self.datastore activeTargets] copy]; +} + +#pragma mark - Helper Methods + +- (NSMutableArray *)currentOutstandingWrites { + NSMutableArray *writes = _outstandingWrites[self.currentUser]; + if (!writes) { + writes = [NSMutableArray array]; + _outstandingWrites[self.currentUser] = writes; + } + return writes; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTAssertTests.m b/Firestore/Example/Tests/Util/FSTAssertTests.m deleted file mode 100644 index 0cba03f..0000000 --- a/Firestore/Example/Tests/Util/FSTAssertTests.m +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Util/FSTAssert.h" - -#import - -@interface FSTAssertTests : XCTestCase -@end - -@implementation FSTAssertTests - -- (void)testFail { - @try { - [self failingMethod]; - XCTFail("Should not have succeeded"); - } @catch (NSException *ex) { - XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); - XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: 0:foo:bar"); - } -} - -// A method guaranteed to fail. Note that the return type is intentionally something that is -// not actually returned in the body to ensure that the function attribute declarations in the -// macro properly mark this macro invocation as never returning. -- (int)failingMethod { - FSTFail(@"%d:%s:%@", 0, "foo", @"bar"); -} - -- (void)testCFail { - @try { - failingFunction(); - XCTFail("Should not have succeeded"); - } @catch (NSException *ex) { - XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); - XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: 0:foo:bar"); - } -} - -// A function guaranteed to fail. Note that the return type is intentionally something that is -// not actually returned in the body to ensure that the function attribute declarations in the -// macro properly mark this macro invocation as never returning. -int failingFunction() { - FSTCFail(@"%d:%s:%@", 0, "foo", @"bar"); -} - -- (void)testAssertNonFailing { - @try { - FSTAssert(YES, @"shouldn't fail"); - } @catch (NSException *ex) { - XCTFail("Should not have failed, but got %@", ex); - } -} - -- (void)testAssertFailing { - @try { - FSTAssert(NO, @"should fail"); - XCTFail("Should not have succeeded"); - } @catch (NSException *ex) { - XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); - XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: should fail"); - } -} - -- (void)testCAssertNonFailing { - @try { - nonAssertingFunction(); - } @catch (NSException *ex) { - XCTFail("Should not have failed, but got %@", ex); - } -} - -int nonAssertingFunction() { - FSTCAssert(YES, @"shouldn't fail"); - return 0; -} - -- (void)testCAssertFailing { - @try { - assertingFunction(); - XCTFail("Should not have succeeded"); - } @catch (NSException *ex) { - XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); - XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: should fail"); - } -} - -int assertingFunction() { - FSTCAssert(NO, @"should fail"); -} - -@end diff --git a/Firestore/Example/Tests/Util/FSTAssertTests.mm b/Firestore/Example/Tests/Util/FSTAssertTests.mm new file mode 100644 index 0000000..0cba03f --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTAssertTests.mm @@ -0,0 +1,105 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Util/FSTAssert.h" + +#import + +@interface FSTAssertTests : XCTestCase +@end + +@implementation FSTAssertTests + +- (void)testFail { + @try { + [self failingMethod]; + XCTFail("Should not have succeeded"); + } @catch (NSException *ex) { + XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); + XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: 0:foo:bar"); + } +} + +// A method guaranteed to fail. Note that the return type is intentionally something that is +// not actually returned in the body to ensure that the function attribute declarations in the +// macro properly mark this macro invocation as never returning. +- (int)failingMethod { + FSTFail(@"%d:%s:%@", 0, "foo", @"bar"); +} + +- (void)testCFail { + @try { + failingFunction(); + XCTFail("Should not have succeeded"); + } @catch (NSException *ex) { + XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); + XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: 0:foo:bar"); + } +} + +// A function guaranteed to fail. Note that the return type is intentionally something that is +// not actually returned in the body to ensure that the function attribute declarations in the +// macro properly mark this macro invocation as never returning. +int failingFunction() { + FSTCFail(@"%d:%s:%@", 0, "foo", @"bar"); +} + +- (void)testAssertNonFailing { + @try { + FSTAssert(YES, @"shouldn't fail"); + } @catch (NSException *ex) { + XCTFail("Should not have failed, but got %@", ex); + } +} + +- (void)testAssertFailing { + @try { + FSTAssert(NO, @"should fail"); + XCTFail("Should not have succeeded"); + } @catch (NSException *ex) { + XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); + XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: should fail"); + } +} + +- (void)testCAssertNonFailing { + @try { + nonAssertingFunction(); + } @catch (NSException *ex) { + XCTFail("Should not have failed, but got %@", ex); + } +} + +int nonAssertingFunction() { + FSTCAssert(YES, @"shouldn't fail"); + return 0; +} + +- (void)testCAssertFailing { + @try { + assertingFunction(); + XCTFail("Should not have succeeded"); + } @catch (NSException *ex) { + XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); + XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: should fail"); + } +} + +int assertingFunction() { + FSTCAssert(NO, @"should fail"); +} + +@end diff --git a/Firestore/Example/Tests/Util/FSTEventAccumulator.m b/Firestore/Example/Tests/Util/FSTEventAccumulator.m deleted file mode 100644 index c4c1602..0000000 --- a/Firestore/Example/Tests/Util/FSTEventAccumulator.m +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Example/Tests/Util/FSTEventAccumulator.h" - -#import - -#import "Firestore/Source/Util/FSTAssert.h" - -#import "Firestore/Example/Tests/Util/XCTestCase+Await.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTEventAccumulator () -- (instancetype)initForTest:(XCTestCase *)testCase NS_DESIGNATED_INITIALIZER; - -@property(nonatomic, weak, readonly) XCTestCase *testCase; - -@property(nonatomic, assign) NSUInteger maxEvents; -@property(nonatomic, strong, nullable) XCTestExpectation *expectation; -@end - -@implementation FSTEventAccumulator { - NSMutableArray *_events; -} - -+ (instancetype)accumulatorForTest:(XCTestCase *)testCase { - return [[FSTEventAccumulator alloc] initForTest:testCase]; -} - -- (instancetype)initForTest:(XCTestCase *)testCase { - if (self = [super init]) { - _testCase = testCase; - _events = [NSMutableArray array]; - } - return self; -} - -- (NSArray *)awaitEvents:(NSUInteger)events name:(NSString *)name { - @synchronized(self) { - FSTAssert(!self.expectation, @"Existing expectation still pending?"); - self.expectation = [self.testCase expectationWithDescription:name]; - self.maxEvents = self.maxEvents + events; - [self checkFulfilled]; - } - - // Don't await within @synchronized block to avoid deadlocking. - [self.testCase awaitExpectations]; - - return [_events subarrayWithRange:NSMakeRange(self.maxEvents - events, events)]; -} - -- (id)awaitEventWithName:(NSString *)name { - NSArray *events = [self awaitEvents:1 name:name]; - return events[0]; -} - -- (void (^)(id _Nullable, NSError *_Nullable))valueEventHandler { - return ^void(id _Nullable value, NSError *_Nullable error) { - // We can't store nil in the _events array, but these are still interesting to tests so store - // NSNull instead. - id event = value ? value : [NSNull null]; - - @synchronized(self) { - [_events addObject:event]; - [self checkFulfilled]; - } - }; -} - -- (void)checkFulfilled { - if (_events.count >= self.maxEvents) { - [self.expectation fulfill]; - self.expectation = nil; - } -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTEventAccumulator.mm b/Firestore/Example/Tests/Util/FSTEventAccumulator.mm new file mode 100644 index 0000000..c4c1602 --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTEventAccumulator.mm @@ -0,0 +1,93 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Example/Tests/Util/FSTEventAccumulator.h" + +#import + +#import "Firestore/Source/Util/FSTAssert.h" + +#import "Firestore/Example/Tests/Util/XCTestCase+Await.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTEventAccumulator () +- (instancetype)initForTest:(XCTestCase *)testCase NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, weak, readonly) XCTestCase *testCase; + +@property(nonatomic, assign) NSUInteger maxEvents; +@property(nonatomic, strong, nullable) XCTestExpectation *expectation; +@end + +@implementation FSTEventAccumulator { + NSMutableArray *_events; +} + ++ (instancetype)accumulatorForTest:(XCTestCase *)testCase { + return [[FSTEventAccumulator alloc] initForTest:testCase]; +} + +- (instancetype)initForTest:(XCTestCase *)testCase { + if (self = [super init]) { + _testCase = testCase; + _events = [NSMutableArray array]; + } + return self; +} + +- (NSArray *)awaitEvents:(NSUInteger)events name:(NSString *)name { + @synchronized(self) { + FSTAssert(!self.expectation, @"Existing expectation still pending?"); + self.expectation = [self.testCase expectationWithDescription:name]; + self.maxEvents = self.maxEvents + events; + [self checkFulfilled]; + } + + // Don't await within @synchronized block to avoid deadlocking. + [self.testCase awaitExpectations]; + + return [_events subarrayWithRange:NSMakeRange(self.maxEvents - events, events)]; +} + +- (id)awaitEventWithName:(NSString *)name { + NSArray *events = [self awaitEvents:1 name:name]; + return events[0]; +} + +- (void (^)(id _Nullable, NSError *_Nullable))valueEventHandler { + return ^void(id _Nullable value, NSError *_Nullable error) { + // We can't store nil in the _events array, but these are still interesting to tests so store + // NSNull instead. + id event = value ? value : [NSNull null]; + + @synchronized(self) { + [_events addObject:event]; + [self checkFulfilled]; + } + }; +} + +- (void)checkFulfilled { + if (_events.count >= self.maxEvents) { + [self.expectation fulfill]; + self.expectation = nil; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTHelpers.m b/Firestore/Example/Tests/Util/FSTHelpers.m deleted file mode 100644 index c1506b0..0000000 --- a/Firestore/Example/Tests/Util/FSTHelpers.m +++ /dev/null @@ -1,350 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Example/Tests/Util/FSTHelpers.h" - -#import -#import - -#import "Firestore/Source/API/FIRFieldPath+Internal.h" -#import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Core/FSTTimestamp.h" -#import "Firestore/Source/Core/FSTView.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" -#import "Firestore/Source/Local/FSTLocalViewChanges.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTDatabaseID.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" -#import "Firestore/Source/Remote/FSTWatchChange.h" -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -/** A string sentinel that can be used with FSTTestPatchMutation() to mark a field for deletion. */ -static NSString *const kDeleteSentinel = @""; - -static const int kMicrosPerSec = 1000000; -static const int kMillisPerSec = 1000; - -FSTTimestamp *FSTTestTimestamp(int year, int month, int day, int hour, int minute, int second) { - NSDate *date = FSTTestDate(year, month, day, hour, minute, second); - return [FSTTimestamp timestampWithDate:date]; -} - -NSDate *FSTTestDate(int year, int month, int day, int hour, int minute, int second) { - NSDateComponents *comps = FSTTestDateComponents(year, month, day, hour, minute, second); - return [[NSCalendar currentCalendar] dateFromComponents:comps]; -} - -NSData *FSTTestData(int bytes, ...) { - va_list args; - va_start(args, bytes); /* Initialize the argument list. */ - - NSMutableData *data = [NSMutableData data]; - - int next = bytes; - while (next >= 0) { - uint8_t byte = (uint8_t)next; - [data appendBytes:&byte length:1]; - next = va_arg(args, int); - } - - va_end(args); - return [data copy]; -} - -FIRGeoPoint *FSTTestGeoPoint(double latitude, double longitude) { - return [[FIRGeoPoint alloc] initWithLatitude:latitude longitude:longitude]; -} - -NSDateComponents *FSTTestDateComponents( - int year, int month, int day, int hour, int minute, int second) { - NSDateComponents *comps = [[NSDateComponents alloc] init]; - comps.year = year; - comps.month = month; - comps.day = day; - comps.hour = hour; - comps.minute = minute; - comps.second = second; - - // Force time zone to UTC to avoid these values changing due to daylight saving. - comps.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; - return comps; -} - -FSTFieldPath *FSTTestFieldPath(NSString *field) { - return [FIRFieldPath pathWithDotSeparatedString:field].internalValue; -} - -FSTFieldValue *FSTTestFieldValue(id _Nullable value) { - FSTDatabaseID *databaseID = - [FSTDatabaseID databaseIDWithProject:@"project" database:kDefaultDatabaseID]; - FSTUserDataConverter *converter = - [[FSTUserDataConverter alloc] initWithDatabaseID:databaseID - preConverter:^id _Nullable(id _Nullable input) { - return input; - }]; - // HACK: We use parsedQueryValue: since it accepts scalars as well as arrays / objects, and - // our tests currently use FSTTestFieldValue() pretty generically so we don't know the intent. - return [converter parsedQueryValue:value]; -} - -FSTObjectValue *FSTTestObjectValue(NSDictionary *data) { - FSTFieldValue *wrapped = FSTTestFieldValue(data); - FSTCAssert([wrapped isKindOfClass:[FSTObjectValue class]], @"Unsupported value: %@", data); - return (FSTObjectValue *)wrapped; -} - -FSTDocumentKey *FSTTestDocKey(NSString *path) { - return [FSTDocumentKey keyWithPathString:path]; -} - -FSTDocumentKeySet *FSTTestDocKeySet(NSArray *keys) { - FSTDocumentKeySet *result = [FSTDocumentKeySet keySet]; - for (FSTDocumentKey *key in keys) { - result = [result setByAddingObject:key]; - } - return result; -} - -FSTSnapshotVersion *FSTTestVersion(FSTTestSnapshotVersion versionMicroseconds) { - int64_t seconds = versionMicroseconds / kMicrosPerSec; - int32_t nanos = (int32_t)(versionMicroseconds % kMicrosPerSec) * kMillisPerSec; - - FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:seconds nanos:nanos]; - return [FSTSnapshotVersion versionWithTimestamp:timestamp]; -} - -FSTDocument *FSTTestDoc(NSString *path, - FSTTestSnapshotVersion version, - NSDictionary *data, - BOOL hasMutations) { - FSTDocumentKey *key = FSTTestDocKey(path); - return [FSTDocument documentWithData:FSTTestObjectValue(data) - key:key - version:FSTTestVersion(version) - hasLocalMutations:hasMutations]; -} - -FSTDeletedDocument *FSTTestDeletedDoc(NSString *path, FSTTestSnapshotVersion version) { - FSTDocumentKey *key = FSTTestDocKey(path); - return [FSTDeletedDocument documentWithKey:key version:FSTTestVersion(version)]; -} - -static NSArray *FSTTestSplitPath(NSString *path) { - if ([path isEqualToString:@""]) { - return @[]; - } else { - return [path componentsSeparatedByString:@"/"]; - } -} - -FSTResourcePath *FSTTestPath(NSString *path) { - return [FSTResourcePath pathWithSegments:FSTTestSplitPath(path)]; -} - -FSTDocumentKeyReference *FSTTestRef(NSString *projectID, NSString *database, NSString *path) { - FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:projectID database:database]; - return [[FSTDocumentKeyReference alloc] initWithKey:FSTTestDocKey(path) databaseID:databaseID]; -} - -FSTQuery *FSTTestQuery(NSString *path) { - return [FSTQuery queryWithPath:FSTTestPath(path)]; -} - -id FSTTestFilter(NSString *field, NSString *opString, id value) { - FSTFieldPath *path = FSTTestFieldPath(field); - FSTRelationFilterOperator op; - if ([opString isEqualToString:@"<"]) { - op = FSTRelationFilterOperatorLessThan; - } else if ([opString isEqualToString:@"<="]) { - op = FSTRelationFilterOperatorLessThanOrEqual; - } else if ([opString isEqualToString:@"=="]) { - op = FSTRelationFilterOperatorEqual; - } else if ([opString isEqualToString:@">="]) { - op = FSTRelationFilterOperatorGreaterThanOrEqual; - } else if ([opString isEqualToString:@">"]) { - op = FSTRelationFilterOperatorGreaterThan; - } else { - FSTCFail(@"Unsupported operator type: %@", opString); - } - - FSTFieldValue *data = FSTTestFieldValue(value); - if ([data isEqual:[FSTDoubleValue nanValue]]) { - FSTCAssert(op == FSTRelationFilterOperatorEqual, @"Must use == with NAN."); - return [[FSTNanFilter alloc] initWithField:path]; - } else if ([data isEqual:[FSTNullValue nullValue]]) { - FSTCAssert(op == FSTRelationFilterOperatorEqual, @"Must use == with Null."); - return [[FSTNullFilter alloc] initWithField:path]; - } else { - return [FSTRelationFilter filterWithField:path filterOperator:op value:data]; - } -} - -FSTSortOrder *FSTTestOrderBy(NSString *field, NSString *direction) { - FSTFieldPath *path = FSTTestFieldPath(field); - BOOL ascending; - if ([direction isEqualToString:@"asc"]) { - ascending = YES; - } else if ([direction isEqualToString:@"desc"]) { - ascending = NO; - } else { - FSTCFail(@"Unsupported direction: %@", direction); - } - return [FSTSortOrder sortOrderWithFieldPath:path ascending:ascending]; -} - -NSComparator FSTTestDocComparator(NSString *fieldPath) { - FSTQuery *query = [FSTTestQuery(@"docs") - queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(fieldPath) - ascending:YES]]; - return [query comparator]; -} - -FSTDocumentSet *FSTTestDocSet(NSComparator comp, NSArray *docs) { - FSTDocumentSet *docSet = [FSTDocumentSet documentSetWithComparator:comp]; - for (FSTDocument *doc in docs) { - docSet = [docSet documentSetByAddingDocument:doc]; - } - return docSet; -} - -FSTSetMutation *FSTTestSetMutation(NSString *path, NSDictionary *values) { - return [[FSTSetMutation alloc] initWithKey:FSTTestDocKey(path) - value:FSTTestObjectValue(values) - precondition:[FSTPrecondition none]]; -} - -FSTPatchMutation *FSTTestPatchMutation(NSString *path, - NSDictionary *values, - NSArray *_Nullable updateMask) { - BOOL merge = updateMask != nil; - - __block FSTObjectValue *objectValue = [FSTObjectValue objectValue]; - NSMutableArray *fieldMaskPaths = [NSMutableArray array]; - [values enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { - FSTFieldPath *path = FSTTestFieldPath(key); - [fieldMaskPaths addObject:path]; - if (![value isEqual:kDeleteSentinel]) { - FSTFieldValue *parsedValue = FSTTestFieldValue(value); - objectValue = [objectValue objectBySettingValue:parsedValue forPath:path]; - } - }]; - - FSTDocumentKey *key = [FSTDocumentKey keyWithPath:FSTTestPath(path)]; - FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:merge ? updateMask : fieldMaskPaths]; - return [[FSTPatchMutation alloc] initWithKey:key - fieldMask:mask - value:objectValue - precondition:[FSTPrecondition preconditionWithExists:YES]]; -} - -// For now this only creates TransformMutations with server timestamps. -FSTTransformMutation *FSTTestTransformMutation(NSString *path, - NSArray *serverTimestampFields) { - FSTDocumentKey *key = [FSTDocumentKey keyWithPath:FSTTestPath(path)]; - NSMutableArray *fieldTransforms = [NSMutableArray array]; - for (NSString *field in serverTimestampFields) { - FSTFieldPath *fieldPath = FSTTestFieldPath(field); - id transformOp = [FSTServerTimestampTransform serverTimestampTransform]; - FSTFieldTransform *transform = - [[FSTFieldTransform alloc] initWithPath:fieldPath transform:transformOp]; - [fieldTransforms addObject:transform]; - } - return [[FSTTransformMutation alloc] initWithKey:key fieldTransforms:fieldTransforms]; -} - -FSTDeleteMutation *FSTTestDeleteMutation(NSString *path) { - return [[FSTDeleteMutation alloc] initWithKey:FSTTestDocKey(path) - precondition:[FSTPrecondition none]]; -} - -FSTMaybeDocumentDictionary *FSTTestDocUpdates(NSArray *docs) { - FSTMaybeDocumentDictionary *updates = [FSTMaybeDocumentDictionary maybeDocumentDictionary]; - for (FSTMaybeDocument *doc in docs) { - updates = [updates dictionaryBySettingObject:doc forKey:doc.key]; - } - return updates; -} - -FSTViewSnapshot *_Nullable FSTTestApplyChanges(FSTView *view, - NSArray *docs, - FSTTargetChange *_Nullable targetChange) { - return [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(docs)] - targetChange:targetChange] - .snapshot; -} - -FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc, - NSArray *updatedInTargets, - NSArray *removedFromTargets) { - FSTDocumentWatchChange *change = - [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:updatedInTargets - removedTargetIDs:removedFromTargets - documentKey:doc.key - document:doc]; - NSMutableDictionary *listens = [NSMutableDictionary dictionary]; - FSTQueryData *dummyQueryData = [FSTQueryData alloc]; - for (NSNumber *targetID in updatedInTargets) { - listens[targetID] = dummyQueryData; - } - for (NSNumber *targetID in removedFromTargets) { - listens[targetID] = dummyQueryData; - } - NSMutableDictionary *pending = [NSMutableDictionary dictionary]; - FSTWatchChangeAggregator *aggregator = - [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:doc.version - listenTargets:listens - pendingTargetResponses:pending]; - [aggregator addWatchChange:change]; - return [aggregator remoteEvent]; -} - -/** Creates a resume token to match the given snapshot version. */ -NSData *_Nullable FSTTestResumeTokenFromSnapshotVersion(FSTTestSnapshotVersion snapshotVersion) { - if (snapshotVersion == 0) { - return nil; - } - - NSString *snapshotString = [NSString stringWithFormat:@"snapshot-%" PRId64, snapshotVersion]; - return [snapshotString dataUsingEncoding:NSUTF8StringEncoding]; -} - -FSTLocalViewChanges *FSTTestViewChanges(FSTQuery *query, - NSArray *addedKeys, - NSArray *removedKeys) { - FSTDocumentKeySet *added = [FSTDocumentKeySet keySet]; - for (NSString *keyPath in addedKeys) { - FSTDocumentKey *key = FSTTestDocKey(keyPath); - added = [added setByAddingObject:key]; - } - FSTDocumentKeySet *removed = [FSTDocumentKeySet keySet]; - for (NSString *keyPath in removedKeys) { - FSTDocumentKey *key = FSTTestDocKey(keyPath); - removed = [removed setByAddingObject:key]; - } - return [FSTLocalViewChanges changesForQuery:query addedKeys:added removedKeys:removed]; -} - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTHelpers.mm b/Firestore/Example/Tests/Util/FSTHelpers.mm new file mode 100644 index 0000000..64fe213 --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTHelpers.mm @@ -0,0 +1,352 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Example/Tests/Util/FSTHelpers.h" + +#include + +#import +#import + +#import "Firestore/Source/API/FIRFieldPath+Internal.h" +#import "Firestore/Source/API/FSTUserDataConverter.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Core/FSTTimestamp.h" +#import "Firestore/Source/Core/FSTView.h" +#import "Firestore/Source/Core/FSTViewSnapshot.h" +#import "Firestore/Source/Local/FSTLocalViewChanges.h" +#import "Firestore/Source/Local/FSTQueryData.h" +#import "Firestore/Source/Model/FSTDatabaseID.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Remote/FSTRemoteEvent.h" +#import "Firestore/Source/Remote/FSTWatchChange.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +/** A string sentinel that can be used with FSTTestPatchMutation() to mark a field for deletion. */ +static NSString *const kDeleteSentinel = @""; + +static const int kMicrosPerSec = 1000000; +static const int kMillisPerSec = 1000; + +FSTTimestamp *FSTTestTimestamp(int year, int month, int day, int hour, int minute, int second) { + NSDate *date = FSTTestDate(year, month, day, hour, minute, second); + return [FSTTimestamp timestampWithDate:date]; +} + +NSDate *FSTTestDate(int year, int month, int day, int hour, int minute, int second) { + NSDateComponents *comps = FSTTestDateComponents(year, month, day, hour, minute, second); + return [[NSCalendar currentCalendar] dateFromComponents:comps]; +} + +NSData *FSTTestData(int bytes, ...) { + va_list args; + va_start(args, bytes); /* Initialize the argument list. */ + + NSMutableData *data = [NSMutableData data]; + + int next = bytes; + while (next >= 0) { + uint8_t byte = (uint8_t)next; + [data appendBytes:&byte length:1]; + next = va_arg(args, int); + } + + va_end(args); + return [data copy]; +} + +FIRGeoPoint *FSTTestGeoPoint(double latitude, double longitude) { + return [[FIRGeoPoint alloc] initWithLatitude:latitude longitude:longitude]; +} + +NSDateComponents *FSTTestDateComponents( + int year, int month, int day, int hour, int minute, int second) { + NSDateComponents *comps = [[NSDateComponents alloc] init]; + comps.year = year; + comps.month = month; + comps.day = day; + comps.hour = hour; + comps.minute = minute; + comps.second = second; + + // Force time zone to UTC to avoid these values changing due to daylight saving. + comps.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; + return comps; +} + +FSTFieldPath *FSTTestFieldPath(NSString *field) { + return [FIRFieldPath pathWithDotSeparatedString:field].internalValue; +} + +FSTFieldValue *FSTTestFieldValue(id _Nullable value) { + FSTDatabaseID *databaseID = + [FSTDatabaseID databaseIDWithProject:@"project" database:kDefaultDatabaseID]; + FSTUserDataConverter *converter = + [[FSTUserDataConverter alloc] initWithDatabaseID:databaseID + preConverter:^id _Nullable(id _Nullable input) { + return input; + }]; + // HACK: We use parsedQueryValue: since it accepts scalars as well as arrays / objects, and + // our tests currently use FSTTestFieldValue() pretty generically so we don't know the intent. + return [converter parsedQueryValue:value]; +} + +FSTObjectValue *FSTTestObjectValue(NSDictionary *data) { + FSTFieldValue *wrapped = FSTTestFieldValue(data); + FSTCAssert([wrapped isKindOfClass:[FSTObjectValue class]], @"Unsupported value: %@", data); + return (FSTObjectValue *)wrapped; +} + +FSTDocumentKey *FSTTestDocKey(NSString *path) { + return [FSTDocumentKey keyWithPathString:path]; +} + +FSTDocumentKeySet *FSTTestDocKeySet(NSArray *keys) { + FSTDocumentKeySet *result = [FSTDocumentKeySet keySet]; + for (FSTDocumentKey *key in keys) { + result = [result setByAddingObject:key]; + } + return result; +} + +FSTSnapshotVersion *FSTTestVersion(FSTTestSnapshotVersion versionMicroseconds) { + int64_t seconds = versionMicroseconds / kMicrosPerSec; + int32_t nanos = (int32_t)(versionMicroseconds % kMicrosPerSec) * kMillisPerSec; + + FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:seconds nanos:nanos]; + return [FSTSnapshotVersion versionWithTimestamp:timestamp]; +} + +FSTDocument *FSTTestDoc(NSString *path, + FSTTestSnapshotVersion version, + NSDictionary *data, + BOOL hasMutations) { + FSTDocumentKey *key = FSTTestDocKey(path); + return [FSTDocument documentWithData:FSTTestObjectValue(data) + key:key + version:FSTTestVersion(version) + hasLocalMutations:hasMutations]; +} + +FSTDeletedDocument *FSTTestDeletedDoc(NSString *path, FSTTestSnapshotVersion version) { + FSTDocumentKey *key = FSTTestDocKey(path); + return [FSTDeletedDocument documentWithKey:key version:FSTTestVersion(version)]; +} + +static NSArray *FSTTestSplitPath(NSString *path) { + if ([path isEqualToString:@""]) { + return @[]; + } else { + return [path componentsSeparatedByString:@"/"]; + } +} + +FSTResourcePath *FSTTestPath(NSString *path) { + return [FSTResourcePath pathWithSegments:FSTTestSplitPath(path)]; +} + +FSTDocumentKeyReference *FSTTestRef(NSString *projectID, NSString *database, NSString *path) { + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:projectID database:database]; + return [[FSTDocumentKeyReference alloc] initWithKey:FSTTestDocKey(path) databaseID:databaseID]; +} + +FSTQuery *FSTTestQuery(NSString *path) { + return [FSTQuery queryWithPath:FSTTestPath(path)]; +} + +id FSTTestFilter(NSString *field, NSString *opString, id value) { + FSTFieldPath *path = FSTTestFieldPath(field); + FSTRelationFilterOperator op; + if ([opString isEqualToString:@"<"]) { + op = FSTRelationFilterOperatorLessThan; + } else if ([opString isEqualToString:@"<="]) { + op = FSTRelationFilterOperatorLessThanOrEqual; + } else if ([opString isEqualToString:@"=="]) { + op = FSTRelationFilterOperatorEqual; + } else if ([opString isEqualToString:@">="]) { + op = FSTRelationFilterOperatorGreaterThanOrEqual; + } else if ([opString isEqualToString:@">"]) { + op = FSTRelationFilterOperatorGreaterThan; + } else { + FSTCFail(@"Unsupported operator type: %@", opString); + } + + FSTFieldValue *data = FSTTestFieldValue(value); + if ([data isEqual:[FSTDoubleValue nanValue]]) { + FSTCAssert(op == FSTRelationFilterOperatorEqual, @"Must use == with NAN."); + return [[FSTNanFilter alloc] initWithField:path]; + } else if ([data isEqual:[FSTNullValue nullValue]]) { + FSTCAssert(op == FSTRelationFilterOperatorEqual, @"Must use == with Null."); + return [[FSTNullFilter alloc] initWithField:path]; + } else { + return [FSTRelationFilter filterWithField:path filterOperator:op value:data]; + } +} + +FSTSortOrder *FSTTestOrderBy(NSString *field, NSString *direction) { + FSTFieldPath *path = FSTTestFieldPath(field); + BOOL ascending; + if ([direction isEqualToString:@"asc"]) { + ascending = YES; + } else if ([direction isEqualToString:@"desc"]) { + ascending = NO; + } else { + FSTCFail(@"Unsupported direction: %@", direction); + } + return [FSTSortOrder sortOrderWithFieldPath:path ascending:ascending]; +} + +NSComparator FSTTestDocComparator(NSString *fieldPath) { + FSTQuery *query = [FSTTestQuery(@"docs") + queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(fieldPath) + ascending:YES]]; + return [query comparator]; +} + +FSTDocumentSet *FSTTestDocSet(NSComparator comp, NSArray *docs) { + FSTDocumentSet *docSet = [FSTDocumentSet documentSetWithComparator:comp]; + for (FSTDocument *doc in docs) { + docSet = [docSet documentSetByAddingDocument:doc]; + } + return docSet; +} + +FSTSetMutation *FSTTestSetMutation(NSString *path, NSDictionary *values) { + return [[FSTSetMutation alloc] initWithKey:FSTTestDocKey(path) + value:FSTTestObjectValue(values) + precondition:[FSTPrecondition none]]; +} + +FSTPatchMutation *FSTTestPatchMutation(NSString *path, + NSDictionary *values, + NSArray *_Nullable updateMask) { + BOOL merge = updateMask != nil; + + __block FSTObjectValue *objectValue = [FSTObjectValue objectValue]; + NSMutableArray *fieldMaskPaths = [NSMutableArray array]; + [values enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + FSTFieldPath *path = FSTTestFieldPath(key); + [fieldMaskPaths addObject:path]; + if (![value isEqual:kDeleteSentinel]) { + FSTFieldValue *parsedValue = FSTTestFieldValue(value); + objectValue = [objectValue objectBySettingValue:parsedValue forPath:path]; + } + }]; + + FSTDocumentKey *key = [FSTDocumentKey keyWithPath:FSTTestPath(path)]; + FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:merge ? updateMask : fieldMaskPaths]; + return [[FSTPatchMutation alloc] initWithKey:key + fieldMask:mask + value:objectValue + precondition:[FSTPrecondition preconditionWithExists:YES]]; +} + +// For now this only creates TransformMutations with server timestamps. +FSTTransformMutation *FSTTestTransformMutation(NSString *path, + NSArray *serverTimestampFields) { + FSTDocumentKey *key = [FSTDocumentKey keyWithPath:FSTTestPath(path)]; + NSMutableArray *fieldTransforms = [NSMutableArray array]; + for (NSString *field in serverTimestampFields) { + FSTFieldPath *fieldPath = FSTTestFieldPath(field); + id transformOp = [FSTServerTimestampTransform serverTimestampTransform]; + FSTFieldTransform *transform = + [[FSTFieldTransform alloc] initWithPath:fieldPath transform:transformOp]; + [fieldTransforms addObject:transform]; + } + return [[FSTTransformMutation alloc] initWithKey:key fieldTransforms:fieldTransforms]; +} + +FSTDeleteMutation *FSTTestDeleteMutation(NSString *path) { + return [[FSTDeleteMutation alloc] initWithKey:FSTTestDocKey(path) + precondition:[FSTPrecondition none]]; +} + +FSTMaybeDocumentDictionary *FSTTestDocUpdates(NSArray *docs) { + FSTMaybeDocumentDictionary *updates = [FSTMaybeDocumentDictionary maybeDocumentDictionary]; + for (FSTMaybeDocument *doc in docs) { + updates = [updates dictionaryBySettingObject:doc forKey:doc.key]; + } + return updates; +} + +FSTViewSnapshot *_Nullable FSTTestApplyChanges(FSTView *view, + NSArray *docs, + FSTTargetChange *_Nullable targetChange) { + return [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(docs)] + targetChange:targetChange] + .snapshot; +} + +FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc, + NSArray *updatedInTargets, + NSArray *removedFromTargets) { + FSTDocumentWatchChange *change = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:updatedInTargets + removedTargetIDs:removedFromTargets + documentKey:doc.key + document:doc]; + NSMutableDictionary *listens = [NSMutableDictionary dictionary]; + FSTQueryData *dummyQueryData = [FSTQueryData alloc]; + for (NSNumber *targetID in updatedInTargets) { + listens[targetID] = dummyQueryData; + } + for (NSNumber *targetID in removedFromTargets) { + listens[targetID] = dummyQueryData; + } + NSMutableDictionary *pending = [NSMutableDictionary dictionary]; + FSTWatchChangeAggregator *aggregator = + [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:doc.version + listenTargets:listens + pendingTargetResponses:pending]; + [aggregator addWatchChange:change]; + return [aggregator remoteEvent]; +} + +/** Creates a resume token to match the given snapshot version. */ +NSData *_Nullable FSTTestResumeTokenFromSnapshotVersion(FSTTestSnapshotVersion snapshotVersion) { + if (snapshotVersion == 0) { + return nil; + } + + NSString *snapshotString = [NSString stringWithFormat:@"snapshot-%" PRId64, snapshotVersion]; + return [snapshotString dataUsingEncoding:NSUTF8StringEncoding]; +} + +FSTLocalViewChanges *FSTTestViewChanges(FSTQuery *query, + NSArray *addedKeys, + NSArray *removedKeys) { + FSTDocumentKeySet *added = [FSTDocumentKeySet keySet]; + for (NSString *keyPath in addedKeys) { + FSTDocumentKey *key = FSTTestDocKey(keyPath); + added = [added setByAddingObject:key]; + } + FSTDocumentKeySet *removed = [FSTDocumentKeySet keySet]; + for (NSString *keyPath in removedKeys) { + FSTDocumentKey *key = FSTTestDocKey(keyPath); + removed = [removed setByAddingObject:key]; + } + return [FSTLocalViewChanges changesForQuery:query addedKeys:added removedKeys:removed]; +} + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTTestDispatchQueue.m b/Firestore/Example/Tests/Util/FSTTestDispatchQueue.m deleted file mode 100644 index 8124cf2..0000000 --- a/Firestore/Example/Tests/Util/FSTTestDispatchQueue.m +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Example/Tests/Util/FSTTestDispatchQueue.h" - -#import - -#import "Firestore/Source/Util/FSTAssert.h" - -@interface FSTTestDispatchQueue () - -@property(nonatomic, weak) XCTestExpectation* expectation; - -@end - -@implementation FSTTestDispatchQueue - -/** The delay used by the idle timeout */ -static const NSTimeInterval kIdleDispatchDelay = 60.0; - -/** The maximum delay we use in a test run. */ -static const NSTimeInterval kTestDispatchDelay = 1.0; - -+ (instancetype)queueWith:(dispatch_queue_t)dispatchQueue { - return [[FSTTestDispatchQueue alloc] initWithQueue:dispatchQueue]; -} - -- (instancetype)initWithQueue:(dispatch_queue_t)dispatchQueue { - return (self = [super initWithQueue:dispatchQueue]); -} - -- (void)dispatchAfterDelay:(NSTimeInterval)delay block:(void (^)(void))block { - [super dispatchAfterDelay:MIN(delay, kTestDispatchDelay) - block:^() { - block(); - if (delay == kIdleDispatchDelay) { - [_expectation fulfill]; - _expectation = nil; - } - }]; -} - -- (void)fulfillOnExecution:(XCTestExpectation*)expectation { - FSTAssert(_expectation == nil, @"Previous expectation still active"); - _expectation = expectation; -} - -@end diff --git a/Firestore/Example/Tests/Util/FSTTestDispatchQueue.mm b/Firestore/Example/Tests/Util/FSTTestDispatchQueue.mm new file mode 100644 index 0000000..8124cf2 --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTTestDispatchQueue.mm @@ -0,0 +1,61 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Example/Tests/Util/FSTTestDispatchQueue.h" + +#import + +#import "Firestore/Source/Util/FSTAssert.h" + +@interface FSTTestDispatchQueue () + +@property(nonatomic, weak) XCTestExpectation* expectation; + +@end + +@implementation FSTTestDispatchQueue + +/** The delay used by the idle timeout */ +static const NSTimeInterval kIdleDispatchDelay = 60.0; + +/** The maximum delay we use in a test run. */ +static const NSTimeInterval kTestDispatchDelay = 1.0; + ++ (instancetype)queueWith:(dispatch_queue_t)dispatchQueue { + return [[FSTTestDispatchQueue alloc] initWithQueue:dispatchQueue]; +} + +- (instancetype)initWithQueue:(dispatch_queue_t)dispatchQueue { + return (self = [super initWithQueue:dispatchQueue]); +} + +- (void)dispatchAfterDelay:(NSTimeInterval)delay block:(void (^)(void))block { + [super dispatchAfterDelay:MIN(delay, kTestDispatchDelay) + block:^() { + block(); + if (delay == kIdleDispatchDelay) { + [_expectation fulfill]; + _expectation = nil; + } + }]; +} + +- (void)fulfillOnExecution:(XCTestExpectation*)expectation { + FSTAssert(_expectation == nil, @"Previous expectation still active"); + _expectation = expectation; +} + +@end diff --git a/Firestore/Example/Tests/Util/XCTestCase+Await.m b/Firestore/Example/Tests/Util/XCTestCase+Await.m deleted file mode 100644 index 7f4356c..0000000 --- a/Firestore/Example/Tests/Util/XCTestCase+Await.m +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Example/Tests/Util/XCTestCase+Await.h" - -#import - -static const double kExpectationWaitSeconds = 10.0; - -@implementation XCTestCase (Await) - -- (void)awaitExpectations { - [self waitForExpectationsWithTimeout:kExpectationWaitSeconds - handler:^(NSError *_Nullable expectationError) { - if (expectationError) { - XCTFail(@"Error waiting for timeout: %@", expectationError); - } - }]; -} - -- (double)defaultExpectationWaitSeconds { - return kExpectationWaitSeconds; -} - -- (FSTVoidErrorBlock)completionForExpectationWithName:(NSString *)expectationName { - XCTestExpectation *expectation = [self expectationWithDescription:expectationName]; - return ^(NSError *error) { - XCTAssertNil(error); - [expectation fulfill]; - }; -} - -@end diff --git a/Firestore/Example/Tests/Util/XCTestCase+Await.mm b/Firestore/Example/Tests/Util/XCTestCase+Await.mm new file mode 100644 index 0000000..7f4356c --- /dev/null +++ b/Firestore/Example/Tests/Util/XCTestCase+Await.mm @@ -0,0 +1,46 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Example/Tests/Util/XCTestCase+Await.h" + +#import + +static const double kExpectationWaitSeconds = 10.0; + +@implementation XCTestCase (Await) + +- (void)awaitExpectations { + [self waitForExpectationsWithTimeout:kExpectationWaitSeconds + handler:^(NSError *_Nullable expectationError) { + if (expectationError) { + XCTFail(@"Error waiting for timeout: %@", expectationError); + } + }]; +} + +- (double)defaultExpectationWaitSeconds { + return kExpectationWaitSeconds; +} + +- (FSTVoidErrorBlock)completionForExpectationWithName:(NSString *)expectationName { + XCTestExpectation *expectation = [self expectationWithDescription:expectationName]; + return ^(NSError *error) { + XCTAssertNil(error); + [expectation fulfill]; + }; +} + +@end diff --git a/Firestore/Source/API/FIRDocumentChange.m b/Firestore/Source/API/FIRDocumentChange.m deleted file mode 100644 index d1d9999..0000000 --- a/Firestore/Source/API/FIRDocumentChange.m +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "FIRDocumentChange.h" - -#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRDocumentChange () - -- (instancetype)initWithType:(FIRDocumentChangeType)type - document:(FIRDocumentSnapshot *)document - oldIndex:(NSUInteger)oldIndex - newIndex:(NSUInteger)newIndex NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FIRDocumentChange (Internal) - -+ (FIRDocumentChangeType)documentChangeTypeForChange:(FSTDocumentViewChange *)change { - if (change.type == FSTDocumentViewChangeTypeAdded) { - return FIRDocumentChangeTypeAdded; - } else if (change.type == FSTDocumentViewChangeTypeModified || - change.type == FSTDocumentViewChangeTypeMetadata) { - return FIRDocumentChangeTypeModified; - } else if (change.type == FSTDocumentViewChangeTypeRemoved) { - return FIRDocumentChangeTypeRemoved; - } else { - FSTFail(@"Unknown FSTDocumentViewChange: %ld", (long)change.type); - } -} - -+ (NSArray *)documentChangesForSnapshot:(FSTViewSnapshot *)snapshot - firestore:(FIRFirestore *)firestore { - if (snapshot.oldDocuments.isEmpty) { - // Special case the first snapshot because index calculation is easy and fast - FSTDocument *_Nullable lastDocument = nil; - NSUInteger index = 0; - NSMutableArray *changes = [NSMutableArray array]; - for (FSTDocumentViewChange *change in snapshot.documentChanges) { - FIRQueryDocumentSnapshot *document = - [FIRQueryDocumentSnapshot snapshotWithFirestore:firestore - documentKey:change.document.key - document:change.document - fromCache:snapshot.isFromCache]; - FSTAssert(change.type == FSTDocumentViewChangeTypeAdded, - @"Invalid event type for first snapshot"); - FSTAssert(!lastDocument || - snapshot.query.comparator(lastDocument, change.document) == NSOrderedAscending, - @"Got added events in wrong order"); - [changes addObject:[[FIRDocumentChange alloc] initWithType:FIRDocumentChangeTypeAdded - document:document - oldIndex:NSNotFound - newIndex:index++]]; - } - return changes; - } else { - // A DocumentSet that is updated incrementally as changes are applied to use to lookup the index - // of a document. - FSTDocumentSet *indexTracker = snapshot.oldDocuments; - NSMutableArray *changes = [NSMutableArray array]; - for (FSTDocumentViewChange *change in snapshot.documentChanges) { - FIRQueryDocumentSnapshot *document = - [FIRQueryDocumentSnapshot snapshotWithFirestore:firestore - documentKey:change.document.key - document:change.document - fromCache:snapshot.isFromCache]; - - NSUInteger oldIndex = NSNotFound; - NSUInteger newIndex = NSNotFound; - if (change.type != FSTDocumentViewChangeTypeAdded) { - oldIndex = [indexTracker indexOfKey:change.document.key]; - FSTAssert(oldIndex != NSNotFound, @"Index for document not found"); - indexTracker = [indexTracker documentSetByRemovingKey:change.document.key]; - } - if (change.type != FSTDocumentViewChangeTypeRemoved) { - indexTracker = [indexTracker documentSetByAddingDocument:change.document]; - newIndex = [indexTracker indexOfKey:change.document.key]; - } - [FIRDocumentChange documentChangeTypeForChange:change]; - FIRDocumentChangeType type = [FIRDocumentChange documentChangeTypeForChange:change]; - [changes addObject:[[FIRDocumentChange alloc] initWithType:type - document:document - oldIndex:oldIndex - newIndex:newIndex]]; - } - return changes; - } -} - -@end - -@implementation FIRDocumentChange - -- (instancetype)initWithType:(FIRDocumentChangeType)type - document:(FIRQueryDocumentSnapshot *)document - oldIndex:(NSUInteger)oldIndex - newIndex:(NSUInteger)newIndex { - if (self = [super init]) { - _type = type; - _document = document; - _oldIndex = oldIndex; - _newIndex = newIndex; - } - return self; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentChange.mm b/Firestore/Source/API/FIRDocumentChange.mm new file mode 100644 index 0000000..d1d9999 --- /dev/null +++ b/Firestore/Source/API/FIRDocumentChange.mm @@ -0,0 +1,129 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRDocumentChange.h" + +#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTViewSnapshot.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRDocumentChange () + +- (instancetype)initWithType:(FIRDocumentChangeType)type + document:(FIRDocumentSnapshot *)document + oldIndex:(NSUInteger)oldIndex + newIndex:(NSUInteger)newIndex NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FIRDocumentChange (Internal) + ++ (FIRDocumentChangeType)documentChangeTypeForChange:(FSTDocumentViewChange *)change { + if (change.type == FSTDocumentViewChangeTypeAdded) { + return FIRDocumentChangeTypeAdded; + } else if (change.type == FSTDocumentViewChangeTypeModified || + change.type == FSTDocumentViewChangeTypeMetadata) { + return FIRDocumentChangeTypeModified; + } else if (change.type == FSTDocumentViewChangeTypeRemoved) { + return FIRDocumentChangeTypeRemoved; + } else { + FSTFail(@"Unknown FSTDocumentViewChange: %ld", (long)change.type); + } +} + ++ (NSArray *)documentChangesForSnapshot:(FSTViewSnapshot *)snapshot + firestore:(FIRFirestore *)firestore { + if (snapshot.oldDocuments.isEmpty) { + // Special case the first snapshot because index calculation is easy and fast + FSTDocument *_Nullable lastDocument = nil; + NSUInteger index = 0; + NSMutableArray *changes = [NSMutableArray array]; + for (FSTDocumentViewChange *change in snapshot.documentChanges) { + FIRQueryDocumentSnapshot *document = + [FIRQueryDocumentSnapshot snapshotWithFirestore:firestore + documentKey:change.document.key + document:change.document + fromCache:snapshot.isFromCache]; + FSTAssert(change.type == FSTDocumentViewChangeTypeAdded, + @"Invalid event type for first snapshot"); + FSTAssert(!lastDocument || + snapshot.query.comparator(lastDocument, change.document) == NSOrderedAscending, + @"Got added events in wrong order"); + [changes addObject:[[FIRDocumentChange alloc] initWithType:FIRDocumentChangeTypeAdded + document:document + oldIndex:NSNotFound + newIndex:index++]]; + } + return changes; + } else { + // A DocumentSet that is updated incrementally as changes are applied to use to lookup the index + // of a document. + FSTDocumentSet *indexTracker = snapshot.oldDocuments; + NSMutableArray *changes = [NSMutableArray array]; + for (FSTDocumentViewChange *change in snapshot.documentChanges) { + FIRQueryDocumentSnapshot *document = + [FIRQueryDocumentSnapshot snapshotWithFirestore:firestore + documentKey:change.document.key + document:change.document + fromCache:snapshot.isFromCache]; + + NSUInteger oldIndex = NSNotFound; + NSUInteger newIndex = NSNotFound; + if (change.type != FSTDocumentViewChangeTypeAdded) { + oldIndex = [indexTracker indexOfKey:change.document.key]; + FSTAssert(oldIndex != NSNotFound, @"Index for document not found"); + indexTracker = [indexTracker documentSetByRemovingKey:change.document.key]; + } + if (change.type != FSTDocumentViewChangeTypeRemoved) { + indexTracker = [indexTracker documentSetByAddingDocument:change.document]; + newIndex = [indexTracker indexOfKey:change.document.key]; + } + [FIRDocumentChange documentChangeTypeForChange:change]; + FIRDocumentChangeType type = [FIRDocumentChange documentChangeTypeForChange:change]; + [changes addObject:[[FIRDocumentChange alloc] initWithType:type + document:document + oldIndex:oldIndex + newIndex:newIndex]]; + } + return changes; + } +} + +@end + +@implementation FIRDocumentChange + +- (instancetype)initWithType:(FIRDocumentChangeType)type + document:(FIRQueryDocumentSnapshot *)document + oldIndex:(NSUInteger)oldIndex + newIndex:(NSUInteger)newIndex { + if (self = [super init]) { + _type = type; + _document = document; + _oldIndex = oldIndex; + _newIndex = newIndex; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentReference.m b/Firestore/Source/API/FIRDocumentReference.m deleted file mode 100644 index 05253f7..0000000 --- a/Firestore/Source/API/FIRDocumentReference.m +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "FIRDocumentReference.h" - -#import - -#import "FIRFirestoreErrors.h" -#import "FIRSnapshotMetadata.h" -#import "Firestore/Source/API/FIRCollectionReference+Internal.h" -#import "Firestore/Source/API/FIRDocumentReference+Internal.h" -#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRListenerRegistration+Internal.h" -#import "Firestore/Source/API/FIRSetOptions+Internal.h" -#import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Core/FSTEventManager.h" -#import "Firestore/Source/Core/FSTFirestoreClient.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTAsyncQueryListener.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FIRDocumentListenOptions - -@interface FIRDocumentListenOptions () - -- (instancetype)initWithIncludeMetadataChanges:(BOOL)includeMetadataChanges - NS_DESIGNATED_INITIALIZER; - -@property(nonatomic, assign, readonly) BOOL includeMetadataChanges; - -@end - -@implementation FIRDocumentListenOptions - -+ (instancetype)options { - return [[FIRDocumentListenOptions alloc] init]; -} - -- (instancetype)initWithIncludeMetadataChanges:(BOOL)includeMetadataChanges { - if (self = [super init]) { - _includeMetadataChanges = includeMetadataChanges; - } - return self; -} - -- (instancetype)init { - return [self initWithIncludeMetadataChanges:NO]; -} - -- (instancetype)includeMetadataChanges:(BOOL)includeMetadataChanges { - return [[FIRDocumentListenOptions alloc] initWithIncludeMetadataChanges:includeMetadataChanges]; -} - -@end - -#pragma mark - FIRDocumentReference - -@interface FIRDocumentReference () -- (instancetype)initWithKey:(FSTDocumentKey *)key - firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; -@property(nonatomic, strong, readonly) FSTDocumentKey *key; -@end - -@implementation FIRDocumentReference (Internal) - -+ (instancetype)referenceWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore { - if (path.length % 2 != 0) { - FSTThrowInvalidArgument( - @"Invalid document reference. Document references must have an even " - "number of segments, but %@ has %d", - path.canonicalString, path.length); - } - return - [FIRDocumentReference referenceWithKey:[FSTDocumentKey keyWithPath:path] firestore:firestore]; -} - -+ (instancetype)referenceWithKey:(FSTDocumentKey *)key firestore:(FIRFirestore *)firestore { - return [[FIRDocumentReference alloc] initWithKey:key firestore:firestore]; -} - -@end - -@implementation FIRDocumentReference - -- (instancetype)initWithKey:(FSTDocumentKey *)key firestore:(FIRFirestore *)firestore { - if (self = [super init]) { - _key = key; - _firestore = firestore; - } - return self; -} - -#pragma mark - NSObject Methods - -- (BOOL)isEqual:(nullable id)other { - if (other == self) return YES; - if (![[other class] isEqual:[self class]]) return NO; - - return [self isEqualToReference:other]; -} - -- (BOOL)isEqualToReference:(nullable FIRDocumentReference *)reference { - if (self == reference) return YES; - if (reference == nil) return NO; - return [self.firestore isEqual:reference.firestore] && [self.key isEqualToKey:reference.key]; -} - -- (NSUInteger)hash { - NSUInteger hash = [self.firestore hash]; - hash = hash * 31u + [self.key hash]; - return hash; -} - -#pragma mark - Public Methods - -- (NSString *)documentID { - return [self.key.path lastSegment]; -} - -- (FIRCollectionReference *)parent { - FSTResourcePath *parentPath = [self.key.path pathByRemovingLastSegment]; - return [FIRCollectionReference referenceWithPath:parentPath firestore:self.firestore]; -} - -- (NSString *)path { - return [self.key.path canonicalString]; -} - -- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath { - if (!collectionPath) { - FSTThrowInvalidArgument(@"Collection path cannot be nil."); - } - FSTResourcePath *subPath = [FSTResourcePath pathWithString:collectionPath]; - FSTResourcePath *path = [self.key.path pathByAppendingPath:subPath]; - return [FIRCollectionReference referenceWithPath:path firestore:self.firestore]; -} - -- (void)setData:(NSDictionary *)documentData { - return [self setData:documentData options:[FIRSetOptions overwrite] completion:nil]; -} - -- (void)setData:(NSDictionary *)documentData options:(FIRSetOptions *)options { - return [self setData:documentData options:options completion:nil]; -} - -- (void)setData:(NSDictionary *)documentData - completion:(nullable void (^)(NSError *_Nullable error))completion { - return [self setData:documentData options:[FIRSetOptions overwrite] completion:completion]; -} - -- (void)setData:(NSDictionary *)documentData - options:(FIRSetOptions *)options - completion:(nullable void (^)(NSError *_Nullable error))completion { - FSTParsedSetData *parsed = options.isMerge - ? [self.firestore.dataConverter parsedMergeData:documentData] - : [self.firestore.dataConverter parsedSetData:documentData]; - return [self.firestore.client - writeMutations:[parsed mutationsWithKey:self.key precondition:[FSTPrecondition none]] - completion:completion]; -} - -- (void)updateData:(NSDictionary *)fields { - return [self updateData:fields completion:nil]; -} - -- (void)updateData:(NSDictionary *)fields - completion:(nullable void (^)(NSError *_Nullable error))completion { - FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; - return [self.firestore.client - writeMutations:[parsed mutationsWithKey:self.key - precondition:[FSTPrecondition preconditionWithExists:YES]] - completion:completion]; -} - -- (void)deleteDocument { - return [self deleteDocumentWithCompletion:nil]; -} - -- (void)deleteDocumentWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { - FSTDeleteMutation *mutation = - [[FSTDeleteMutation alloc] initWithKey:self.key precondition:[FSTPrecondition none]]; - return [self.firestore.client writeMutations:@[ mutation ] completion:completion]; -} - -- (void)getDocumentWithCompletion:(void (^)(FIRDocumentSnapshot *_Nullable document, - NSError *_Nullable error))completion { - FSTListenOptions *listenOptions = - [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES - includeDocumentMetadataChanges:YES - waitForSyncWhenOnline:YES]; - - dispatch_semaphore_t registered = dispatch_semaphore_create(0); - __block id listenerRegistration; - FIRDocumentSnapshotBlock listener = ^(FIRDocumentSnapshot *snapshot, NSError *error) { - if (error) { - completion(nil, error); - return; - } - - // Remove query first before passing event to user to avoid user actions affecting the - // now stale query. - dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER); - [listenerRegistration remove]; - - if (!snapshot.exists && snapshot.metadata.fromCache) { - // TODO(dimond): Reconsider how to raise missing documents when offline. - // If we're online and the document doesn't exist then we call the completion with - // a document with document.exists set to false. If we're offline however, we call the - // completion handler with an error. Two options: - // 1) Cache the negative response from the server so we can deliver that even when you're - // offline. - // 2) Actually call the completion handler with an error if the document doesn't exist when - // you are offline. - // TODO(dimond): Use proper error domain - completion(nil, - [NSError errorWithDomain:FIRFirestoreErrorDomain - code:FIRFirestoreErrorCodeUnavailable - userInfo:@{ - NSLocalizedDescriptionKey : - @"Failed to get document because the client is offline.", - }]); - } else { - completion(snapshot, nil); - } - }; - - listenerRegistration = - [self addSnapshotListenerInternalWithOptions:listenOptions listener:listener]; - dispatch_semaphore_signal(registered); -} - -- (id)addSnapshotListener:(FIRDocumentSnapshotBlock)listener { - return [self addSnapshotListenerWithOptions:nil listener:listener]; -} - -- (id)addSnapshotListenerWithOptions: - (nullable FIRDocumentListenOptions *)options - listener:(FIRDocumentSnapshotBlock)listener { - return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options] - listener:listener]; -} - -- (id) -addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions - listener:(FIRDocumentSnapshotBlock)listener { - FIRFirestore *firestore = self.firestore; - FSTQuery *query = [FSTQuery queryWithPath:self.key.path]; - FSTDocumentKey *key = self.key; - - FSTViewSnapshotHandler snapshotHandler = ^(FSTViewSnapshot *snapshot, NSError *error) { - if (error) { - listener(nil, error); - return; - } - - FSTAssert(snapshot.documents.count <= 1, @"Too many document returned on a document query"); - FSTDocument *document = [snapshot.documents documentForKey:key]; - - FIRDocumentSnapshot *result = [FIRDocumentSnapshot snapshotWithFirestore:firestore - documentKey:key - document:document - fromCache:snapshot.fromCache]; - listener(result, nil); - }; - - FSTAsyncQueryListener *asyncListener = - [[FSTAsyncQueryListener alloc] initWithDispatchQueue:self.firestore.client.userDispatchQueue - snapshotHandler:snapshotHandler]; - - FSTQueryListener *internalListener = - [firestore.client listenToQuery:query - options:internalOptions - viewSnapshotHandler:[asyncListener asyncSnapshotHandler]]; - return [[FSTListenerRegistration alloc] initWithClient:self.firestore.client - asyncListener:asyncListener - internalListener:internalListener]; -} - -/** Converts the public API options object to the internal options object. */ -- (FSTListenOptions *)internalOptions:(nullable FIRDocumentListenOptions *)options { - return - [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:options.includeMetadataChanges - includeDocumentMetadataChanges:options.includeMetadataChanges - waitForSyncWhenOnline:NO]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentReference.mm b/Firestore/Source/API/FIRDocumentReference.mm new file mode 100644 index 0000000..05253f7 --- /dev/null +++ b/Firestore/Source/API/FIRDocumentReference.mm @@ -0,0 +1,311 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRDocumentReference.h" + +#import + +#import "FIRFirestoreErrors.h" +#import "FIRSnapshotMetadata.h" +#import "Firestore/Source/API/FIRCollectionReference+Internal.h" +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRListenerRegistration+Internal.h" +#import "Firestore/Source/API/FIRSetOptions+Internal.h" +#import "Firestore/Source/API/FSTUserDataConverter.h" +#import "Firestore/Source/Core/FSTEventManager.h" +#import "Firestore/Source/Core/FSTFirestoreClient.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTAsyncQueryListener.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FIRDocumentListenOptions + +@interface FIRDocumentListenOptions () + +- (instancetype)initWithIncludeMetadataChanges:(BOOL)includeMetadataChanges + NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, assign, readonly) BOOL includeMetadataChanges; + +@end + +@implementation FIRDocumentListenOptions + ++ (instancetype)options { + return [[FIRDocumentListenOptions alloc] init]; +} + +- (instancetype)initWithIncludeMetadataChanges:(BOOL)includeMetadataChanges { + if (self = [super init]) { + _includeMetadataChanges = includeMetadataChanges; + } + return self; +} + +- (instancetype)init { + return [self initWithIncludeMetadataChanges:NO]; +} + +- (instancetype)includeMetadataChanges:(BOOL)includeMetadataChanges { + return [[FIRDocumentListenOptions alloc] initWithIncludeMetadataChanges:includeMetadataChanges]; +} + +@end + +#pragma mark - FIRDocumentReference + +@interface FIRDocumentReference () +- (instancetype)initWithKey:(FSTDocumentKey *)key + firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; +@property(nonatomic, strong, readonly) FSTDocumentKey *key; +@end + +@implementation FIRDocumentReference (Internal) + ++ (instancetype)referenceWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore { + if (path.length % 2 != 0) { + FSTThrowInvalidArgument( + @"Invalid document reference. Document references must have an even " + "number of segments, but %@ has %d", + path.canonicalString, path.length); + } + return + [FIRDocumentReference referenceWithKey:[FSTDocumentKey keyWithPath:path] firestore:firestore]; +} + ++ (instancetype)referenceWithKey:(FSTDocumentKey *)key firestore:(FIRFirestore *)firestore { + return [[FIRDocumentReference alloc] initWithKey:key firestore:firestore]; +} + +@end + +@implementation FIRDocumentReference + +- (instancetype)initWithKey:(FSTDocumentKey *)key firestore:(FIRFirestore *)firestore { + if (self = [super init]) { + _key = key; + _firestore = firestore; + } + return self; +} + +#pragma mark - NSObject Methods + +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + return [self isEqualToReference:other]; +} + +- (BOOL)isEqualToReference:(nullable FIRDocumentReference *)reference { + if (self == reference) return YES; + if (reference == nil) return NO; + return [self.firestore isEqual:reference.firestore] && [self.key isEqualToKey:reference.key]; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.firestore hash]; + hash = hash * 31u + [self.key hash]; + return hash; +} + +#pragma mark - Public Methods + +- (NSString *)documentID { + return [self.key.path lastSegment]; +} + +- (FIRCollectionReference *)parent { + FSTResourcePath *parentPath = [self.key.path pathByRemovingLastSegment]; + return [FIRCollectionReference referenceWithPath:parentPath firestore:self.firestore]; +} + +- (NSString *)path { + return [self.key.path canonicalString]; +} + +- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath { + if (!collectionPath) { + FSTThrowInvalidArgument(@"Collection path cannot be nil."); + } + FSTResourcePath *subPath = [FSTResourcePath pathWithString:collectionPath]; + FSTResourcePath *path = [self.key.path pathByAppendingPath:subPath]; + return [FIRCollectionReference referenceWithPath:path firestore:self.firestore]; +} + +- (void)setData:(NSDictionary *)documentData { + return [self setData:documentData options:[FIRSetOptions overwrite] completion:nil]; +} + +- (void)setData:(NSDictionary *)documentData options:(FIRSetOptions *)options { + return [self setData:documentData options:options completion:nil]; +} + +- (void)setData:(NSDictionary *)documentData + completion:(nullable void (^)(NSError *_Nullable error))completion { + return [self setData:documentData options:[FIRSetOptions overwrite] completion:completion]; +} + +- (void)setData:(NSDictionary *)documentData + options:(FIRSetOptions *)options + completion:(nullable void (^)(NSError *_Nullable error))completion { + FSTParsedSetData *parsed = options.isMerge + ? [self.firestore.dataConverter parsedMergeData:documentData] + : [self.firestore.dataConverter parsedSetData:documentData]; + return [self.firestore.client + writeMutations:[parsed mutationsWithKey:self.key precondition:[FSTPrecondition none]] + completion:completion]; +} + +- (void)updateData:(NSDictionary *)fields { + return [self updateData:fields completion:nil]; +} + +- (void)updateData:(NSDictionary *)fields + completion:(nullable void (^)(NSError *_Nullable error))completion { + FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; + return [self.firestore.client + writeMutations:[parsed mutationsWithKey:self.key + precondition:[FSTPrecondition preconditionWithExists:YES]] + completion:completion]; +} + +- (void)deleteDocument { + return [self deleteDocumentWithCompletion:nil]; +} + +- (void)deleteDocumentWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { + FSTDeleteMutation *mutation = + [[FSTDeleteMutation alloc] initWithKey:self.key precondition:[FSTPrecondition none]]; + return [self.firestore.client writeMutations:@[ mutation ] completion:completion]; +} + +- (void)getDocumentWithCompletion:(void (^)(FIRDocumentSnapshot *_Nullable document, + NSError *_Nullable error))completion { + FSTListenOptions *listenOptions = + [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES + includeDocumentMetadataChanges:YES + waitForSyncWhenOnline:YES]; + + dispatch_semaphore_t registered = dispatch_semaphore_create(0); + __block id listenerRegistration; + FIRDocumentSnapshotBlock listener = ^(FIRDocumentSnapshot *snapshot, NSError *error) { + if (error) { + completion(nil, error); + return; + } + + // Remove query first before passing event to user to avoid user actions affecting the + // now stale query. + dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER); + [listenerRegistration remove]; + + if (!snapshot.exists && snapshot.metadata.fromCache) { + // TODO(dimond): Reconsider how to raise missing documents when offline. + // If we're online and the document doesn't exist then we call the completion with + // a document with document.exists set to false. If we're offline however, we call the + // completion handler with an error. Two options: + // 1) Cache the negative response from the server so we can deliver that even when you're + // offline. + // 2) Actually call the completion handler with an error if the document doesn't exist when + // you are offline. + // TODO(dimond): Use proper error domain + completion(nil, + [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnavailable + userInfo:@{ + NSLocalizedDescriptionKey : + @"Failed to get document because the client is offline.", + }]); + } else { + completion(snapshot, nil); + } + }; + + listenerRegistration = + [self addSnapshotListenerInternalWithOptions:listenOptions listener:listener]; + dispatch_semaphore_signal(registered); +} + +- (id)addSnapshotListener:(FIRDocumentSnapshotBlock)listener { + return [self addSnapshotListenerWithOptions:nil listener:listener]; +} + +- (id)addSnapshotListenerWithOptions: + (nullable FIRDocumentListenOptions *)options + listener:(FIRDocumentSnapshotBlock)listener { + return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options] + listener:listener]; +} + +- (id) +addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions + listener:(FIRDocumentSnapshotBlock)listener { + FIRFirestore *firestore = self.firestore; + FSTQuery *query = [FSTQuery queryWithPath:self.key.path]; + FSTDocumentKey *key = self.key; + + FSTViewSnapshotHandler snapshotHandler = ^(FSTViewSnapshot *snapshot, NSError *error) { + if (error) { + listener(nil, error); + return; + } + + FSTAssert(snapshot.documents.count <= 1, @"Too many document returned on a document query"); + FSTDocument *document = [snapshot.documents documentForKey:key]; + + FIRDocumentSnapshot *result = [FIRDocumentSnapshot snapshotWithFirestore:firestore + documentKey:key + document:document + fromCache:snapshot.fromCache]; + listener(result, nil); + }; + + FSTAsyncQueryListener *asyncListener = + [[FSTAsyncQueryListener alloc] initWithDispatchQueue:self.firestore.client.userDispatchQueue + snapshotHandler:snapshotHandler]; + + FSTQueryListener *internalListener = + [firestore.client listenToQuery:query + options:internalOptions + viewSnapshotHandler:[asyncListener asyncSnapshotHandler]]; + return [[FSTListenerRegistration alloc] initWithClient:self.firestore.client + asyncListener:asyncListener + internalListener:internalListener]; +} + +/** Converts the public API options object to the internal options object. */ +- (FSTListenOptions *)internalOptions:(nullable FIRDocumentListenOptions *)options { + return + [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:options.includeMetadataChanges + includeDocumentMetadataChanges:options.includeMetadataChanges + waitForSyncWhenOnline:NO]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentSnapshot.m b/Firestore/Source/API/FIRDocumentSnapshot.m deleted file mode 100644 index 10709e8..0000000 --- a/Firestore/Source/API/FIRDocumentSnapshot.m +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "FIRDocumentSnapshot.h" - -#import "Firestore/Source/API/FIRDocumentReference+Internal.h" -#import "Firestore/Source/API/FIRFieldPath+Internal.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" -#import "Firestore/Source/API/FIRSnapshotOptions+Internal.h" -#import "Firestore/Source/Model/FSTDatabaseID.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRDocumentSnapshot () - -- (instancetype)initWithFirestore:(FIRFirestore *)firestore - documentKey:(FSTDocumentKey *)documentKey - document:(nullable FSTDocument *)document - fromCache:(BOOL)fromCache NS_DESIGNATED_INITIALIZER; - -@property(nonatomic, strong, readonly) FIRFirestore *firestore; -@property(nonatomic, strong, readonly) FSTDocumentKey *internalKey; -@property(nonatomic, strong, readonly, nullable) FSTDocument *internalDocument; -@property(nonatomic, assign, readonly) BOOL fromCache; - -@end - -@implementation FIRDocumentSnapshot (Internal) - -+ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore - documentKey:(FSTDocumentKey *)documentKey - document:(nullable FSTDocument *)document - fromCache:(BOOL)fromCache { - return [[[self class] alloc] initWithFirestore:firestore - documentKey:documentKey - document:document - fromCache:fromCache]; -} - -@end - -@implementation FIRDocumentSnapshot { - FIRSnapshotMetadata *_cachedMetadata; -} - -@dynamic metadata; - -- (instancetype)initWithFirestore:(FIRFirestore *)firestore - documentKey:(FSTDocumentKey *)documentKey - document:(nullable FSTDocument *)document - fromCache:(BOOL)fromCache { - if (self = [super init]) { - _firestore = firestore; - _internalKey = documentKey; - _internalDocument = document; - _fromCache = fromCache; - } - return self; -} - -// NSObject Methods -- (BOOL)isEqual:(nullable id)other { - if (other == self) return YES; - // self class could be FIRDocumentSnapshot or subtype. So we compare with base type explicitly. - if (![other isKindOfClass:[FIRDocumentSnapshot class]]) return NO; - - return [self isEqualToSnapshot:other]; -} - -- (BOOL)isEqualToSnapshot:(nullable FIRDocumentSnapshot *)snapshot { - if (self == snapshot) return YES; - if (snapshot == nil) return NO; - - return [self.firestore isEqual:snapshot.firestore] && - [self.internalKey isEqual:snapshot.internalKey] && - (self.internalDocument == snapshot.internalDocument || - [self.internalDocument isEqual:snapshot.internalDocument]) && - self.fromCache == snapshot.fromCache; -} - -- (NSUInteger)hash { - NSUInteger hash = [self.firestore hash]; - hash = hash * 31u + [self.internalKey hash]; - hash = hash * 31u + [self.internalDocument hash]; - hash = hash * 31u + (self.fromCache ? 1 : 0); - return hash; -} - -@dynamic exists; - -- (BOOL)exists { - return _internalDocument != nil; -} - -- (FIRDocumentReference *)reference { - return [FIRDocumentReference referenceWithKey:self.internalKey firestore:self.firestore]; -} - -- (NSString *)documentID { - return [self.internalKey.path lastSegment]; -} - -- (FIRSnapshotMetadata *)metadata { - if (!_cachedMetadata) { - _cachedMetadata = [FIRSnapshotMetadata - snapshotMetadataWithPendingWrites:self.internalDocument.hasLocalMutations - fromCache:self.fromCache]; - } - return _cachedMetadata; -} - -- (nullable NSDictionary *)data { - return [self dataWithOptions:[FIRSnapshotOptions defaultOptions]]; -} - -- (nullable NSDictionary *)dataWithOptions:(FIRSnapshotOptions *)options { - return self.internalDocument == nil - ? nil - : [self convertedObject:[self.internalDocument data] - options:[FSTFieldValueOptions optionsForSnapshotOptions:options]]; -} - -- (nullable id)valueForField:(id)field { - return [self valueForField:field options:[FIRSnapshotOptions defaultOptions]]; -} - -- (nullable id)valueForField:(id)field options:(FIRSnapshotOptions *)options { - FIRFieldPath *fieldPath; - - if ([field isKindOfClass:[NSString class]]) { - fieldPath = [FIRFieldPath pathWithDotSeparatedString:field]; - } else if ([field isKindOfClass:[FIRFieldPath class]]) { - fieldPath = field; - } else { - FSTThrowInvalidArgument(@"Subscript key must be an NSString or FIRFieldPath."); - } - - FSTFieldValue *fieldValue = [[self.internalDocument data] valueForPath:fieldPath.internalValue]; - return fieldValue == nil - ? nil - : [self convertedValue:fieldValue - options:[FSTFieldValueOptions optionsForSnapshotOptions:options]]; -} - -- (nullable id)objectForKeyedSubscript:(id)key { - return [self valueForField:key]; -} - -- (id)convertedValue:(FSTFieldValue *)value options:(FSTFieldValueOptions *)options { - if ([value isKindOfClass:[FSTObjectValue class]]) { - return [self convertedObject:(FSTObjectValue *)value options:options]; - } else if ([value isKindOfClass:[FSTArrayValue class]]) { - return [self convertedArray:(FSTArrayValue *)value options:options]; - } else if ([value isKindOfClass:[FSTReferenceValue class]]) { - FSTReferenceValue *ref = (FSTReferenceValue *)value; - FSTDatabaseID *refDatabase = ref.databaseID; - FSTDatabaseID *database = self.firestore.databaseID; - if (![refDatabase isEqualToDatabaseId:database]) { - // TODO(b/32073923): Log this as a proper warning. - NSLog( - @"WARNING: Document %@ contains a document reference within a different database " - "(%@/%@) which is not supported. It will be treated as a reference within the " - "current database (%@/%@) instead.", - self.reference.path, refDatabase.projectID, refDatabase.databaseID, database.projectID, - database.databaseID); - } - return [FIRDocumentReference referenceWithKey:[ref valueWithOptions:options] - firestore:self.firestore]; - } else { - return [value valueWithOptions:options]; - } -} - -- (NSDictionary *)convertedObject:(FSTObjectValue *)objectValue - options:(FSTFieldValueOptions *)options { - NSMutableDictionary *result = [NSMutableDictionary dictionary]; - [objectValue.internalValue - enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *value, BOOL *stop) { - result[key] = [self convertedValue:value options:options]; - }]; - return result; -} - -- (NSArray *)convertedArray:(FSTArrayValue *)arrayValue - options:(FSTFieldValueOptions *)options { - NSArray *internalValue = arrayValue.internalValue; - NSMutableArray *result = [NSMutableArray arrayWithCapacity:internalValue.count]; - [internalValue enumerateObjectsUsingBlock:^(id value, NSUInteger idx, BOOL *stop) { - [result addObject:[self convertedValue:value options:options]]; - }]; - return result; -} - -@end - -@interface FIRQueryDocumentSnapshot () - -- (instancetype)initWithFirestore:(FIRFirestore *)firestore - documentKey:(FSTDocumentKey *)documentKey - document:(FSTDocument *)document - fromCache:(BOOL)fromCache NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FIRQueryDocumentSnapshot - -- (instancetype)initWithFirestore:(FIRFirestore *)firestore - documentKey:(FSTDocumentKey *)documentKey - document:(FSTDocument *)document - fromCache:(BOOL)fromCache { - self = [super initWithFirestore:firestore - documentKey:documentKey - document:document - fromCache:fromCache]; - return self; -} - -- (NSDictionary *)data { - NSDictionary *data = [super data]; - FSTAssert(data, @"Document in a QueryDocumentSnapshot should exist"); - return data; -} - -- (NSDictionary *)dataWithOptions:(FIRSnapshotOptions *)options { - NSDictionary *data = [super dataWithOptions:options]; - FSTAssert(data, @"Document in a QueryDocumentSnapshot should exist"); - return data; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentSnapshot.mm b/Firestore/Source/API/FIRDocumentSnapshot.mm new file mode 100644 index 0000000..10709e8 --- /dev/null +++ b/Firestore/Source/API/FIRDocumentSnapshot.mm @@ -0,0 +1,252 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRDocumentSnapshot.h" + +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRFieldPath+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" +#import "Firestore/Source/API/FIRSnapshotOptions+Internal.h" +#import "Firestore/Source/Model/FSTDatabaseID.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRDocumentSnapshot () + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(nullable FSTDocument *)document + fromCache:(BOOL)fromCache NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FIRFirestore *firestore; +@property(nonatomic, strong, readonly) FSTDocumentKey *internalKey; +@property(nonatomic, strong, readonly, nullable) FSTDocument *internalDocument; +@property(nonatomic, assign, readonly) BOOL fromCache; + +@end + +@implementation FIRDocumentSnapshot (Internal) + ++ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(nullable FSTDocument *)document + fromCache:(BOOL)fromCache { + return [[[self class] alloc] initWithFirestore:firestore + documentKey:documentKey + document:document + fromCache:fromCache]; +} + +@end + +@implementation FIRDocumentSnapshot { + FIRSnapshotMetadata *_cachedMetadata; +} + +@dynamic metadata; + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(nullable FSTDocument *)document + fromCache:(BOOL)fromCache { + if (self = [super init]) { + _firestore = firestore; + _internalKey = documentKey; + _internalDocument = document; + _fromCache = fromCache; + } + return self; +} + +// NSObject Methods +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + // self class could be FIRDocumentSnapshot or subtype. So we compare with base type explicitly. + if (![other isKindOfClass:[FIRDocumentSnapshot class]]) return NO; + + return [self isEqualToSnapshot:other]; +} + +- (BOOL)isEqualToSnapshot:(nullable FIRDocumentSnapshot *)snapshot { + if (self == snapshot) return YES; + if (snapshot == nil) return NO; + + return [self.firestore isEqual:snapshot.firestore] && + [self.internalKey isEqual:snapshot.internalKey] && + (self.internalDocument == snapshot.internalDocument || + [self.internalDocument isEqual:snapshot.internalDocument]) && + self.fromCache == snapshot.fromCache; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.firestore hash]; + hash = hash * 31u + [self.internalKey hash]; + hash = hash * 31u + [self.internalDocument hash]; + hash = hash * 31u + (self.fromCache ? 1 : 0); + return hash; +} + +@dynamic exists; + +- (BOOL)exists { + return _internalDocument != nil; +} + +- (FIRDocumentReference *)reference { + return [FIRDocumentReference referenceWithKey:self.internalKey firestore:self.firestore]; +} + +- (NSString *)documentID { + return [self.internalKey.path lastSegment]; +} + +- (FIRSnapshotMetadata *)metadata { + if (!_cachedMetadata) { + _cachedMetadata = [FIRSnapshotMetadata + snapshotMetadataWithPendingWrites:self.internalDocument.hasLocalMutations + fromCache:self.fromCache]; + } + return _cachedMetadata; +} + +- (nullable NSDictionary *)data { + return [self dataWithOptions:[FIRSnapshotOptions defaultOptions]]; +} + +- (nullable NSDictionary *)dataWithOptions:(FIRSnapshotOptions *)options { + return self.internalDocument == nil + ? nil + : [self convertedObject:[self.internalDocument data] + options:[FSTFieldValueOptions optionsForSnapshotOptions:options]]; +} + +- (nullable id)valueForField:(id)field { + return [self valueForField:field options:[FIRSnapshotOptions defaultOptions]]; +} + +- (nullable id)valueForField:(id)field options:(FIRSnapshotOptions *)options { + FIRFieldPath *fieldPath; + + if ([field isKindOfClass:[NSString class]]) { + fieldPath = [FIRFieldPath pathWithDotSeparatedString:field]; + } else if ([field isKindOfClass:[FIRFieldPath class]]) { + fieldPath = field; + } else { + FSTThrowInvalidArgument(@"Subscript key must be an NSString or FIRFieldPath."); + } + + FSTFieldValue *fieldValue = [[self.internalDocument data] valueForPath:fieldPath.internalValue]; + return fieldValue == nil + ? nil + : [self convertedValue:fieldValue + options:[FSTFieldValueOptions optionsForSnapshotOptions:options]]; +} + +- (nullable id)objectForKeyedSubscript:(id)key { + return [self valueForField:key]; +} + +- (id)convertedValue:(FSTFieldValue *)value options:(FSTFieldValueOptions *)options { + if ([value isKindOfClass:[FSTObjectValue class]]) { + return [self convertedObject:(FSTObjectValue *)value options:options]; + } else if ([value isKindOfClass:[FSTArrayValue class]]) { + return [self convertedArray:(FSTArrayValue *)value options:options]; + } else if ([value isKindOfClass:[FSTReferenceValue class]]) { + FSTReferenceValue *ref = (FSTReferenceValue *)value; + FSTDatabaseID *refDatabase = ref.databaseID; + FSTDatabaseID *database = self.firestore.databaseID; + if (![refDatabase isEqualToDatabaseId:database]) { + // TODO(b/32073923): Log this as a proper warning. + NSLog( + @"WARNING: Document %@ contains a document reference within a different database " + "(%@/%@) which is not supported. It will be treated as a reference within the " + "current database (%@/%@) instead.", + self.reference.path, refDatabase.projectID, refDatabase.databaseID, database.projectID, + database.databaseID); + } + return [FIRDocumentReference referenceWithKey:[ref valueWithOptions:options] + firestore:self.firestore]; + } else { + return [value valueWithOptions:options]; + } +} + +- (NSDictionary *)convertedObject:(FSTObjectValue *)objectValue + options:(FSTFieldValueOptions *)options { + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + [objectValue.internalValue + enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *value, BOOL *stop) { + result[key] = [self convertedValue:value options:options]; + }]; + return result; +} + +- (NSArray *)convertedArray:(FSTArrayValue *)arrayValue + options:(FSTFieldValueOptions *)options { + NSArray *internalValue = arrayValue.internalValue; + NSMutableArray *result = [NSMutableArray arrayWithCapacity:internalValue.count]; + [internalValue enumerateObjectsUsingBlock:^(id value, NSUInteger idx, BOOL *stop) { + [result addObject:[self convertedValue:value options:options]]; + }]; + return result; +} + +@end + +@interface FIRQueryDocumentSnapshot () + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(FSTDocument *)document + fromCache:(BOOL)fromCache NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FIRQueryDocumentSnapshot + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(FSTDocument *)document + fromCache:(BOOL)fromCache { + self = [super initWithFirestore:firestore + documentKey:documentKey + document:document + fromCache:fromCache]; + return self; +} + +- (NSDictionary *)data { + NSDictionary *data = [super data]; + FSTAssert(data, @"Document in a QueryDocumentSnapshot should exist"); + return data; +} + +- (NSDictionary *)dataWithOptions:(FIRSnapshotOptions *)options { + NSDictionary *data = [super dataWithOptions:options]; + FSTAssert(data, @"Document in a QueryDocumentSnapshot should exist"); + return data; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldPath.m b/Firestore/Source/API/FIRFieldPath.m deleted file mode 100644 index f4e532f..0000000 --- a/Firestore/Source/API/FIRFieldPath.m +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/API/FIRFieldPath+Internal.h" - -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FIRFieldPath - -- (instancetype)initWithFields:(NSArray *)fieldNames { - if (fieldNames.count == 0) { - FSTThrowInvalidArgument(@"Invalid field path. Provided names must not be empty."); - } - - for (int i = 0; i < fieldNames.count; ++i) { - if (fieldNames[i].length == 0) { - FSTThrowInvalidArgument(@"Invalid field name at index %d. Field names must not be empty.", i); - } - } - - return [self initPrivate:[FSTFieldPath pathWithSegments:fieldNames]]; -} - -+ (instancetype)documentID { - return [[FIRFieldPath alloc] initPrivate:FSTFieldPath.keyFieldPath]; -} - -- (instancetype)initPrivate:(FSTFieldPath *)fieldPath { - if (self = [super init]) { - _internalValue = fieldPath; - } - return self; -} - -+ (instancetype)pathWithDotSeparatedString:(NSString *)path { - if ([[FIRFieldPath reservedCharactersRegex] - numberOfMatchesInString:path - options:0 - range:NSMakeRange(0, path.length)] > 0) { - FSTThrowInvalidArgument( - @"Invalid field path (%@). Paths must not contain '~', '*', '/', '[', or ']'", path); - } - @try { - return [[FIRFieldPath alloc] initWithFields:[path componentsSeparatedByString:@"."]]; - } @catch (NSException *exception) { - FSTThrowInvalidArgument( - @"Invalid field path (%@). Paths must not be empty, begin with '.', end with '.', or " - @"contain '..'", - path); - } -} - -/** Matches any characters in a field path string that are reserved. */ -+ (NSRegularExpression *)reservedCharactersRegex { - static NSRegularExpression *regex = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - regex = [NSRegularExpression regularExpressionWithPattern:@"[~*/\\[\\]]" options:0 error:nil]; - }); - return regex; -} - -- (id)copyWithZone:(NSZone *__nullable)zone { - return [[[self class] alloc] initPrivate:self.internalValue]; -} - -- (BOOL)isEqual:(nullable id)object { - if (self == object) { - return YES; - } - - if (![object isKindOfClass:[FIRFieldPath class]]) { - return NO; - } - - return [self.internalValue isEqual:((FIRFieldPath *)object).internalValue]; -} - -- (NSUInteger)hash { - return [self.internalValue hash]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldPath.mm b/Firestore/Source/API/FIRFieldPath.mm new file mode 100644 index 0000000..f4e532f --- /dev/null +++ b/Firestore/Source/API/FIRFieldPath.mm @@ -0,0 +1,101 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/API/FIRFieldPath+Internal.h" + +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRFieldPath + +- (instancetype)initWithFields:(NSArray *)fieldNames { + if (fieldNames.count == 0) { + FSTThrowInvalidArgument(@"Invalid field path. Provided names must not be empty."); + } + + for (int i = 0; i < fieldNames.count; ++i) { + if (fieldNames[i].length == 0) { + FSTThrowInvalidArgument(@"Invalid field name at index %d. Field names must not be empty.", i); + } + } + + return [self initPrivate:[FSTFieldPath pathWithSegments:fieldNames]]; +} + ++ (instancetype)documentID { + return [[FIRFieldPath alloc] initPrivate:FSTFieldPath.keyFieldPath]; +} + +- (instancetype)initPrivate:(FSTFieldPath *)fieldPath { + if (self = [super init]) { + _internalValue = fieldPath; + } + return self; +} + ++ (instancetype)pathWithDotSeparatedString:(NSString *)path { + if ([[FIRFieldPath reservedCharactersRegex] + numberOfMatchesInString:path + options:0 + range:NSMakeRange(0, path.length)] > 0) { + FSTThrowInvalidArgument( + @"Invalid field path (%@). Paths must not contain '~', '*', '/', '[', or ']'", path); + } + @try { + return [[FIRFieldPath alloc] initWithFields:[path componentsSeparatedByString:@"."]]; + } @catch (NSException *exception) { + FSTThrowInvalidArgument( + @"Invalid field path (%@). Paths must not be empty, begin with '.', end with '.', or " + @"contain '..'", + path); + } +} + +/** Matches any characters in a field path string that are reserved. */ ++ (NSRegularExpression *)reservedCharactersRegex { + static NSRegularExpression *regex = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + regex = [NSRegularExpression regularExpressionWithPattern:@"[~*/\\[\\]]" options:0 error:nil]; + }); + return regex; +} + +- (id)copyWithZone:(NSZone *__nullable)zone { + return [[[self class] alloc] initPrivate:self.internalValue]; +} + +- (BOOL)isEqual:(nullable id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[FIRFieldPath class]]) { + return NO; + } + + return [self.internalValue isEqual:((FIRFieldPath *)object).internalValue]; +} + +- (NSUInteger)hash { + return [self.internalValue hash]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldValue.m b/Firestore/Source/API/FIRFieldValue.m deleted file mode 100644 index 7ae4fb0..0000000 --- a/Firestore/Source/API/FIRFieldValue.m +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/API/FIRFieldValue+Internal.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRFieldValue () -- (instancetype)initPrivate NS_DESIGNATED_INITIALIZER; -@end - -#pragma mark - FSTDeleteFieldValue - -@interface FSTDeleteFieldValue () -/** Returns a single shared instance of the class. */ -+ (instancetype)deleteFieldValue; -@end - -@implementation FSTDeleteFieldValue - -- (instancetype)initPrivate { - self = [super initPrivate]; - return self; -} - -+ (instancetype)deleteFieldValue { - static FSTDeleteFieldValue *sharedInstance = nil; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - sharedInstance = [[FSTDeleteFieldValue alloc] initPrivate]; - }); - return sharedInstance; -} - -@end - -#pragma mark - FSTServerTimestampFieldValue - -@interface FSTServerTimestampFieldValue () -/** Returns a single shared instance of the class. */ -+ (instancetype)serverTimestampFieldValue; -@end - -@implementation FSTServerTimestampFieldValue - -- (instancetype)initPrivate { - self = [super initPrivate]; - return self; -} - -+ (instancetype)serverTimestampFieldValue { - static FSTServerTimestampFieldValue *sharedInstance = nil; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - sharedInstance = [[FSTServerTimestampFieldValue alloc] initPrivate]; - }); - return sharedInstance; -} - -@end - -#pragma mark - FIRFieldValue - -@implementation FIRFieldValue - -- (instancetype)initPrivate { - self = [super init]; - return self; -} - -+ (instancetype)fieldValueForDelete { - return [FSTDeleteFieldValue deleteFieldValue]; -} - -+ (instancetype)fieldValueForServerTimestamp { - return [FSTServerTimestampFieldValue serverTimestampFieldValue]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldValue.mm b/Firestore/Source/API/FIRFieldValue.mm new file mode 100644 index 0000000..7ae4fb0 --- /dev/null +++ b/Firestore/Source/API/FIRFieldValue.mm @@ -0,0 +1,96 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/API/FIRFieldValue+Internal.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRFieldValue () +- (instancetype)initPrivate NS_DESIGNATED_INITIALIZER; +@end + +#pragma mark - FSTDeleteFieldValue + +@interface FSTDeleteFieldValue () +/** Returns a single shared instance of the class. */ ++ (instancetype)deleteFieldValue; +@end + +@implementation FSTDeleteFieldValue + +- (instancetype)initPrivate { + self = [super initPrivate]; + return self; +} + ++ (instancetype)deleteFieldValue { + static FSTDeleteFieldValue *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTDeleteFieldValue alloc] initPrivate]; + }); + return sharedInstance; +} + +@end + +#pragma mark - FSTServerTimestampFieldValue + +@interface FSTServerTimestampFieldValue () +/** Returns a single shared instance of the class. */ ++ (instancetype)serverTimestampFieldValue; +@end + +@implementation FSTServerTimestampFieldValue + +- (instancetype)initPrivate { + self = [super initPrivate]; + return self; +} + ++ (instancetype)serverTimestampFieldValue { + static FSTServerTimestampFieldValue *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTServerTimestampFieldValue alloc] initPrivate]; + }); + return sharedInstance; +} + +@end + +#pragma mark - FIRFieldValue + +@implementation FIRFieldValue + +- (instancetype)initPrivate { + self = [super init]; + return self; +} + ++ (instancetype)fieldValueForDelete { + return [FSTDeleteFieldValue deleteFieldValue]; +} + ++ (instancetype)fieldValueForServerTimestamp { + return [FSTServerTimestampFieldValue serverTimestampFieldValue]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFirestore.m b/Firestore/Source/API/FIRFirestore.m deleted file mode 100644 index 9df3711..0000000 --- a/Firestore/Source/API/FIRFirestore.m +++ /dev/null @@ -1,317 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "FIRFirestore.h" - -#import -#import -#import - -#import "FIRFirestoreSettings.h" -#import "Firestore/Source/API/FIRCollectionReference+Internal.h" -#import "Firestore/Source/API/FIRDocumentReference+Internal.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRTransaction+Internal.h" -#import "Firestore/Source/API/FIRWriteBatch+Internal.h" -#import "Firestore/Source/API/FSTUserDataConverter.h" - -#import "Firestore/Source/Auth/FSTCredentialsProvider.h" -#import "Firestore/Source/Core/FSTDatabaseInfo.h" -#import "Firestore/Source/Core/FSTFirestoreClient.h" -#import "Firestore/Source/Model/FSTDatabaseID.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTDispatchQueue.h" -#import "Firestore/Source/Util/FSTLogger.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -NSString *const FIRFirestoreErrorDomain = @"FIRFirestoreErrorDomain"; - -@interface FIRFirestore () - -@property(nonatomic, strong) FSTDatabaseID *databaseID; -@property(nonatomic, strong) NSString *persistenceKey; -@property(nonatomic, strong) id credentialsProvider; -@property(nonatomic, strong) FSTDispatchQueue *workerDispatchQueue; - -// Note that `client` is updated after initialization, but marking this readwrite would generate an -// incorrect setter (since we make the assignment to `client` inside an `@synchronized` block. -@property(nonatomic, strong, readonly) FSTFirestoreClient *client; -@property(nonatomic, strong, readonly) FSTUserDataConverter *dataConverter; - -@end - -@implementation FIRFirestore { - // All guarded by @synchronized(self) - FIRFirestoreSettings *_settings; - FSTFirestoreClient *_client; -} - -+ (NSMutableDictionary *)instances { - static dispatch_once_t token = 0; - static NSMutableDictionary *instances; - dispatch_once(&token, ^{ - instances = [NSMutableDictionary dictionary]; - }); - return instances; -} - -+ (instancetype)firestore { - FIRApp *app = [FIRApp defaultApp]; - if (!app) { - FSTThrowInvalidUsage(@"FIRAppNotConfiguredException", - @"Failed to get FirebaseApp instance. Please call FirebaseApp.configure() " - @"before using Firestore"); - } - return [self firestoreForApp:app database:kDefaultDatabaseID]; -} - -+ (instancetype)firestoreForApp:(FIRApp *)app { - return [self firestoreForApp:app database:kDefaultDatabaseID]; -} - -// TODO(b/62410906): make this public -+ (instancetype)firestoreForApp:(FIRApp *)app database:(NSString *)database { - if (!app) { - FSTThrowInvalidArgument( - @"FirebaseApp instance may not be nil. Use FirebaseApp.app() if you'd " - "like to use the default FirebaseApp instance."); - } - if (!database) { - FSTThrowInvalidArgument( - @"database identifier may not be nil. Use '%@' if you want the default " - "database", - kDefaultDatabaseID); - } - NSString *key = [NSString stringWithFormat:@"%@|%@", app.name, database]; - - NSMutableDictionary *instances = self.instances; - @synchronized(instances) { - FIRFirestore *firestore = instances[key]; - if (!firestore) { - NSString *projectID = app.options.projectID; - FSTAssert(projectID, @"FirebaseOptions.projectID cannot be nil."); - - FSTDispatchQueue *workerDispatchQueue = [FSTDispatchQueue - queueWith:dispatch_queue_create("com.google.firebase.firestore", DISPATCH_QUEUE_SERIAL)]; - - id credentialsProvider; - credentialsProvider = [[FSTFirebaseCredentialsProvider alloc] initWithApp:app]; - - NSString *persistenceKey = app.name; - - firestore = [[FIRFirestore alloc] initWithProjectID:projectID - database:database - persistenceKey:persistenceKey - credentialsProvider:credentialsProvider - workerDispatchQueue:workerDispatchQueue - firebaseApp:app]; - instances[key] = firestore; - } - - return firestore; - } -} - -- (instancetype)initWithProjectID:(NSString *)projectID - database:(NSString *)database - persistenceKey:(NSString *)persistenceKey - credentialsProvider:(id)credentialsProvider - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue - firebaseApp:(FIRApp *)app { - if (self = [super init]) { - _databaseID = [FSTDatabaseID databaseIDWithProject:projectID database:database]; - FSTPreConverterBlock block = ^id _Nullable(id _Nullable input) { - if ([input isKindOfClass:[FIRDocumentReference class]]) { - FIRDocumentReference *documentReference = (FIRDocumentReference *)input; - return [[FSTDocumentKeyReference alloc] initWithKey:documentReference.key - databaseID:documentReference.firestore.databaseID]; - } else { - return input; - } - }; - _dataConverter = - [[FSTUserDataConverter alloc] initWithDatabaseID:_databaseID preConverter:block]; - _persistenceKey = persistenceKey; - _credentialsProvider = credentialsProvider; - _workerDispatchQueue = workerDispatchQueue; - _app = app; - _settings = [[FIRFirestoreSettings alloc] init]; - } - return self; -} - -- (FIRFirestoreSettings *)settings { - @synchronized(self) { - // Disallow mutation of our internal settings - return [_settings copy]; - } -} - -- (void)setSettings:(FIRFirestoreSettings *)settings { - @synchronized(self) { - // As a special exception, don't throw if the same settings are passed repeatedly. This should - // make it more friendly to create a Firestore instance. - if (_client && ![_settings isEqual:settings]) { - FSTThrowInvalidUsage(@"FIRIllegalStateException", - @"Firestore instance has already been started and its settings can no " - "longer be changed. You can only set settings before calling any " - "other methods on a Firestore instance."); - } - _settings = [settings copy]; - } -} - -/** - * Ensures that the FirestoreClient is configured and returns it. - */ -- (FSTFirestoreClient *)client { - [self ensureClientConfigured]; - return _client; -} - -- (void)ensureClientConfigured { - @synchronized(self) { - if (!_client) { - // These values are validated elsewhere; this is just double-checking: - FSTAssert(_settings.host, @"FirestoreSettings.host cannot be nil."); - FSTAssert(_settings.dispatchQueue, @"FirestoreSettings.dispatchQueue cannot be nil."); - - FSTDatabaseInfo *databaseInfo = - [FSTDatabaseInfo databaseInfoWithDatabaseID:_databaseID - persistenceKey:_persistenceKey - host:_settings.host - sslEnabled:_settings.sslEnabled]; - - FSTDispatchQueue *userDispatchQueue = [FSTDispatchQueue queueWith:_settings.dispatchQueue]; - - _client = [FSTFirestoreClient clientWithDatabaseInfo:databaseInfo - usePersistence:_settings.persistenceEnabled - credentialsProvider:_credentialsProvider - userDispatchQueue:userDispatchQueue - workerDispatchQueue:_workerDispatchQueue]; - } - } -} - -- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath { - if (!collectionPath) { - FSTThrowInvalidArgument(@"Collection path cannot be nil."); - } - [self ensureClientConfigured]; - FSTResourcePath *path = [FSTResourcePath pathWithString:collectionPath]; - return [FIRCollectionReference referenceWithPath:path firestore:self]; -} - -- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath { - if (!documentPath) { - FSTThrowInvalidArgument(@"Document path cannot be nil."); - } - [self ensureClientConfigured]; - FSTResourcePath *path = [FSTResourcePath pathWithString:documentPath]; - return [FIRDocumentReference referenceWithPath:path firestore:self]; -} - -- (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **))updateBlock - dispatchQueue:(dispatch_queue_t)queue - completion: - (void (^)(id _Nullable result, NSError *_Nullable error))completion { - // We wrap the function they provide in order to use internal implementation classes for - // FSTTransaction, and to run the user callback block on the proper queue. - if (!updateBlock) { - FSTThrowInvalidArgument(@"Transaction block cannot be nil."); - } else if (!completion) { - FSTThrowInvalidArgument(@"Transaction completion block cannot be nil."); - } - - FSTTransactionBlock wrappedUpdate = - ^(FSTTransaction *internalTransaction, - void (^internalCompletion)(id _Nullable, NSError *_Nullable)) { - FIRTransaction *transaction = - [FIRTransaction transactionWithFSTTransaction:internalTransaction firestore:self]; - dispatch_async(queue, ^{ - NSError *_Nullable error = nil; - id _Nullable result = updateBlock(transaction, &error); - if (error) { - // Force the result to be nil in the case of an error, in case the user set both. - result = nil; - } - internalCompletion(result, error); - }); - }; - [self.client transactionWithRetries:5 updateBlock:wrappedUpdate completion:completion]; -} - -- (FIRWriteBatch *)batch { - [self ensureClientConfigured]; - - return [FIRWriteBatch writeBatchWithFirestore:self]; -} - -- (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **error))updateBlock - completion: - (void (^)(id _Nullable result, NSError *_Nullable error))completion { - static dispatch_queue_t transactionDispatchQueue; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - transactionDispatchQueue = dispatch_queue_create("com.google.firebase.firestore.transaction", - DISPATCH_QUEUE_CONCURRENT); - }); - [self runTransactionWithBlock:updateBlock - dispatchQueue:transactionDispatchQueue - completion:completion]; -} - -- (void)shutdownWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { - FSTFirestoreClient *client; - @synchronized(self) { - client = _client; - _client = nil; - } - - if (!client) { - // We should be dispatching the callback on the user dispatch queue but if the client is nil - // here that queue was never created. - completion(nil); - } else { - [client shutdownWithCompletion:completion]; - } -} - -+ (BOOL)isLoggingEnabled { - return FIRIsLoggableLevel(FIRLoggerLevelDebug, NO); -} - -+ (void)enableLogging:(BOOL)logging { - FIRSetLoggerLevel(logging ? FIRLoggerLevelDebug : FIRLoggerLevelNotice); -} - -- (void)enableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { - [self ensureClientConfigured]; - [self.client enableNetworkWithCompletion:completion]; -} - -- (void)disableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable))completion { - [self ensureClientConfigured]; - [self.client disableNetworkWithCompletion:completion]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFirestore.mm b/Firestore/Source/API/FIRFirestore.mm new file mode 100644 index 0000000..10367bd --- /dev/null +++ b/Firestore/Source/API/FIRFirestore.mm @@ -0,0 +1,317 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRFirestore.h" + +#import +#import +#import + +#import "FIRFirestoreSettings.h" +#import "Firestore/Source/API/FIRCollectionReference+Internal.h" +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRTransaction+Internal.h" +#import "Firestore/Source/API/FIRWriteBatch+Internal.h" +#import "Firestore/Source/API/FSTUserDataConverter.h" + +#import "Firestore/Source/Auth/FSTCredentialsProvider.h" +#import "Firestore/Source/Core/FSTDatabaseInfo.h" +#import "Firestore/Source/Core/FSTFirestoreClient.h" +#import "Firestore/Source/Model/FSTDatabaseID.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTDispatchQueue.h" +#import "Firestore/Source/Util/FSTLogger.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +extern "C" NSString *const FIRFirestoreErrorDomain = @"FIRFirestoreErrorDomain"; + +@interface FIRFirestore () + +@property(nonatomic, strong) FSTDatabaseID *databaseID; +@property(nonatomic, strong) NSString *persistenceKey; +@property(nonatomic, strong) id credentialsProvider; +@property(nonatomic, strong) FSTDispatchQueue *workerDispatchQueue; + +// Note that `client` is updated after initialization, but marking this readwrite would generate an +// incorrect setter (since we make the assignment to `client` inside an `@synchronized` block. +@property(nonatomic, strong, readonly) FSTFirestoreClient *client; +@property(nonatomic, strong, readonly) FSTUserDataConverter *dataConverter; + +@end + +@implementation FIRFirestore { + // All guarded by @synchronized(self) + FIRFirestoreSettings *_settings; + FSTFirestoreClient *_client; +} + ++ (NSMutableDictionary *)instances { + static dispatch_once_t token = 0; + static NSMutableDictionary *instances; + dispatch_once(&token, ^{ + instances = [NSMutableDictionary dictionary]; + }); + return instances; +} + ++ (instancetype)firestore { + FIRApp *app = [FIRApp defaultApp]; + if (!app) { + FSTThrowInvalidUsage(@"FIRAppNotConfiguredException", + @"Failed to get FirebaseApp instance. Please call FirebaseApp.configure() " + @"before using Firestore"); + } + return [self firestoreForApp:app database:kDefaultDatabaseID]; +} + ++ (instancetype)firestoreForApp:(FIRApp *)app { + return [self firestoreForApp:app database:kDefaultDatabaseID]; +} + +// TODO(b/62410906): make this public ++ (instancetype)firestoreForApp:(FIRApp *)app database:(NSString *)database { + if (!app) { + FSTThrowInvalidArgument( + @"FirebaseApp instance may not be nil. Use FirebaseApp.app() if you'd " + "like to use the default FirebaseApp instance."); + } + if (!database) { + FSTThrowInvalidArgument( + @"database identifier may not be nil. Use '%@' if you want the default " + "database", + kDefaultDatabaseID); + } + NSString *key = [NSString stringWithFormat:@"%@|%@", app.name, database]; + + NSMutableDictionary *instances = self.instances; + @synchronized(instances) { + FIRFirestore *firestore = instances[key]; + if (!firestore) { + NSString *projectID = app.options.projectID; + FSTAssert(projectID, @"FirebaseOptions.projectID cannot be nil."); + + FSTDispatchQueue *workerDispatchQueue = [FSTDispatchQueue + queueWith:dispatch_queue_create("com.google.firebase.firestore", DISPATCH_QUEUE_SERIAL)]; + + id credentialsProvider; + credentialsProvider = [[FSTFirebaseCredentialsProvider alloc] initWithApp:app]; + + NSString *persistenceKey = app.name; + + firestore = [[FIRFirestore alloc] initWithProjectID:projectID + database:database + persistenceKey:persistenceKey + credentialsProvider:credentialsProvider + workerDispatchQueue:workerDispatchQueue + firebaseApp:app]; + instances[key] = firestore; + } + + return firestore; + } +} + +- (instancetype)initWithProjectID:(NSString *)projectID + database:(NSString *)database + persistenceKey:(NSString *)persistenceKey + credentialsProvider:(id)credentialsProvider + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + firebaseApp:(FIRApp *)app { + if (self = [super init]) { + _databaseID = [FSTDatabaseID databaseIDWithProject:projectID database:database]; + FSTPreConverterBlock block = ^id _Nullable(id _Nullable input) { + if ([input isKindOfClass:[FIRDocumentReference class]]) { + FIRDocumentReference *documentReference = (FIRDocumentReference *)input; + return [[FSTDocumentKeyReference alloc] initWithKey:documentReference.key + databaseID:documentReference.firestore.databaseID]; + } else { + return input; + } + }; + _dataConverter = + [[FSTUserDataConverter alloc] initWithDatabaseID:_databaseID preConverter:block]; + _persistenceKey = persistenceKey; + _credentialsProvider = credentialsProvider; + _workerDispatchQueue = workerDispatchQueue; + _app = app; + _settings = [[FIRFirestoreSettings alloc] init]; + } + return self; +} + +- (FIRFirestoreSettings *)settings { + @synchronized(self) { + // Disallow mutation of our internal settings + return [_settings copy]; + } +} + +- (void)setSettings:(FIRFirestoreSettings *)settings { + @synchronized(self) { + // As a special exception, don't throw if the same settings are passed repeatedly. This should + // make it more friendly to create a Firestore instance. + if (_client && ![_settings isEqual:settings]) { + FSTThrowInvalidUsage(@"FIRIllegalStateException", + @"Firestore instance has already been started and its settings can no " + "longer be changed. You can only set settings before calling any " + "other methods on a Firestore instance."); + } + _settings = [settings copy]; + } +} + +/** + * Ensures that the FirestoreClient is configured and returns it. + */ +- (FSTFirestoreClient *)client { + [self ensureClientConfigured]; + return _client; +} + +- (void)ensureClientConfigured { + @synchronized(self) { + if (!_client) { + // These values are validated elsewhere; this is just double-checking: + FSTAssert(_settings.host, @"FirestoreSettings.host cannot be nil."); + FSTAssert(_settings.dispatchQueue, @"FirestoreSettings.dispatchQueue cannot be nil."); + + FSTDatabaseInfo *databaseInfo = + [FSTDatabaseInfo databaseInfoWithDatabaseID:_databaseID + persistenceKey:_persistenceKey + host:_settings.host + sslEnabled:_settings.sslEnabled]; + + FSTDispatchQueue *userDispatchQueue = [FSTDispatchQueue queueWith:_settings.dispatchQueue]; + + _client = [FSTFirestoreClient clientWithDatabaseInfo:databaseInfo + usePersistence:_settings.persistenceEnabled + credentialsProvider:_credentialsProvider + userDispatchQueue:userDispatchQueue + workerDispatchQueue:_workerDispatchQueue]; + } + } +} + +- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath { + if (!collectionPath) { + FSTThrowInvalidArgument(@"Collection path cannot be nil."); + } + [self ensureClientConfigured]; + FSTResourcePath *path = [FSTResourcePath pathWithString:collectionPath]; + return [FIRCollectionReference referenceWithPath:path firestore:self]; +} + +- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath { + if (!documentPath) { + FSTThrowInvalidArgument(@"Document path cannot be nil."); + } + [self ensureClientConfigured]; + FSTResourcePath *path = [FSTResourcePath pathWithString:documentPath]; + return [FIRDocumentReference referenceWithPath:path firestore:self]; +} + +- (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **))updateBlock + dispatchQueue:(dispatch_queue_t)queue + completion: + (void (^)(id _Nullable result, NSError *_Nullable error))completion { + // We wrap the function they provide in order to use internal implementation classes for + // FSTTransaction, and to run the user callback block on the proper queue. + if (!updateBlock) { + FSTThrowInvalidArgument(@"Transaction block cannot be nil."); + } else if (!completion) { + FSTThrowInvalidArgument(@"Transaction completion block cannot be nil."); + } + + FSTTransactionBlock wrappedUpdate = + ^(FSTTransaction *internalTransaction, + void (^internalCompletion)(id _Nullable, NSError *_Nullable)) { + FIRTransaction *transaction = + [FIRTransaction transactionWithFSTTransaction:internalTransaction firestore:self]; + dispatch_async(queue, ^{ + NSError *_Nullable error = nil; + id _Nullable result = updateBlock(transaction, &error); + if (error) { + // Force the result to be nil in the case of an error, in case the user set both. + result = nil; + } + internalCompletion(result, error); + }); + }; + [self.client transactionWithRetries:5 updateBlock:wrappedUpdate completion:completion]; +} + +- (FIRWriteBatch *)batch { + [self ensureClientConfigured]; + + return [FIRWriteBatch writeBatchWithFirestore:self]; +} + +- (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **error))updateBlock + completion: + (void (^)(id _Nullable result, NSError *_Nullable error))completion { + static dispatch_queue_t transactionDispatchQueue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + transactionDispatchQueue = dispatch_queue_create("com.google.firebase.firestore.transaction", + DISPATCH_QUEUE_CONCURRENT); + }); + [self runTransactionWithBlock:updateBlock + dispatchQueue:transactionDispatchQueue + completion:completion]; +} + +- (void)shutdownWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { + FSTFirestoreClient *client; + @synchronized(self) { + client = _client; + _client = nil; + } + + if (!client) { + // We should be dispatching the callback on the user dispatch queue but if the client is nil + // here that queue was never created. + completion(nil); + } else { + [client shutdownWithCompletion:completion]; + } +} + ++ (BOOL)isLoggingEnabled { + return FIRIsLoggableLevel(FIRLoggerLevelDebug, NO); +} + ++ (void)enableLogging:(BOOL)logging { + FIRSetLoggerLevel(logging ? FIRLoggerLevelDebug : FIRLoggerLevelNotice); +} + +- (void)enableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { + [self ensureClientConfigured]; + [self.client enableNetworkWithCompletion:completion]; +} + +- (void)disableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable))completion { + [self ensureClientConfigured]; + [self.client disableNetworkWithCompletion:completion]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFirestoreSettings.m b/Firestore/Source/API/FIRFirestoreSettings.m deleted file mode 100644 index 9677ff6..0000000 --- a/Firestore/Source/API/FIRFirestoreSettings.m +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "FIRFirestoreSettings.h" - -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -static NSString *const kDefaultHost = @"firestore.googleapis.com"; -static const BOOL kDefaultSSLEnabled = YES; -static const BOOL kDefaultPersistenceEnabled = YES; - -@implementation FIRFirestoreSettings - -- (instancetype)init { - if (self = [super init]) { - _host = kDefaultHost; - _sslEnabled = kDefaultSSLEnabled; - _dispatchQueue = dispatch_get_main_queue(); - _persistenceEnabled = kDefaultPersistenceEnabled; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (self == other) { - return YES; - } else if (![other isKindOfClass:[FIRFirestoreSettings class]]) { - return NO; - } - - FIRFirestoreSettings *otherSettings = (FIRFirestoreSettings *)other; - return [self.host isEqual:otherSettings.host] && - self.isSSLEnabled == otherSettings.isSSLEnabled && - self.dispatchQueue == otherSettings.dispatchQueue && - self.isPersistenceEnabled == otherSettings.isPersistenceEnabled; -} - -- (NSUInteger)hash { - NSUInteger result = [self.host hash]; - result = 31 * result + (self.isSSLEnabled ? 1231 : 1237); - // Ignore the dispatchQueue to avoid having to deal with sizeof(dispatch_queue_t). - result = 31 * result + (self.isPersistenceEnabled ? 1231 : 1237); - return result; -} - -- (id)copyWithZone:(nullable NSZone *)zone { - FIRFirestoreSettings *copy = [[FIRFirestoreSettings alloc] init]; - copy.host = _host; - copy.sslEnabled = _sslEnabled; - copy.dispatchQueue = _dispatchQueue; - copy.persistenceEnabled = _persistenceEnabled; - return copy; -} - -- (void)setHost:(NSString *)host { - if (!host) { - FSTThrowInvalidArgument( - @"host setting may not be nil. You should generally just use the default value " - "(which is %@)", - kDefaultHost); - } - _host = [host mutableCopy]; -} - -- (void)setDispatchQueue:(dispatch_queue_t)dispatchQueue { - if (!dispatchQueue) { - FSTThrowInvalidArgument( - @"dispatch queue setting may not be nil. Create a new dispatch queue with " - "dispatch_queue_create(\"com.example.MyQueue\", NULL) or just use the default " - "(which is the main queue, returned from dispatch_get_main_queue())"); - } - _dispatchQueue = dispatchQueue; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFirestoreSettings.mm b/Firestore/Source/API/FIRFirestoreSettings.mm new file mode 100644 index 0000000..9677ff6 --- /dev/null +++ b/Firestore/Source/API/FIRFirestoreSettings.mm @@ -0,0 +1,92 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRFirestoreSettings.h" + +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const kDefaultHost = @"firestore.googleapis.com"; +static const BOOL kDefaultSSLEnabled = YES; +static const BOOL kDefaultPersistenceEnabled = YES; + +@implementation FIRFirestoreSettings + +- (instancetype)init { + if (self = [super init]) { + _host = kDefaultHost; + _sslEnabled = kDefaultSSLEnabled; + _dispatchQueue = dispatch_get_main_queue(); + _persistenceEnabled = kDefaultPersistenceEnabled; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } else if (![other isKindOfClass:[FIRFirestoreSettings class]]) { + return NO; + } + + FIRFirestoreSettings *otherSettings = (FIRFirestoreSettings *)other; + return [self.host isEqual:otherSettings.host] && + self.isSSLEnabled == otherSettings.isSSLEnabled && + self.dispatchQueue == otherSettings.dispatchQueue && + self.isPersistenceEnabled == otherSettings.isPersistenceEnabled; +} + +- (NSUInteger)hash { + NSUInteger result = [self.host hash]; + result = 31 * result + (self.isSSLEnabled ? 1231 : 1237); + // Ignore the dispatchQueue to avoid having to deal with sizeof(dispatch_queue_t). + result = 31 * result + (self.isPersistenceEnabled ? 1231 : 1237); + return result; +} + +- (id)copyWithZone:(nullable NSZone *)zone { + FIRFirestoreSettings *copy = [[FIRFirestoreSettings alloc] init]; + copy.host = _host; + copy.sslEnabled = _sslEnabled; + copy.dispatchQueue = _dispatchQueue; + copy.persistenceEnabled = _persistenceEnabled; + return copy; +} + +- (void)setHost:(NSString *)host { + if (!host) { + FSTThrowInvalidArgument( + @"host setting may not be nil. You should generally just use the default value " + "(which is %@)", + kDefaultHost); + } + _host = [host mutableCopy]; +} + +- (void)setDispatchQueue:(dispatch_queue_t)dispatchQueue { + if (!dispatchQueue) { + FSTThrowInvalidArgument( + @"dispatch queue setting may not be nil. Create a new dispatch queue with " + "dispatch_queue_create(\"com.example.MyQueue\", NULL) or just use the default " + "(which is the main queue, returned from dispatch_get_main_queue())"); + } + _dispatchQueue = dispatchQueue; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFirestoreVersion.m b/Firestore/Source/API/FIRFirestoreVersion.m deleted file mode 100644 index 4f8bb28..0000000 --- a/Firestore/Source/API/FIRFirestoreVersion.m +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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. - */ - -#ifndef FIRFirestore_VERSION -#error "FIRFirestore_VERSION is not defined: add -DFIRFirestore_VERSION=... to the build invocation" -#endif - -// The following two macros supply the incantation so that the C -// preprocessor does not try to parse the version as a floating -// point number. See -// https://www.guyrutenberg.com/2008/12/20/expanding-macros-into-string-constants-in-c/ -#define STR(x) STR_EXPAND(x) -#define STR_EXPAND(x) #x - -const unsigned char *const FirebaseFirestoreVersionString = - (const unsigned char *const)STR(FIRFirestore_VERSION); diff --git a/Firestore/Source/API/FIRFirestoreVersion.mm b/Firestore/Source/API/FIRFirestoreVersion.mm new file mode 100644 index 0000000..b1fe480 --- /dev/null +++ b/Firestore/Source/API/FIRFirestoreVersion.mm @@ -0,0 +1,29 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#ifndef FIRFirestore_VERSION +#error "FIRFirestore_VERSION is not defined: add -DFIRFirestore_VERSION=... to the build invocation" +#endif + +// The following two macros supply the incantation so that the C +// preprocessor does not try to parse the version as a floating +// point number. See +// https://www.guyrutenberg.com/2008/12/20/expanding-macros-into-string-constants-in-c/ +#define STR(x) STR_EXPAND(x) +#define STR_EXPAND(x) #x + +extern "C" const unsigned char *const FirebaseFirestoreVersionString = + (const unsigned char *const)STR(FIRFirestore_VERSION); diff --git a/Firestore/Source/API/FIRListenerRegistration.m b/Firestore/Source/API/FIRListenerRegistration.m deleted file mode 100644 index 9f4ddd5..0000000 --- a/Firestore/Source/API/FIRListenerRegistration.m +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/API/FIRListenerRegistration+Internal.h" - -#import "Firestore/Source/Core/FSTFirestoreClient.h" -#import "Firestore/Source/Util/FSTAsyncQueryListener.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTListenerRegistration () - -/** The client that was used to register this listen. */ -@property(nonatomic, strong, readonly) FSTFirestoreClient *client; - -/** The async listener that is used to mute events synchronously. */ -@property(nonatomic, strong, readonly) FSTAsyncQueryListener *asyncListener; - -/** The internal FSTQueryListener that can be used to unlisten the query. */ -@property(nonatomic, strong, readwrite) FSTQueryListener *internalListener; - -@end - -@implementation FSTListenerRegistration - -- (instancetype)initWithClient:(FSTFirestoreClient *)client - asyncListener:(FSTAsyncQueryListener *)asyncListener - internalListener:(FSTQueryListener *)internalListener { - if (self = [super init]) { - _client = client; - _asyncListener = asyncListener; - _internalListener = internalListener; - } - return self; -} - -- (void)remove { - [self.asyncListener mute]; - [self.client removeListener:self.internalListener]; - _internalListener = nil; - _asyncListener = nil; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRListenerRegistration.mm b/Firestore/Source/API/FIRListenerRegistration.mm new file mode 100644 index 0000000..9f4ddd5 --- /dev/null +++ b/Firestore/Source/API/FIRListenerRegistration.mm @@ -0,0 +1,59 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/API/FIRListenerRegistration+Internal.h" + +#import "Firestore/Source/Core/FSTFirestoreClient.h" +#import "Firestore/Source/Util/FSTAsyncQueryListener.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTListenerRegistration () + +/** The client that was used to register this listen. */ +@property(nonatomic, strong, readonly) FSTFirestoreClient *client; + +/** The async listener that is used to mute events synchronously. */ +@property(nonatomic, strong, readonly) FSTAsyncQueryListener *asyncListener; + +/** The internal FSTQueryListener that can be used to unlisten the query. */ +@property(nonatomic, strong, readwrite) FSTQueryListener *internalListener; + +@end + +@implementation FSTListenerRegistration + +- (instancetype)initWithClient:(FSTFirestoreClient *)client + asyncListener:(FSTAsyncQueryListener *)asyncListener + internalListener:(FSTQueryListener *)internalListener { + if (self = [super init]) { + _client = client; + _asyncListener = asyncListener; + _internalListener = internalListener; + } + return self; +} + +- (void)remove { + [self.asyncListener mute]; + [self.client removeListener:self.internalListener]; + _internalListener = nil; + _asyncListener = nil; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuery.m b/Firestore/Source/API/FIRQuery.m deleted file mode 100644 index 1bbf91e..0000000 --- a/Firestore/Source/API/FIRQuery.m +++ /dev/null @@ -1,633 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "FIRQuery.h" - -#import "FIRDocumentReference.h" -#import "Firestore/Source/API/FIRDocumentReference+Internal.h" -#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" -#import "Firestore/Source/API/FIRFieldPath+Internal.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRListenerRegistration+Internal.h" -#import "Firestore/Source/API/FIRQuery+Internal.h" -#import "Firestore/Source/API/FIRQuerySnapshot+Internal.h" -#import "Firestore/Source/API/FIRQuery_Init.h" -#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" -#import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Core/FSTEventManager.h" -#import "Firestore/Source/Core/FSTFirestoreClient.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTAsyncQueryListener.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRQueryListenOptions () - -- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges - includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges - NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FIRQueryListenOptions - -+ (instancetype)options { - return [[FIRQueryListenOptions alloc] init]; -} - -- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges - includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges { - if (self = [super init]) { - _includeQueryMetadataChanges = includeQueryMetadataChanges; - _includeDocumentMetadataChanges = includeDocumentMetadataChanges; - } - return self; -} - -- (instancetype)init { - return [self initWithIncludeQueryMetadataChanges:NO includeDocumentMetadataChanges:NO]; -} - -- (instancetype)includeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges { - return [[FIRQueryListenOptions alloc] - initWithIncludeQueryMetadataChanges:includeQueryMetadataChanges - includeDocumentMetadataChanges:_includeDocumentMetadataChanges]; -} - -- (instancetype)includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges { - return [[FIRQueryListenOptions alloc] - initWithIncludeQueryMetadataChanges:_includeQueryMetadataChanges - includeDocumentMetadataChanges:includeDocumentMetadataChanges]; -} - -@end - -@interface FIRQuery () -@property(nonatomic, strong, readonly) FSTQuery *query; -@end - -@implementation FIRQuery (Internal) -+ (instancetype)referenceWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore { - return [[FIRQuery alloc] initWithQuery:query firestore:firestore]; -} -@end - -@implementation FIRQuery - -#pragma mark - Constructor Methods - -- (instancetype)initWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore { - if (self = [super init]) { - _query = query; - _firestore = firestore; - } - return self; -} - -#pragma mark - NSObject Methods - -- (BOOL)isEqual:(nullable id)other { - if (other == self) return YES; - if (![[other class] isEqual:[self class]]) return NO; - - return [self isEqualToQuery:other]; -} - -- (BOOL)isEqualToQuery:(nullable FIRQuery *)query { - if (self == query) return YES; - if (query == nil) return NO; - - return [self.firestore isEqual:query.firestore] && [self.query isEqual:query.query]; -} - -- (NSUInteger)hash { - NSUInteger hash = [self.firestore hash]; - hash = hash * 31u + [self.query hash]; - return hash; -} - -#pragma mark - Public Methods - -- (void)getDocumentsWithCompletion:(void (^)(FIRQuerySnapshot *_Nullable snapshot, - NSError *_Nullable error))completion { - FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES - includeDocumentMetadataChanges:YES - waitForSyncWhenOnline:YES]; - - dispatch_semaphore_t registered = dispatch_semaphore_create(0); - __block id listenerRegistration; - FIRQuerySnapshotBlock listener = ^(FIRQuerySnapshot *snapshot, NSError *error) { - if (error) { - completion(nil, error); - return; - } - - // Remove query first before passing event to user to avoid user actions affecting the - // now stale query. - dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER); - [listenerRegistration remove]; - - completion(snapshot, nil); - }; - - listenerRegistration = [self addSnapshotListenerInternalWithOptions:options listener:listener]; - dispatch_semaphore_signal(registered); -} - -- (id)addSnapshotListener:(FIRQuerySnapshotBlock)listener { - return [self addSnapshotListenerWithOptions:nil listener:listener]; -} - -- (id)addSnapshotListenerWithOptions: - (nullable FIRQueryListenOptions *)options - listener:(FIRQuerySnapshotBlock)listener { - return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options] - listener:listener]; -} - -- (id) -addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions - listener:(FIRQuerySnapshotBlock)listener { - FIRFirestore *firestore = self.firestore; - FSTQuery *query = self.query; - - FSTViewSnapshotHandler snapshotHandler = ^(FSTViewSnapshot *snapshot, NSError *error) { - if (error) { - listener(nil, error); - return; - } - - FIRSnapshotMetadata *metadata = - [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:snapshot.hasPendingWrites - fromCache:snapshot.fromCache]; - - listener([FIRQuerySnapshot snapshotWithFirestore:firestore - originalQuery:query - snapshot:snapshot - metadata:metadata], - nil); - }; - - FSTAsyncQueryListener *asyncListener = - [[FSTAsyncQueryListener alloc] initWithDispatchQueue:self.firestore.client.userDispatchQueue - snapshotHandler:snapshotHandler]; - - FSTQueryListener *internalListener = - [firestore.client listenToQuery:query - options:internalOptions - viewSnapshotHandler:[asyncListener asyncSnapshotHandler]]; - return [[FSTListenerRegistration alloc] initWithClient:self.firestore.client - asyncListener:asyncListener - internalListener:internalListener]; -} - -- (FIRQuery *)queryWhereField:(NSString *)field isEqualTo:(id)value { - return [self queryWithFilterOperator:FSTRelationFilterOperatorEqual field:field value:value]; -} - -- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isEqualTo:(id)value { - return [self queryWithFilterOperator:FSTRelationFilterOperatorEqual - path:path.internalValue - value:value]; -} - -- (FIRQuery *)queryWhereField:(NSString *)field isLessThan:(id)value { - return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThan field:field value:value]; -} - -- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isLessThan:(id)value { - return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThan - path:path.internalValue - value:value]; -} - -- (FIRQuery *)queryWhereField:(NSString *)field isLessThanOrEqualTo:(id)value { - return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThanOrEqual - field:field - value:value]; -} - -- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isLessThanOrEqualTo:(id)value { - return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThanOrEqual - path:path.internalValue - value:value]; -} - -- (FIRQuery *)queryWhereField:(NSString *)field isGreaterThan:(id)value { - return - [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThan field:field value:value]; -} - -- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isGreaterThan:(id)value { - return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThan - path:path.internalValue - value:value]; -} - -- (FIRQuery *)queryWhereField:(NSString *)field isGreaterThanOrEqualTo:(id)value { - return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThanOrEqual - field:field - value:value]; -} - -- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isGreaterThanOrEqualTo:(id)value { - return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThanOrEqual - path:path.internalValue - value:value]; -} - -- (FIRQuery *)queryFilteredUsingComparisonPredicate:(NSPredicate *)predicate { - NSComparisonPredicate *comparison = (NSComparisonPredicate *)predicate; - if (comparison.comparisonPredicateModifier != NSDirectPredicateModifier) { - FSTThrowInvalidArgument(@"Invalid query. Predicate cannot have an aggregate modifier."); - } - NSString *path; - id value = nil; - if ([comparison.leftExpression expressionType] == NSKeyPathExpressionType && - [comparison.rightExpression expressionType] == NSConstantValueExpressionType) { - path = comparison.leftExpression.keyPath; - value = comparison.rightExpression.constantValue; - switch (comparison.predicateOperatorType) { - case NSEqualToPredicateOperatorType: - return [self queryWhereField:path isEqualTo:value]; - case NSLessThanPredicateOperatorType: - return [self queryWhereField:path isLessThan:value]; - case NSLessThanOrEqualToPredicateOperatorType: - return [self queryWhereField:path isLessThanOrEqualTo:value]; - case NSGreaterThanPredicateOperatorType: - return [self queryWhereField:path isGreaterThan:value]; - case NSGreaterThanOrEqualToPredicateOperatorType: - return [self queryWhereField:path isGreaterThanOrEqualTo:value]; - default:; // Fallback below to throw assertion. - } - } else if ([comparison.leftExpression expressionType] == NSConstantValueExpressionType && - [comparison.rightExpression expressionType] == NSKeyPathExpressionType) { - path = comparison.rightExpression.keyPath; - value = comparison.leftExpression.constantValue; - switch (comparison.predicateOperatorType) { - case NSEqualToPredicateOperatorType: - return [self queryWhereField:path isEqualTo:value]; - case NSLessThanPredicateOperatorType: - return [self queryWhereField:path isGreaterThan:value]; - case NSLessThanOrEqualToPredicateOperatorType: - return [self queryWhereField:path isGreaterThanOrEqualTo:value]; - case NSGreaterThanPredicateOperatorType: - return [self queryWhereField:path isLessThan:value]; - case NSGreaterThanOrEqualToPredicateOperatorType: - return [self queryWhereField:path isLessThanOrEqualTo:value]; - default:; // Fallback below to throw assertion. - } - } else { - FSTThrowInvalidArgument( - @"Invalid query. Predicate comparisons must include a key path and a constant."); - } - // Fallback cases of unsupported comparison operator. - switch (comparison.predicateOperatorType) { - case NSCustomSelectorPredicateOperatorType: - FSTThrowInvalidArgument(@"Invalid query. Custom predicate filters are not supported."); - break; - default: - FSTThrowInvalidArgument(@"Invalid query. Operator type %lu is not supported.", - (unsigned long)comparison.predicateOperatorType); - } -} - -- (FIRQuery *)queryFilteredUsingCompoundPredicate:(NSPredicate *)predicate { - NSCompoundPredicate *compound = (NSCompoundPredicate *)predicate; - if (compound.compoundPredicateType != NSAndPredicateType || compound.subpredicates.count == 0) { - FSTThrowInvalidArgument(@"Invalid query. Only compound queries using AND are supported."); - } - FIRQuery *query = self; - for (NSPredicate *pred in compound.subpredicates) { - query = [query queryFilteredUsingPredicate:pred]; - } - return query; -} - -- (FIRQuery *)queryFilteredUsingPredicate:(NSPredicate *)predicate { - if ([predicate isKindOfClass:[NSComparisonPredicate class]]) { - return [self queryFilteredUsingComparisonPredicate:predicate]; - } else if ([predicate isKindOfClass:[NSCompoundPredicate class]]) { - return [self queryFilteredUsingCompoundPredicate:predicate]; - } else if ([predicate isKindOfClass:[[NSPredicate - predicateWithBlock:^BOOL(id obj, NSDictionary *bindings) { - return true; - }] class]]) { - FSTThrowInvalidArgument( - @"Invalid query. Block-based predicates are not " - "supported. Please use predicateWithFormat to " - "create predicates instead."); - } else { - FSTThrowInvalidArgument( - @"Invalid query. Expect comparison or compound of " - "comparison predicate. Please use " - "predicateWithFormat to create predicates."); - } -} - -- (FIRQuery *)queryOrderedByField:(NSString *)field { - return - [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field] descending:NO]; -} - -- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)fieldPath { - return [self queryOrderedByFieldPath:fieldPath descending:NO]; -} - -- (FIRQuery *)queryOrderedByField:(NSString *)field descending:(BOOL)descending { - return [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field] - descending:descending]; -} - -- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)fieldPath descending:(BOOL)descending { - [self validateNewOrderByPath:fieldPath.internalValue]; - if (self.query.startAt) { - FSTThrowInvalidUsage( - @"InvalidQueryException", - @"Invalid query. You must not specify a starting point before specifying the order by."); - } - if (self.query.endAt) { - FSTThrowInvalidUsage( - @"InvalidQueryException", - @"Invalid query. You must not specify an ending point before specifying the order by."); - } - FSTSortOrder *sortOrder = - [FSTSortOrder sortOrderWithFieldPath:fieldPath.internalValue ascending:!descending]; - return [FIRQuery referenceWithQuery:[self.query queryByAddingSortOrder:sortOrder] - firestore:self.firestore]; -} - -- (FIRQuery *)queryLimitedTo:(NSInteger)limit { - if (limit <= 0) { - FSTThrowInvalidArgument(@"Invalid Query. Query limit (%ld) is invalid. Limit must be positive.", - (long)limit); - } - return [FIRQuery referenceWithQuery:[self.query queryBySettingLimit:limit] firestore:_firestore]; -} - -- (FIRQuery *)queryStartingAtDocument:(FIRDocumentSnapshot *)snapshot { - FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:YES]; - return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] - firestore:self.firestore]; -} - -- (FIRQuery *)queryStartingAtValues:(NSArray *)fieldValues { - FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:YES]; - return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] - firestore:self.firestore]; -} - -- (FIRQuery *)queryStartingAfterDocument:(FIRDocumentSnapshot *)snapshot { - FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:NO]; - return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] - firestore:self.firestore]; -} - -- (FIRQuery *)queryStartingAfterValues:(NSArray *)fieldValues { - FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:NO]; - return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] - firestore:self.firestore]; -} - -- (FIRQuery *)queryEndingBeforeDocument:(FIRDocumentSnapshot *)snapshot { - FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:YES]; - return - [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; -} - -- (FIRQuery *)queryEndingBeforeValues:(NSArray *)fieldValues { - FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:YES]; - return - [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; -} - -- (FIRQuery *)queryEndingAtDocument:(FIRDocumentSnapshot *)snapshot { - FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:NO]; - return - [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; -} - -- (FIRQuery *)queryEndingAtValues:(NSArray *)fieldValues { - FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:NO]; - return - [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; -} - -#pragma mark - Private Methods - -/** Private helper for all of the queryWhereField: methods. */ -- (FIRQuery *)queryWithFilterOperator:(FSTRelationFilterOperator)filterOperator - field:(NSString *)field - value:(id)value { - return [self queryWithFilterOperator:filterOperator - path:[FIRFieldPath pathWithDotSeparatedString:field].internalValue - value:value]; -} - -- (FIRQuery *)queryWithFilterOperator:(FSTRelationFilterOperator)filterOperator - path:(FSTFieldPath *)fieldPath - value:(id)value { - FSTFieldValue *fieldValue; - if ([fieldPath isKeyFieldPath]) { - if ([value isKindOfClass:[NSString class]]) { - NSString *documentKey = (NSString *)value; - if ([documentKey containsString:@"/"]) { - FSTThrowInvalidArgument( - @"Invalid query. When querying by document ID you must provide " - "a valid document ID, but '%@' contains a '/' character.", - documentKey); - } else if (documentKey.length == 0) { - FSTThrowInvalidArgument( - @"Invalid query. When querying by document ID you must provide " - "a valid document ID, but it was an empty string."); - } - FSTResourcePath *path = [self.query.path pathByAppendingSegment:documentKey]; - fieldValue = [FSTReferenceValue referenceValue:[FSTDocumentKey keyWithPath:path] - databaseID:self.firestore.databaseID]; - } else if ([value isKindOfClass:[FIRDocumentReference class]]) { - FIRDocumentReference *ref = (FIRDocumentReference *)value; - fieldValue = [FSTReferenceValue referenceValue:ref.key databaseID:self.firestore.databaseID]; - } else { - FSTThrowInvalidArgument( - @"Invalid query. When querying by document ID you must provide a " - "valid string or DocumentReference, but it was of type: %@", - NSStringFromClass([value class])); - } - } else { - fieldValue = [self.firestore.dataConverter parsedQueryValue:value]; - } - - id filter; - if ([fieldValue isEqual:[FSTNullValue nullValue]]) { - if (filterOperator != FSTRelationFilterOperatorEqual) { - FSTThrowInvalidUsage(@"InvalidQueryException", - @"Invalid Query. You can only perform equality comparisons on nil / " - "NSNull."); - } - filter = [[FSTNullFilter alloc] initWithField:fieldPath]; - } else if ([fieldValue isEqual:[FSTDoubleValue nanValue]]) { - if (filterOperator != FSTRelationFilterOperatorEqual) { - FSTThrowInvalidUsage(@"InvalidQueryException", - @"Invalid Query. You can only perform equality comparisons on NaN."); - } - filter = [[FSTNanFilter alloc] initWithField:fieldPath]; - } else { - filter = [FSTRelationFilter filterWithField:fieldPath - filterOperator:filterOperator - value:fieldValue]; - [self validateNewRelationFilter:filter]; - } - return [FIRQuery referenceWithQuery:[self.query queryByAddingFilter:filter] - firestore:self.firestore]; -} - -- (void)validateNewRelationFilter:(FSTRelationFilter *)filter { - if ([filter isInequality]) { - FSTFieldPath *existingField = [self.query inequalityFilterField]; - if (existingField && ![existingField isEqual:filter.field]) { - FSTThrowInvalidUsage( - @"InvalidQueryException", - @"Invalid Query. All where filters with an inequality " - "(lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) must be on the same " - "field. But you have inequality filters on '%@' and '%@'", - existingField, filter.field); - } - - FSTFieldPath *firstOrderByField = [self.query firstSortOrderField]; - if (firstOrderByField) { - [self validateOrderByField:firstOrderByField matchesInequalityField:filter.field]; - } - } -} - -- (void)validateNewOrderByPath:(FSTFieldPath *)fieldPath { - if (![self.query firstSortOrderField]) { - // This is the first order by. It must match any inequality. - FSTFieldPath *inequalityField = [self.query inequalityFilterField]; - if (inequalityField) { - [self validateOrderByField:fieldPath matchesInequalityField:inequalityField]; - } - } -} - -- (void)validateOrderByField:(FSTFieldPath *)orderByField - matchesInequalityField:(FSTFieldPath *)inequalityField { - if (!([orderByField isEqual:inequalityField])) { - FSTThrowInvalidUsage( - @"InvalidQueryException", - @"Invalid query. You have a where filter with an " - "inequality (lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) on field '%@' " - "and so you must also use '%@' as your first queryOrderedBy field, but your first " - "queryOrderedBy is currently on field '%@' instead.", - inequalityField, inequalityField, orderByField); - } -} - -/** - * Create a FSTBound from a query given the document. - * - * Note that the FSTBound will always include the key of the document and the position will be - * unambiguous. - * - * Will throw if the document does not contain all fields of the order by of the query. - */ -- (FSTBound *)boundFromSnapshot:(FIRDocumentSnapshot *)snapshot isBefore:(BOOL)isBefore { - if (![snapshot exists]) { - FSTThrowInvalidUsage(@"InvalidQueryException", - @"Invalid query. You are trying to start or end a query using a document " - @"that doesn't exist."); - } - FSTDocument *document = snapshot.internalDocument; - NSMutableArray *components = [NSMutableArray array]; - - // Because people expect to continue/end a query at the exact document provided, we need to - // use the implicit sort order rather than the explicit sort order, because it's guaranteed to - // contain the document key. That way the position becomes unambiguous and the query - // continues/ends exactly at the provided document. Without the key (by using the explicit sort - // orders), multiple documents could match the position, yielding duplicate results. - for (FSTSortOrder *sortOrder in self.query.sortOrders) { - if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) { - [components addObject:[FSTReferenceValue referenceValue:document.key - databaseID:self.firestore.databaseID]]; - } else { - FSTFieldValue *value = [document fieldForPath:sortOrder.field]; - if (value != nil) { - [components addObject:value]; - } else { - FSTThrowInvalidUsage(@"InvalidQueryException", - @"Invalid query. You are trying to start or end a query using a " - "document for which the field '%@' (used as the order by) " - "does not exist.", - sortOrder.field.canonicalString); - } - } - } - return [FSTBound boundWithPosition:components isBefore:isBefore]; -} - -/** Converts a list of field values to an FSTBound. */ -- (FSTBound *)boundFromFieldValues:(NSArray *)fieldValues isBefore:(BOOL)isBefore { - // Use explicit sort order because it has to match the query the user made - NSArray *explicitSortOrders = self.query.explicitSortOrders; - if (fieldValues.count > explicitSortOrders.count) { - FSTThrowInvalidUsage(@"InvalidQueryException", - @"Invalid query. You are trying to start or end a query using more values " - @"than were specified in the order by."); - } - - NSMutableArray *components = [NSMutableArray array]; - [fieldValues enumerateObjectsUsingBlock:^(id rawValue, NSUInteger idx, BOOL *stop) { - FSTSortOrder *sortOrder = explicitSortOrders[idx]; - if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) { - if (![rawValue isKindOfClass:[NSString class]]) { - FSTThrowInvalidUsage(@"InvalidQueryException", - @"Invalid query. Expected a string for the document ID."); - } - NSString *documentID = (NSString *)rawValue; - if ([documentID containsString:@"/"]) { - FSTThrowInvalidUsage(@"InvalidQueryException", - @"Invalid query. Document ID '%@' contains a slash.", documentID); - } - FSTDocumentKey *key = - [FSTDocumentKey keyWithPath:[self.query.path pathByAppendingSegment:documentID]]; - [components - addObject:[FSTReferenceValue referenceValue:key databaseID:self.firestore.databaseID]]; - } else { - FSTFieldValue *fieldValue = [self.firestore.dataConverter parsedQueryValue:rawValue]; - [components addObject:fieldValue]; - } - }]; - - return [FSTBound boundWithPosition:components isBefore:isBefore]; -} - -/** Converts the public API options object to the internal options object. */ -- (FSTListenOptions *)internalOptions:(nullable FIRQueryListenOptions *)options { - return [[FSTListenOptions alloc] - initWithIncludeQueryMetadataChanges:options.includeQueryMetadataChanges - includeDocumentMetadataChanges:options.includeDocumentMetadataChanges - waitForSyncWhenOnline:NO]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuery.mm b/Firestore/Source/API/FIRQuery.mm new file mode 100644 index 0000000..1bbf91e --- /dev/null +++ b/Firestore/Source/API/FIRQuery.mm @@ -0,0 +1,633 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRQuery.h" + +#import "FIRDocumentReference.h" +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/API/FIRFieldPath+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRListenerRegistration+Internal.h" +#import "Firestore/Source/API/FIRQuery+Internal.h" +#import "Firestore/Source/API/FIRQuerySnapshot+Internal.h" +#import "Firestore/Source/API/FIRQuery_Init.h" +#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" +#import "Firestore/Source/API/FSTUserDataConverter.h" +#import "Firestore/Source/Core/FSTEventManager.h" +#import "Firestore/Source/Core/FSTFirestoreClient.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTAsyncQueryListener.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRQueryListenOptions () + +- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges + includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges + NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FIRQueryListenOptions + ++ (instancetype)options { + return [[FIRQueryListenOptions alloc] init]; +} + +- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges + includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges { + if (self = [super init]) { + _includeQueryMetadataChanges = includeQueryMetadataChanges; + _includeDocumentMetadataChanges = includeDocumentMetadataChanges; + } + return self; +} + +- (instancetype)init { + return [self initWithIncludeQueryMetadataChanges:NO includeDocumentMetadataChanges:NO]; +} + +- (instancetype)includeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges { + return [[FIRQueryListenOptions alloc] + initWithIncludeQueryMetadataChanges:includeQueryMetadataChanges + includeDocumentMetadataChanges:_includeDocumentMetadataChanges]; +} + +- (instancetype)includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges { + return [[FIRQueryListenOptions alloc] + initWithIncludeQueryMetadataChanges:_includeQueryMetadataChanges + includeDocumentMetadataChanges:includeDocumentMetadataChanges]; +} + +@end + +@interface FIRQuery () +@property(nonatomic, strong, readonly) FSTQuery *query; +@end + +@implementation FIRQuery (Internal) ++ (instancetype)referenceWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore { + return [[FIRQuery alloc] initWithQuery:query firestore:firestore]; +} +@end + +@implementation FIRQuery + +#pragma mark - Constructor Methods + +- (instancetype)initWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore { + if (self = [super init]) { + _query = query; + _firestore = firestore; + } + return self; +} + +#pragma mark - NSObject Methods + +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + return [self isEqualToQuery:other]; +} + +- (BOOL)isEqualToQuery:(nullable FIRQuery *)query { + if (self == query) return YES; + if (query == nil) return NO; + + return [self.firestore isEqual:query.firestore] && [self.query isEqual:query.query]; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.firestore hash]; + hash = hash * 31u + [self.query hash]; + return hash; +} + +#pragma mark - Public Methods + +- (void)getDocumentsWithCompletion:(void (^)(FIRQuerySnapshot *_Nullable snapshot, + NSError *_Nullable error))completion { + FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES + includeDocumentMetadataChanges:YES + waitForSyncWhenOnline:YES]; + + dispatch_semaphore_t registered = dispatch_semaphore_create(0); + __block id listenerRegistration; + FIRQuerySnapshotBlock listener = ^(FIRQuerySnapshot *snapshot, NSError *error) { + if (error) { + completion(nil, error); + return; + } + + // Remove query first before passing event to user to avoid user actions affecting the + // now stale query. + dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER); + [listenerRegistration remove]; + + completion(snapshot, nil); + }; + + listenerRegistration = [self addSnapshotListenerInternalWithOptions:options listener:listener]; + dispatch_semaphore_signal(registered); +} + +- (id)addSnapshotListener:(FIRQuerySnapshotBlock)listener { + return [self addSnapshotListenerWithOptions:nil listener:listener]; +} + +- (id)addSnapshotListenerWithOptions: + (nullable FIRQueryListenOptions *)options + listener:(FIRQuerySnapshotBlock)listener { + return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options] + listener:listener]; +} + +- (id) +addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions + listener:(FIRQuerySnapshotBlock)listener { + FIRFirestore *firestore = self.firestore; + FSTQuery *query = self.query; + + FSTViewSnapshotHandler snapshotHandler = ^(FSTViewSnapshot *snapshot, NSError *error) { + if (error) { + listener(nil, error); + return; + } + + FIRSnapshotMetadata *metadata = + [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:snapshot.hasPendingWrites + fromCache:snapshot.fromCache]; + + listener([FIRQuerySnapshot snapshotWithFirestore:firestore + originalQuery:query + snapshot:snapshot + metadata:metadata], + nil); + }; + + FSTAsyncQueryListener *asyncListener = + [[FSTAsyncQueryListener alloc] initWithDispatchQueue:self.firestore.client.userDispatchQueue + snapshotHandler:snapshotHandler]; + + FSTQueryListener *internalListener = + [firestore.client listenToQuery:query + options:internalOptions + viewSnapshotHandler:[asyncListener asyncSnapshotHandler]]; + return [[FSTListenerRegistration alloc] initWithClient:self.firestore.client + asyncListener:asyncListener + internalListener:internalListener]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorEqual field:field value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorEqual + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isLessThan:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThan field:field value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isLessThan:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThan + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isLessThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThanOrEqual + field:field + value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isLessThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThanOrEqual + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isGreaterThan:(id)value { + return + [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThan field:field value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isGreaterThan:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThan + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isGreaterThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThanOrEqual + field:field + value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isGreaterThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThanOrEqual + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryFilteredUsingComparisonPredicate:(NSPredicate *)predicate { + NSComparisonPredicate *comparison = (NSComparisonPredicate *)predicate; + if (comparison.comparisonPredicateModifier != NSDirectPredicateModifier) { + FSTThrowInvalidArgument(@"Invalid query. Predicate cannot have an aggregate modifier."); + } + NSString *path; + id value = nil; + if ([comparison.leftExpression expressionType] == NSKeyPathExpressionType && + [comparison.rightExpression expressionType] == NSConstantValueExpressionType) { + path = comparison.leftExpression.keyPath; + value = comparison.rightExpression.constantValue; + switch (comparison.predicateOperatorType) { + case NSEqualToPredicateOperatorType: + return [self queryWhereField:path isEqualTo:value]; + case NSLessThanPredicateOperatorType: + return [self queryWhereField:path isLessThan:value]; + case NSLessThanOrEqualToPredicateOperatorType: + return [self queryWhereField:path isLessThanOrEqualTo:value]; + case NSGreaterThanPredicateOperatorType: + return [self queryWhereField:path isGreaterThan:value]; + case NSGreaterThanOrEqualToPredicateOperatorType: + return [self queryWhereField:path isGreaterThanOrEqualTo:value]; + default:; // Fallback below to throw assertion. + } + } else if ([comparison.leftExpression expressionType] == NSConstantValueExpressionType && + [comparison.rightExpression expressionType] == NSKeyPathExpressionType) { + path = comparison.rightExpression.keyPath; + value = comparison.leftExpression.constantValue; + switch (comparison.predicateOperatorType) { + case NSEqualToPredicateOperatorType: + return [self queryWhereField:path isEqualTo:value]; + case NSLessThanPredicateOperatorType: + return [self queryWhereField:path isGreaterThan:value]; + case NSLessThanOrEqualToPredicateOperatorType: + return [self queryWhereField:path isGreaterThanOrEqualTo:value]; + case NSGreaterThanPredicateOperatorType: + return [self queryWhereField:path isLessThan:value]; + case NSGreaterThanOrEqualToPredicateOperatorType: + return [self queryWhereField:path isLessThanOrEqualTo:value]; + default:; // Fallback below to throw assertion. + } + } else { + FSTThrowInvalidArgument( + @"Invalid query. Predicate comparisons must include a key path and a constant."); + } + // Fallback cases of unsupported comparison operator. + switch (comparison.predicateOperatorType) { + case NSCustomSelectorPredicateOperatorType: + FSTThrowInvalidArgument(@"Invalid query. Custom predicate filters are not supported."); + break; + default: + FSTThrowInvalidArgument(@"Invalid query. Operator type %lu is not supported.", + (unsigned long)comparison.predicateOperatorType); + } +} + +- (FIRQuery *)queryFilteredUsingCompoundPredicate:(NSPredicate *)predicate { + NSCompoundPredicate *compound = (NSCompoundPredicate *)predicate; + if (compound.compoundPredicateType != NSAndPredicateType || compound.subpredicates.count == 0) { + FSTThrowInvalidArgument(@"Invalid query. Only compound queries using AND are supported."); + } + FIRQuery *query = self; + for (NSPredicate *pred in compound.subpredicates) { + query = [query queryFilteredUsingPredicate:pred]; + } + return query; +} + +- (FIRQuery *)queryFilteredUsingPredicate:(NSPredicate *)predicate { + if ([predicate isKindOfClass:[NSComparisonPredicate class]]) { + return [self queryFilteredUsingComparisonPredicate:predicate]; + } else if ([predicate isKindOfClass:[NSCompoundPredicate class]]) { + return [self queryFilteredUsingCompoundPredicate:predicate]; + } else if ([predicate isKindOfClass:[[NSPredicate + predicateWithBlock:^BOOL(id obj, NSDictionary *bindings) { + return true; + }] class]]) { + FSTThrowInvalidArgument( + @"Invalid query. Block-based predicates are not " + "supported. Please use predicateWithFormat to " + "create predicates instead."); + } else { + FSTThrowInvalidArgument( + @"Invalid query. Expect comparison or compound of " + "comparison predicate. Please use " + "predicateWithFormat to create predicates."); + } +} + +- (FIRQuery *)queryOrderedByField:(NSString *)field { + return + [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field] descending:NO]; +} + +- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)fieldPath { + return [self queryOrderedByFieldPath:fieldPath descending:NO]; +} + +- (FIRQuery *)queryOrderedByField:(NSString *)field descending:(BOOL)descending { + return [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field] + descending:descending]; +} + +- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)fieldPath descending:(BOOL)descending { + [self validateNewOrderByPath:fieldPath.internalValue]; + if (self.query.startAt) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid query. You must not specify a starting point before specifying the order by."); + } + if (self.query.endAt) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid query. You must not specify an ending point before specifying the order by."); + } + FSTSortOrder *sortOrder = + [FSTSortOrder sortOrderWithFieldPath:fieldPath.internalValue ascending:!descending]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingSortOrder:sortOrder] + firestore:self.firestore]; +} + +- (FIRQuery *)queryLimitedTo:(NSInteger)limit { + if (limit <= 0) { + FSTThrowInvalidArgument(@"Invalid Query. Query limit (%ld) is invalid. Limit must be positive.", + (long)limit); + } + return [FIRQuery referenceWithQuery:[self.query queryBySettingLimit:limit] firestore:_firestore]; +} + +- (FIRQuery *)queryStartingAtDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:YES]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryStartingAtValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:YES]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryStartingAfterDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:NO]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryStartingAfterValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:NO]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingBeforeDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:YES]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingBeforeValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:YES]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingAtDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:NO]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingAtValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:NO]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +#pragma mark - Private Methods + +/** Private helper for all of the queryWhereField: methods. */ +- (FIRQuery *)queryWithFilterOperator:(FSTRelationFilterOperator)filterOperator + field:(NSString *)field + value:(id)value { + return [self queryWithFilterOperator:filterOperator + path:[FIRFieldPath pathWithDotSeparatedString:field].internalValue + value:value]; +} + +- (FIRQuery *)queryWithFilterOperator:(FSTRelationFilterOperator)filterOperator + path:(FSTFieldPath *)fieldPath + value:(id)value { + FSTFieldValue *fieldValue; + if ([fieldPath isKeyFieldPath]) { + if ([value isKindOfClass:[NSString class]]) { + NSString *documentKey = (NSString *)value; + if ([documentKey containsString:@"/"]) { + FSTThrowInvalidArgument( + @"Invalid query. When querying by document ID you must provide " + "a valid document ID, but '%@' contains a '/' character.", + documentKey); + } else if (documentKey.length == 0) { + FSTThrowInvalidArgument( + @"Invalid query. When querying by document ID you must provide " + "a valid document ID, but it was an empty string."); + } + FSTResourcePath *path = [self.query.path pathByAppendingSegment:documentKey]; + fieldValue = [FSTReferenceValue referenceValue:[FSTDocumentKey keyWithPath:path] + databaseID:self.firestore.databaseID]; + } else if ([value isKindOfClass:[FIRDocumentReference class]]) { + FIRDocumentReference *ref = (FIRDocumentReference *)value; + fieldValue = [FSTReferenceValue referenceValue:ref.key databaseID:self.firestore.databaseID]; + } else { + FSTThrowInvalidArgument( + @"Invalid query. When querying by document ID you must provide a " + "valid string or DocumentReference, but it was of type: %@", + NSStringFromClass([value class])); + } + } else { + fieldValue = [self.firestore.dataConverter parsedQueryValue:value]; + } + + id filter; + if ([fieldValue isEqual:[FSTNullValue nullValue]]) { + if (filterOperator != FSTRelationFilterOperatorEqual) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid Query. You can only perform equality comparisons on nil / " + "NSNull."); + } + filter = [[FSTNullFilter alloc] initWithField:fieldPath]; + } else if ([fieldValue isEqual:[FSTDoubleValue nanValue]]) { + if (filterOperator != FSTRelationFilterOperatorEqual) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid Query. You can only perform equality comparisons on NaN."); + } + filter = [[FSTNanFilter alloc] initWithField:fieldPath]; + } else { + filter = [FSTRelationFilter filterWithField:fieldPath + filterOperator:filterOperator + value:fieldValue]; + [self validateNewRelationFilter:filter]; + } + return [FIRQuery referenceWithQuery:[self.query queryByAddingFilter:filter] + firestore:self.firestore]; +} + +- (void)validateNewRelationFilter:(FSTRelationFilter *)filter { + if ([filter isInequality]) { + FSTFieldPath *existingField = [self.query inequalityFilterField]; + if (existingField && ![existingField isEqual:filter.field]) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid Query. All where filters with an inequality " + "(lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) must be on the same " + "field. But you have inequality filters on '%@' and '%@'", + existingField, filter.field); + } + + FSTFieldPath *firstOrderByField = [self.query firstSortOrderField]; + if (firstOrderByField) { + [self validateOrderByField:firstOrderByField matchesInequalityField:filter.field]; + } + } +} + +- (void)validateNewOrderByPath:(FSTFieldPath *)fieldPath { + if (![self.query firstSortOrderField]) { + // This is the first order by. It must match any inequality. + FSTFieldPath *inequalityField = [self.query inequalityFilterField]; + if (inequalityField) { + [self validateOrderByField:fieldPath matchesInequalityField:inequalityField]; + } + } +} + +- (void)validateOrderByField:(FSTFieldPath *)orderByField + matchesInequalityField:(FSTFieldPath *)inequalityField { + if (!([orderByField isEqual:inequalityField])) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid query. You have a where filter with an " + "inequality (lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) on field '%@' " + "and so you must also use '%@' as your first queryOrderedBy field, but your first " + "queryOrderedBy is currently on field '%@' instead.", + inequalityField, inequalityField, orderByField); + } +} + +/** + * Create a FSTBound from a query given the document. + * + * Note that the FSTBound will always include the key of the document and the position will be + * unambiguous. + * + * Will throw if the document does not contain all fields of the order by of the query. + */ +- (FSTBound *)boundFromSnapshot:(FIRDocumentSnapshot *)snapshot isBefore:(BOOL)isBefore { + if (![snapshot exists]) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. You are trying to start or end a query using a document " + @"that doesn't exist."); + } + FSTDocument *document = snapshot.internalDocument; + NSMutableArray *components = [NSMutableArray array]; + + // Because people expect to continue/end a query at the exact document provided, we need to + // use the implicit sort order rather than the explicit sort order, because it's guaranteed to + // contain the document key. That way the position becomes unambiguous and the query + // continues/ends exactly at the provided document. Without the key (by using the explicit sort + // orders), multiple documents could match the position, yielding duplicate results. + for (FSTSortOrder *sortOrder in self.query.sortOrders) { + if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) { + [components addObject:[FSTReferenceValue referenceValue:document.key + databaseID:self.firestore.databaseID]]; + } else { + FSTFieldValue *value = [document fieldForPath:sortOrder.field]; + if (value != nil) { + [components addObject:value]; + } else { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. You are trying to start or end a query using a " + "document for which the field '%@' (used as the order by) " + "does not exist.", + sortOrder.field.canonicalString); + } + } + } + return [FSTBound boundWithPosition:components isBefore:isBefore]; +} + +/** Converts a list of field values to an FSTBound. */ +- (FSTBound *)boundFromFieldValues:(NSArray *)fieldValues isBefore:(BOOL)isBefore { + // Use explicit sort order because it has to match the query the user made + NSArray *explicitSortOrders = self.query.explicitSortOrders; + if (fieldValues.count > explicitSortOrders.count) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. You are trying to start or end a query using more values " + @"than were specified in the order by."); + } + + NSMutableArray *components = [NSMutableArray array]; + [fieldValues enumerateObjectsUsingBlock:^(id rawValue, NSUInteger idx, BOOL *stop) { + FSTSortOrder *sortOrder = explicitSortOrders[idx]; + if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) { + if (![rawValue isKindOfClass:[NSString class]]) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. Expected a string for the document ID."); + } + NSString *documentID = (NSString *)rawValue; + if ([documentID containsString:@"/"]) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. Document ID '%@' contains a slash.", documentID); + } + FSTDocumentKey *key = + [FSTDocumentKey keyWithPath:[self.query.path pathByAppendingSegment:documentID]]; + [components + addObject:[FSTReferenceValue referenceValue:key databaseID:self.firestore.databaseID]]; + } else { + FSTFieldValue *fieldValue = [self.firestore.dataConverter parsedQueryValue:rawValue]; + [components addObject:fieldValue]; + } + }]; + + return [FSTBound boundWithPosition:components isBefore:isBefore]; +} + +/** Converts the public API options object to the internal options object. */ +- (FSTListenOptions *)internalOptions:(nullable FIRQueryListenOptions *)options { + return [[FSTListenOptions alloc] + initWithIncludeQueryMetadataChanges:options.includeQueryMetadataChanges + includeDocumentMetadataChanges:options.includeDocumentMetadataChanges + waitForSyncWhenOnline:NO]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuerySnapshot.m b/Firestore/Source/API/FIRQuerySnapshot.m deleted file mode 100644 index abee84c..0000000 --- a/Firestore/Source/API/FIRQuerySnapshot.m +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/API/FIRQuerySnapshot+Internal.h" - -#import "FIRFirestore.h" -#import "FIRSnapshotMetadata.h" -#import "Firestore/Source/API/FIRDocumentChange+Internal.h" -#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" -#import "Firestore/Source/API/FIRQuery+Internal.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRQuerySnapshot () - -- (instancetype)initWithFirestore:(FIRFirestore *)firestore - originalQuery:(FSTQuery *)query - snapshot:(FSTViewSnapshot *)snapshot - metadata:(FIRSnapshotMetadata *)metadata; - -@property(nonatomic, strong, readonly) FIRFirestore *firestore; -@property(nonatomic, strong, readonly) FSTQuery *originalQuery; -@property(nonatomic, strong, readonly) FSTViewSnapshot *snapshot; - -@end - -@implementation FIRQuerySnapshot (Internal) - -+ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore - originalQuery:(FSTQuery *)query - snapshot:(FSTViewSnapshot *)snapshot - metadata:(FIRSnapshotMetadata *)metadata { - return [[FIRQuerySnapshot alloc] initWithFirestore:firestore - originalQuery:query - snapshot:snapshot - metadata:metadata]; -} - -@end - -@implementation FIRQuerySnapshot { - // Cached value of the documents property. - NSArray *_documents; - - // Cached value of the documentChanges property. - NSArray *_documentChanges; -} - -- (instancetype)initWithFirestore:(FIRFirestore *)firestore - originalQuery:(FSTQuery *)query - snapshot:(FSTViewSnapshot *)snapshot - metadata:(FIRSnapshotMetadata *)metadata { - if (self = [super init]) { - _firestore = firestore; - _originalQuery = query; - _snapshot = snapshot; - _metadata = metadata; - } - return self; -} - -// NSObject Methods -- (BOOL)isEqual:(nullable id)other { - if (other == self) return YES; - if (![[other class] isEqual:[self class]]) return NO; - - return [self isEqualToSnapshot:other]; -} - -- (BOOL)isEqualToSnapshot:(nullable FIRQuerySnapshot *)snapshot { - if (self == snapshot) return YES; - if (snapshot == nil) return NO; - - return [self.firestore isEqual:snapshot.firestore] && - [self.originalQuery isEqual:snapshot.originalQuery] && - [self.snapshot isEqual:snapshot.snapshot] && [self.metadata isEqual:snapshot.metadata]; -} - -- (NSUInteger)hash { - NSUInteger hash = [self.firestore hash]; - hash = hash * 31u + [self.originalQuery hash]; - hash = hash * 31u + [self.snapshot hash]; - hash = hash * 31u + [self.metadata hash]; - return hash; -} - -@dynamic empty; - -- (FIRQuery *)query { - return [FIRQuery referenceWithQuery:self.originalQuery firestore:self.firestore]; -} - -- (BOOL)isEmpty { - return self.snapshot.documents.isEmpty; -} - -// This property is exposed as an NSInteger instead of an NSUInteger since (as of Xcode 8.1) -// Swift bridges NSUInteger as UInt, and we want to avoid forcing Swift users to cast their ints -// where we can. See cr/146959032 for additional context. -- (NSInteger)count { - return self.snapshot.documents.count; -} - -- (NSArray *)documents { - if (!_documents) { - FSTDocumentSet *documentSet = self.snapshot.documents; - FIRFirestore *firestore = self.firestore; - BOOL fromCache = self.metadata.fromCache; - - NSMutableArray *result = [NSMutableArray array]; - for (FSTDocument *document in documentSet.documentEnumerator) { - [result addObject:[FIRQueryDocumentSnapshot snapshotWithFirestore:firestore - documentKey:document.key - document:document - fromCache:fromCache]]; - } - - _documents = result; - } - return _documents; -} - -- (NSArray *)documentChanges { - if (!_documentChanges) { - _documentChanges = - [FIRDocumentChange documentChangesForSnapshot:self.snapshot firestore:self.firestore]; - } - return _documentChanges; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuerySnapshot.mm b/Firestore/Source/API/FIRQuerySnapshot.mm new file mode 100644 index 0000000..abee84c --- /dev/null +++ b/Firestore/Source/API/FIRQuerySnapshot.mm @@ -0,0 +1,151 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/API/FIRQuerySnapshot+Internal.h" + +#import "FIRFirestore.h" +#import "FIRSnapshotMetadata.h" +#import "Firestore/Source/API/FIRDocumentChange+Internal.h" +#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/API/FIRQuery+Internal.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTViewSnapshot.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRQuerySnapshot () + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + originalQuery:(FSTQuery *)query + snapshot:(FSTViewSnapshot *)snapshot + metadata:(FIRSnapshotMetadata *)metadata; + +@property(nonatomic, strong, readonly) FIRFirestore *firestore; +@property(nonatomic, strong, readonly) FSTQuery *originalQuery; +@property(nonatomic, strong, readonly) FSTViewSnapshot *snapshot; + +@end + +@implementation FIRQuerySnapshot (Internal) + ++ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore + originalQuery:(FSTQuery *)query + snapshot:(FSTViewSnapshot *)snapshot + metadata:(FIRSnapshotMetadata *)metadata { + return [[FIRQuerySnapshot alloc] initWithFirestore:firestore + originalQuery:query + snapshot:snapshot + metadata:metadata]; +} + +@end + +@implementation FIRQuerySnapshot { + // Cached value of the documents property. + NSArray *_documents; + + // Cached value of the documentChanges property. + NSArray *_documentChanges; +} + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + originalQuery:(FSTQuery *)query + snapshot:(FSTViewSnapshot *)snapshot + metadata:(FIRSnapshotMetadata *)metadata { + if (self = [super init]) { + _firestore = firestore; + _originalQuery = query; + _snapshot = snapshot; + _metadata = metadata; + } + return self; +} + +// NSObject Methods +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + return [self isEqualToSnapshot:other]; +} + +- (BOOL)isEqualToSnapshot:(nullable FIRQuerySnapshot *)snapshot { + if (self == snapshot) return YES; + if (snapshot == nil) return NO; + + return [self.firestore isEqual:snapshot.firestore] && + [self.originalQuery isEqual:snapshot.originalQuery] && + [self.snapshot isEqual:snapshot.snapshot] && [self.metadata isEqual:snapshot.metadata]; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.firestore hash]; + hash = hash * 31u + [self.originalQuery hash]; + hash = hash * 31u + [self.snapshot hash]; + hash = hash * 31u + [self.metadata hash]; + return hash; +} + +@dynamic empty; + +- (FIRQuery *)query { + return [FIRQuery referenceWithQuery:self.originalQuery firestore:self.firestore]; +} + +- (BOOL)isEmpty { + return self.snapshot.documents.isEmpty; +} + +// This property is exposed as an NSInteger instead of an NSUInteger since (as of Xcode 8.1) +// Swift bridges NSUInteger as UInt, and we want to avoid forcing Swift users to cast their ints +// where we can. See cr/146959032 for additional context. +- (NSInteger)count { + return self.snapshot.documents.count; +} + +- (NSArray *)documents { + if (!_documents) { + FSTDocumentSet *documentSet = self.snapshot.documents; + FIRFirestore *firestore = self.firestore; + BOOL fromCache = self.metadata.fromCache; + + NSMutableArray *result = [NSMutableArray array]; + for (FSTDocument *document in documentSet.documentEnumerator) { + [result addObject:[FIRQueryDocumentSnapshot snapshotWithFirestore:firestore + documentKey:document.key + document:document + fromCache:fromCache]]; + } + + _documents = result; + } + return _documents; +} + +- (NSArray *)documentChanges { + if (!_documentChanges) { + _documentChanges = + [FIRDocumentChange documentChangesForSnapshot:self.snapshot firestore:self.firestore]; + } + return _documentChanges; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSetOptions.m b/Firestore/Source/API/FIRSetOptions.m deleted file mode 100644 index b41086c..0000000 --- a/Firestore/Source/API/FIRSetOptions.m +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/API/FIRSetOptions+Internal.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FIRSetOptions - -- (instancetype)initWithMerge:(BOOL)merge { - if (self = [super init]) { - _merge = merge; - } - return self; -} - -+ (instancetype)merge { - return [[FIRSetOptions alloc] initWithMerge:YES]; -} - -- (BOOL)isEqual:(id)other { - if (self == other) { - return YES; - } else if (![other isKindOfClass:[FIRSetOptions class]]) { - return NO; - } - - FIRSetOptions *otherOptions = (FIRSetOptions *)other; - return otherOptions.merge == self.merge; -} - -- (NSUInteger)hash { - return self.merge ? 1231 : 1237; -} -@end - -@implementation FIRSetOptions (Internal) - -+ (instancetype)overwrite { - static FIRSetOptions *overwriteInstance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - overwriteInstance = [[FIRSetOptions alloc] initWithMerge:NO]; - }); - return overwriteInstance; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSetOptions.mm b/Firestore/Source/API/FIRSetOptions.mm new file mode 100644 index 0000000..b41086c --- /dev/null +++ b/Firestore/Source/API/FIRSetOptions.mm @@ -0,0 +1,63 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/API/FIRSetOptions+Internal.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRSetOptions + +- (instancetype)initWithMerge:(BOOL)merge { + if (self = [super init]) { + _merge = merge; + } + return self; +} + ++ (instancetype)merge { + return [[FIRSetOptions alloc] initWithMerge:YES]; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } else if (![other isKindOfClass:[FIRSetOptions class]]) { + return NO; + } + + FIRSetOptions *otherOptions = (FIRSetOptions *)other; + return otherOptions.merge == self.merge; +} + +- (NSUInteger)hash { + return self.merge ? 1231 : 1237; +} +@end + +@implementation FIRSetOptions (Internal) + ++ (instancetype)overwrite { + static FIRSetOptions *overwriteInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + overwriteInstance = [[FIRSetOptions alloc] initWithMerge:NO]; + }); + return overwriteInstance; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSnapshotMetadata.m b/Firestore/Source/API/FIRSnapshotMetadata.m deleted file mode 100644 index 27747ce..0000000 --- a/Firestore/Source/API/FIRSnapshotMetadata.m +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "FIRSnapshotMetadata.h" - -#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRSnapshotMetadata () - -- (instancetype)initWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache; - -@end - -@implementation FIRSnapshotMetadata (Internal) - -+ (instancetype)snapshotMetadataWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache { - return [[FIRSnapshotMetadata alloc] initWithPendingWrites:pendingWrites fromCache:fromCache]; -} - -@end - -@implementation FIRSnapshotMetadata - -- (instancetype)initWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache { - if (self = [super init]) { - _pendingWrites = pendingWrites; - _fromCache = fromCache; - } - return self; -} - -// NSObject Methods -- (BOOL)isEqual:(nullable id)other { - if (other == self) return YES; - if (![[other class] isEqual:[self class]]) return NO; - - return [self isEqualToMetadata:other]; -} - -- (BOOL)isEqualToMetadata:(nullable FIRSnapshotMetadata *)metadata { - if (self == metadata) return YES; - if (metadata == nil) return NO; - - return self.pendingWrites == metadata.pendingWrites && self.fromCache == metadata.fromCache; -} - -- (NSUInteger)hash { - NSUInteger hash = self.pendingWrites ? 1 : 0; - hash = hash * 31u + (self.fromCache ? 1 : 0); - return hash; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSnapshotMetadata.mm b/Firestore/Source/API/FIRSnapshotMetadata.mm new file mode 100644 index 0000000..27747ce --- /dev/null +++ b/Firestore/Source/API/FIRSnapshotMetadata.mm @@ -0,0 +1,70 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRSnapshotMetadata.h" + +#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRSnapshotMetadata () + +- (instancetype)initWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache; + +@end + +@implementation FIRSnapshotMetadata (Internal) + ++ (instancetype)snapshotMetadataWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache { + return [[FIRSnapshotMetadata alloc] initWithPendingWrites:pendingWrites fromCache:fromCache]; +} + +@end + +@implementation FIRSnapshotMetadata + +- (instancetype)initWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache { + if (self = [super init]) { + _pendingWrites = pendingWrites; + _fromCache = fromCache; + } + return self; +} + +// NSObject Methods +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + return [self isEqualToMetadata:other]; +} + +- (BOOL)isEqualToMetadata:(nullable FIRSnapshotMetadata *)metadata { + if (self == metadata) return YES; + if (metadata == nil) return NO; + + return self.pendingWrites == metadata.pendingWrites && self.fromCache == metadata.fromCache; +} + +- (NSUInteger)hash { + NSUInteger hash = self.pendingWrites ? 1 : 0; + hash = hash * 31u + (self.fromCache ? 1 : 0); + return hash; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSnapshotOptions.m b/Firestore/Source/API/FIRSnapshotOptions.m deleted file mode 100644 index 72ea3cc..0000000 --- a/Firestore/Source/API/FIRSnapshotOptions.m +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "FIRDocumentSnapshot.h" - -#import "Firestore/Source/API/FIRSnapshotOptions+Internal.h" -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRSnapshotOptions () - -@property(nonatomic) FSTServerTimestampBehavior serverTimestampBehavior; - -@end - -@implementation FIRSnapshotOptions - -- (instancetype)initWithServerTimestampBehavior: - (FSTServerTimestampBehavior)serverTimestampBehavior { - self = [super init]; - - if (self) { - _serverTimestampBehavior = serverTimestampBehavior; - } - - return self; -} - -+ (instancetype)defaultOptions { - static FIRSnapshotOptions *sharedInstance = nil; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - sharedInstance = - [[FIRSnapshotOptions alloc] initWithServerTimestampBehavior:FSTServerTimestampBehaviorNone]; - }); - - return sharedInstance; -} - -+ (instancetype)serverTimestampBehavior:(FIRServerTimestampBehavior)serverTimestampBehavior { - switch (serverTimestampBehavior) { - case FIRServerTimestampBehaviorEstimate: - return [[FIRSnapshotOptions alloc] - initWithServerTimestampBehavior:FSTServerTimestampBehaviorEstimate]; - case FIRServerTimestampBehaviorPrevious: - return [[FIRSnapshotOptions alloc] - initWithServerTimestampBehavior:FSTServerTimestampBehaviorPrevious]; - case FIRServerTimestampBehaviorNone: - return [FIRSnapshotOptions defaultOptions]; - default: - FSTFail(@"Encountered unknown server timestamp behavior: %d", (int)serverTimestampBehavior); - } -} - -@end - -NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/Firestore/Source/API/FIRSnapshotOptions.mm b/Firestore/Source/API/FIRSnapshotOptions.mm new file mode 100644 index 0000000..72ea3cc --- /dev/null +++ b/Firestore/Source/API/FIRSnapshotOptions.mm @@ -0,0 +1,72 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRDocumentSnapshot.h" + +#import "Firestore/Source/API/FIRSnapshotOptions+Internal.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRSnapshotOptions () + +@property(nonatomic) FSTServerTimestampBehavior serverTimestampBehavior; + +@end + +@implementation FIRSnapshotOptions + +- (instancetype)initWithServerTimestampBehavior: + (FSTServerTimestampBehavior)serverTimestampBehavior { + self = [super init]; + + if (self) { + _serverTimestampBehavior = serverTimestampBehavior; + } + + return self; +} + ++ (instancetype)defaultOptions { + static FIRSnapshotOptions *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = + [[FIRSnapshotOptions alloc] initWithServerTimestampBehavior:FSTServerTimestampBehaviorNone]; + }); + + return sharedInstance; +} + ++ (instancetype)serverTimestampBehavior:(FIRServerTimestampBehavior)serverTimestampBehavior { + switch (serverTimestampBehavior) { + case FIRServerTimestampBehaviorEstimate: + return [[FIRSnapshotOptions alloc] + initWithServerTimestampBehavior:FSTServerTimestampBehaviorEstimate]; + case FIRServerTimestampBehaviorPrevious: + return [[FIRSnapshotOptions alloc] + initWithServerTimestampBehavior:FSTServerTimestampBehaviorPrevious]; + case FIRServerTimestampBehaviorNone: + return [FIRSnapshotOptions defaultOptions]; + default: + FSTFail(@"Encountered unknown server timestamp behavior: %d", (int)serverTimestampBehavior); + } +} + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/Firestore/Source/API/FIRTransaction.m b/Firestore/Source/API/FIRTransaction.m deleted file mode 100644 index 5edff19..0000000 --- a/Firestore/Source/API/FIRTransaction.m +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/API/FIRTransaction+Internal.h" - -#import "Firestore/Source/API/FIRDocumentReference+Internal.h" -#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRSetOptions+Internal.h" -#import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Core/FSTTransaction.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FIRTransaction - -@interface FIRTransaction () - -- (instancetype)initWithTransaction:(FSTTransaction *)transaction - firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; - -@property(nonatomic, strong, readonly) FSTTransaction *internalTransaction; -@property(nonatomic, strong, readonly) FIRFirestore *firestore; -@end - -@implementation FIRTransaction (Internal) - -+ (instancetype)transactionWithFSTTransaction:(FSTTransaction *)transaction - firestore:(FIRFirestore *)firestore { - return [[FIRTransaction alloc] initWithTransaction:transaction firestore:firestore]; -} - -@end - -@implementation FIRTransaction - -- (instancetype)initWithTransaction:(FSTTransaction *)transaction - firestore:(FIRFirestore *)firestore { - self = [super init]; - if (self) { - _internalTransaction = transaction; - _firestore = firestore; - } - return self; -} - -- (FIRTransaction *)setData:(NSDictionary *)data - forDocument:(FIRDocumentReference *)document { - return [self setData:data forDocument:document options:[FIRSetOptions overwrite]]; -} - -- (FIRTransaction *)setData:(NSDictionary *)data - forDocument:(FIRDocumentReference *)document - options:(FIRSetOptions *)options { - [self validateReference:document]; - FSTParsedSetData *parsed = options.isMerge ? [self.firestore.dataConverter parsedMergeData:data] - : [self.firestore.dataConverter parsedSetData:data]; - [self.internalTransaction setData:parsed forDocument:document.key]; - return self; -} - -- (FIRTransaction *)updateData:(NSDictionary *)fields - forDocument:(FIRDocumentReference *)document { - [self validateReference:document]; - FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; - [self.internalTransaction updateData:parsed forDocument:document.key]; - return self; -} - -- (FIRTransaction *)deleteDocument:(FIRDocumentReference *)document { - [self validateReference:document]; - [self.internalTransaction deleteDocument:document.key]; - return self; -} - -- (void)getDocument:(FIRDocumentReference *)document - completion:(void (^)(FIRDocumentSnapshot *_Nullable document, - NSError *_Nullable error))completion { - [self validateReference:document]; - [self.internalTransaction - lookupDocumentsForKeys:@[ document.key ] - completion:^(NSArray *_Nullable documents, - NSError *_Nullable error) { - if (error) { - completion(nil, error); - return; - } - FSTAssert(documents.count == 1, - @"Mismatch in docs returned from document lookup."); - FSTMaybeDocument *internalDoc = documents.firstObject; - if ([internalDoc isKindOfClass:[FSTDeletedDocument class]]) { - completion(nil, nil); - return; - } - FIRDocumentSnapshot *doc = - [FIRDocumentSnapshot snapshotWithFirestore:self.firestore - documentKey:internalDoc.key - document:(FSTDocument *)internalDoc - fromCache:NO]; - completion(doc, nil); - }]; -} - -- (FIRDocumentSnapshot *_Nullable)getDocument:(FIRDocumentReference *)document - error:(NSError *__autoreleasing *)error { - [self validateReference:document]; - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - __block FIRDocumentSnapshot *result; - // We have to explicitly assign the innerError into a local to cause it to retain correctly. - __block NSError *outerError = nil; - [self getDocument:document - completion:^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable innerError) { - result = snapshot; - outerError = innerError; - dispatch_semaphore_signal(semaphore); - }]; - dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); - if (error) { - *error = outerError; - } - return result; -} - -- (void)validateReference:(FIRDocumentReference *)reference { - if (reference.firestore != self.firestore) { - FSTThrowInvalidArgument(@"Provided document reference is from a different Firestore instance."); - } -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRTransaction.mm b/Firestore/Source/API/FIRTransaction.mm new file mode 100644 index 0000000..5edff19 --- /dev/null +++ b/Firestore/Source/API/FIRTransaction.mm @@ -0,0 +1,148 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/API/FIRTransaction+Internal.h" + +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRSetOptions+Internal.h" +#import "Firestore/Source/API/FSTUserDataConverter.h" +#import "Firestore/Source/Core/FSTTransaction.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FIRTransaction + +@interface FIRTransaction () + +- (instancetype)initWithTransaction:(FSTTransaction *)transaction + firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FSTTransaction *internalTransaction; +@property(nonatomic, strong, readonly) FIRFirestore *firestore; +@end + +@implementation FIRTransaction (Internal) + ++ (instancetype)transactionWithFSTTransaction:(FSTTransaction *)transaction + firestore:(FIRFirestore *)firestore { + return [[FIRTransaction alloc] initWithTransaction:transaction firestore:firestore]; +} + +@end + +@implementation FIRTransaction + +- (instancetype)initWithTransaction:(FSTTransaction *)transaction + firestore:(FIRFirestore *)firestore { + self = [super init]; + if (self) { + _internalTransaction = transaction; + _firestore = firestore; + } + return self; +} + +- (FIRTransaction *)setData:(NSDictionary *)data + forDocument:(FIRDocumentReference *)document { + return [self setData:data forDocument:document options:[FIRSetOptions overwrite]]; +} + +- (FIRTransaction *)setData:(NSDictionary *)data + forDocument:(FIRDocumentReference *)document + options:(FIRSetOptions *)options { + [self validateReference:document]; + FSTParsedSetData *parsed = options.isMerge ? [self.firestore.dataConverter parsedMergeData:data] + : [self.firestore.dataConverter parsedSetData:data]; + [self.internalTransaction setData:parsed forDocument:document.key]; + return self; +} + +- (FIRTransaction *)updateData:(NSDictionary *)fields + forDocument:(FIRDocumentReference *)document { + [self validateReference:document]; + FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; + [self.internalTransaction updateData:parsed forDocument:document.key]; + return self; +} + +- (FIRTransaction *)deleteDocument:(FIRDocumentReference *)document { + [self validateReference:document]; + [self.internalTransaction deleteDocument:document.key]; + return self; +} + +- (void)getDocument:(FIRDocumentReference *)document + completion:(void (^)(FIRDocumentSnapshot *_Nullable document, + NSError *_Nullable error))completion { + [self validateReference:document]; + [self.internalTransaction + lookupDocumentsForKeys:@[ document.key ] + completion:^(NSArray *_Nullable documents, + NSError *_Nullable error) { + if (error) { + completion(nil, error); + return; + } + FSTAssert(documents.count == 1, + @"Mismatch in docs returned from document lookup."); + FSTMaybeDocument *internalDoc = documents.firstObject; + if ([internalDoc isKindOfClass:[FSTDeletedDocument class]]) { + completion(nil, nil); + return; + } + FIRDocumentSnapshot *doc = + [FIRDocumentSnapshot snapshotWithFirestore:self.firestore + documentKey:internalDoc.key + document:(FSTDocument *)internalDoc + fromCache:NO]; + completion(doc, nil); + }]; +} + +- (FIRDocumentSnapshot *_Nullable)getDocument:(FIRDocumentReference *)document + error:(NSError *__autoreleasing *)error { + [self validateReference:document]; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block FIRDocumentSnapshot *result; + // We have to explicitly assign the innerError into a local to cause it to retain correctly. + __block NSError *outerError = nil; + [self getDocument:document + completion:^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable innerError) { + result = snapshot; + outerError = innerError; + dispatch_semaphore_signal(semaphore); + }]; + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + if (error) { + *error = outerError; + } + return result; +} + +- (void)validateReference:(FIRDocumentReference *)reference { + if (reference.firestore != self.firestore) { + FSTThrowInvalidArgument(@"Provided document reference is from a different Firestore instance."); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRWriteBatch.m b/Firestore/Source/API/FIRWriteBatch.m deleted file mode 100644 index b1cfa09..0000000 --- a/Firestore/Source/API/FIRWriteBatch.m +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/API/FIRWriteBatch+Internal.h" - -#import "Firestore/Source/API/FIRDocumentReference+Internal.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRSetOptions+Internal.h" -#import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Core/FSTFirestoreClient.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FIRWriteBatch - -@interface FIRWriteBatch () - -- (instancetype)initWithFirestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; - -@property(nonatomic, strong, readonly) FIRFirestore *firestore; -@property(nonatomic, strong, readonly) NSMutableArray *mutations; -@property(nonatomic, assign) BOOL committed; - -@end - -@implementation FIRWriteBatch (Internal) - -+ (instancetype)writeBatchWithFirestore:(FIRFirestore *)firestore { - return [[FIRWriteBatch alloc] initWithFirestore:firestore]; -} - -@end - -@implementation FIRWriteBatch - -- (instancetype)initWithFirestore:(FIRFirestore *)firestore { - self = [super init]; - if (self) { - _firestore = firestore; - _mutations = [NSMutableArray array]; - } - return self; -} - -- (FIRWriteBatch *)setData:(NSDictionary *)data - forDocument:(FIRDocumentReference *)document { - return [self setData:data forDocument:document options:[FIRSetOptions overwrite]]; -} - -- (FIRWriteBatch *)setData:(NSDictionary *)data - forDocument:(FIRDocumentReference *)document - options:(FIRSetOptions *)options { - [self verifyNotCommitted]; - [self validateReference:document]; - FSTParsedSetData *parsed = options.isMerge ? [self.firestore.dataConverter parsedMergeData:data] - : [self.firestore.dataConverter parsedSetData:data]; - [self.mutations addObjectsFromArray:[parsed mutationsWithKey:document.key - precondition:[FSTPrecondition none]]]; - return self; -} - -- (FIRWriteBatch *)updateData:(NSDictionary *)fields - forDocument:(FIRDocumentReference *)document { - [self verifyNotCommitted]; - [self validateReference:document]; - FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; - [self.mutations - addObjectsFromArray:[parsed mutationsWithKey:document.key - precondition:[FSTPrecondition preconditionWithExists:YES]]]; - return self; -} - -- (FIRWriteBatch *)deleteDocument:(FIRDocumentReference *)document { - [self verifyNotCommitted]; - [self validateReference:document]; - [self.mutations addObject:[[FSTDeleteMutation alloc] initWithKey:document.key - precondition:[FSTPrecondition none]]]; - return self; -} - -- (void)commit { - [self commitWithCompletion:nil]; -} - -- (void)commitWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { - [self verifyNotCommitted]; - self.committed = TRUE; - [self.firestore.client writeMutations:self.mutations completion:completion]; -} - -- (void)verifyNotCommitted { - if (self.committed) { - FSTThrowInvalidUsage(@"FIRIllegalStateException", - @"A write batch can no longer be used after commit has been called."); - } -} - -- (void)validateReference:(FIRDocumentReference *)reference { - if (reference.firestore != self.firestore) { - FSTThrowInvalidArgument(@"Provided document reference is from a different Firestore instance."); - } -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRWriteBatch.mm b/Firestore/Source/API/FIRWriteBatch.mm new file mode 100644 index 0000000..b1cfa09 --- /dev/null +++ b/Firestore/Source/API/FIRWriteBatch.mm @@ -0,0 +1,121 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/API/FIRWriteBatch+Internal.h" + +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRSetOptions+Internal.h" +#import "Firestore/Source/API/FSTUserDataConverter.h" +#import "Firestore/Source/Core/FSTFirestoreClient.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FIRWriteBatch + +@interface FIRWriteBatch () + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FIRFirestore *firestore; +@property(nonatomic, strong, readonly) NSMutableArray *mutations; +@property(nonatomic, assign) BOOL committed; + +@end + +@implementation FIRWriteBatch (Internal) + ++ (instancetype)writeBatchWithFirestore:(FIRFirestore *)firestore { + return [[FIRWriteBatch alloc] initWithFirestore:firestore]; +} + +@end + +@implementation FIRWriteBatch + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore { + self = [super init]; + if (self) { + _firestore = firestore; + _mutations = [NSMutableArray array]; + } + return self; +} + +- (FIRWriteBatch *)setData:(NSDictionary *)data + forDocument:(FIRDocumentReference *)document { + return [self setData:data forDocument:document options:[FIRSetOptions overwrite]]; +} + +- (FIRWriteBatch *)setData:(NSDictionary *)data + forDocument:(FIRDocumentReference *)document + options:(FIRSetOptions *)options { + [self verifyNotCommitted]; + [self validateReference:document]; + FSTParsedSetData *parsed = options.isMerge ? [self.firestore.dataConverter parsedMergeData:data] + : [self.firestore.dataConverter parsedSetData:data]; + [self.mutations addObjectsFromArray:[parsed mutationsWithKey:document.key + precondition:[FSTPrecondition none]]]; + return self; +} + +- (FIRWriteBatch *)updateData:(NSDictionary *)fields + forDocument:(FIRDocumentReference *)document { + [self verifyNotCommitted]; + [self validateReference:document]; + FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; + [self.mutations + addObjectsFromArray:[parsed mutationsWithKey:document.key + precondition:[FSTPrecondition preconditionWithExists:YES]]]; + return self; +} + +- (FIRWriteBatch *)deleteDocument:(FIRDocumentReference *)document { + [self verifyNotCommitted]; + [self validateReference:document]; + [self.mutations addObject:[[FSTDeleteMutation alloc] initWithKey:document.key + precondition:[FSTPrecondition none]]]; + return self; +} + +- (void)commit { + [self commitWithCompletion:nil]; +} + +- (void)commitWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { + [self verifyNotCommitted]; + self.committed = TRUE; + [self.firestore.client writeMutations:self.mutations completion:completion]; +} + +- (void)verifyNotCommitted { + if (self.committed) { + FSTThrowInvalidUsage(@"FIRIllegalStateException", + @"A write batch can no longer be used after commit has been called."); + } +} + +- (void)validateReference:(FIRDocumentReference *)reference { + if (reference.firestore != self.firestore) { + FSTThrowInvalidArgument(@"Provided document reference is from a different Firestore instance."); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FSTUserDataConverter.m b/Firestore/Source/API/FSTUserDataConverter.m deleted file mode 100644 index 414aadb..0000000 --- a/Firestore/Source/API/FSTUserDataConverter.m +++ /dev/null @@ -1,598 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/API/FSTUserDataConverter.h" - -#import "FIRGeoPoint.h" -#import "Firestore/Source/API/FIRDocumentReference+Internal.h" -#import "Firestore/Source/API/FIRFieldPath+Internal.h" -#import "Firestore/Source/API/FIRFieldValue+Internal.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRSetOptions+Internal.h" -#import "Firestore/Source/Core/FSTTimestamp.h" -#import "Firestore/Source/Model/FSTDatabaseID.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -static NSString *const RESERVED_FIELD_DESIGNATOR = @"__"; - -#pragma mark - FSTParsedSetData - -@implementation FSTParsedSetData -- (instancetype)initWithData:(FSTObjectValue *)data - fieldMask:(nullable FSTFieldMask *)fieldMask - fieldTransforms:(NSArray *)fieldTransforms { - self = [super init]; - if (self) { - _data = data; - _fieldMask = fieldMask; - _fieldTransforms = fieldTransforms; - } - return self; -} - -- (NSArray *)mutationsWithKey:(FSTDocumentKey *)key - precondition:(FSTPrecondition *)precondition { - NSMutableArray *mutations = [NSMutableArray array]; - if (self.fieldMask) { - [mutations addObject:[[FSTPatchMutation alloc] initWithKey:key - fieldMask:self.fieldMask - value:self.data - precondition:precondition]]; - } else { - [mutations addObject:[[FSTSetMutation alloc] initWithKey:key - value:self.data - precondition:precondition]]; - } - if (self.fieldTransforms.count > 0) { - [mutations addObject:[[FSTTransformMutation alloc] initWithKey:key - fieldTransforms:self.fieldTransforms]]; - } - return mutations; -} - -@end - -#pragma mark - FSTParsedUpdateData - -@implementation FSTParsedUpdateData -- (instancetype)initWithData:(FSTObjectValue *)data - fieldMask:(FSTFieldMask *)fieldMask - fieldTransforms:(NSArray *)fieldTransforms { - self = [super init]; - if (self) { - _data = data; - _fieldMask = fieldMask; - _fieldTransforms = fieldTransforms; - } - return self; -} - -- (NSArray *)mutationsWithKey:(FSTDocumentKey *)key - precondition:(FSTPrecondition *)precondition { - NSMutableArray *mutations = [NSMutableArray array]; - [mutations addObject:[[FSTPatchMutation alloc] initWithKey:key - fieldMask:self.fieldMask - value:self.data - precondition:precondition]]; - if (self.fieldTransforms.count > 0) { - [mutations addObject:[[FSTTransformMutation alloc] initWithKey:key - fieldTransforms:self.fieldTransforms]]; - } - return mutations; -} - -@end - -/** - * Represents what type of API method provided the data being parsed; useful for determining which - * error conditions apply during parsing and providing better error messages. - */ -typedef NS_ENUM(NSInteger, FSTUserDataSource) { - FSTUserDataSourceSet, - FSTUserDataSourceMergeSet, - FSTUserDataSourceUpdate, - FSTUserDataSourceQueryValue, // from a where clause or cursor bound. -}; - -#pragma mark - FSTParseContext - -/** - * A "context" object passed around while parsing user data. - */ -@interface FSTParseContext : NSObject -/** The current path being parsed. */ -// TODO(b/34871131): path should never be nil, but we don't support array paths right now. -@property(nonatomic, strong, readonly, nullable) FSTFieldPath *path; - -/** Whether or not this context corresponds to an element of an array. */ -@property(nonatomic, assign, readonly, getter=isArrayElement) BOOL arrayElement; - -/** - * What type of API method provided the data being parsed; useful for determining which error - * conditions apply during parsing and providing better error messages. - */ -@property(nonatomic, assign) FSTUserDataSource dataSource; -@property(nonatomic, strong, readonly) NSMutableArray *fieldTransforms; -@property(nonatomic, strong, readonly) NSMutableArray *fieldMask; - -- (instancetype)init NS_UNAVAILABLE; -/** - * Initializes a FSTParseContext with the given source and path. - * - * @param dataSource Indicates what kind of API method this data came from. - * @param path A path within the object being parsed. This could be an empty path (in which case - * the context represents the root of the data being parsed), or a nonempty path (indicating the - * context represents a nested location within the data). - * - * TODO(b/34871131): We don't support array paths right now, so path can be nil to indicate - * the context represents any location within an array (in which case certain features will not work - * and errors will be somewhat compromised). - */ -- (instancetype)initWithSource:(FSTUserDataSource)dataSource - path:(nullable FSTFieldPath *)path - arrayElement:(BOOL)arrayElement - fieldTransforms:(NSMutableArray *)fieldTransforms - fieldMask:(NSMutableArray *)fieldMask - NS_DESIGNATED_INITIALIZER; - -// Helpers to get a FSTParseContext for a child field. -- (instancetype)contextForField:(NSString *)fieldName; -- (instancetype)contextForFieldPath:(FSTFieldPath *)fieldPath; -- (instancetype)contextForArrayIndex:(NSUInteger)index; - -/** Returns true for the non-query parse contexts (Set, MergeSet and Update) */ -- (BOOL)isWrite; -@end - -@implementation FSTParseContext - -+ (instancetype)contextWithSource:(FSTUserDataSource)dataSource path:(nullable FSTFieldPath *)path { - FSTParseContext *context = [[FSTParseContext alloc] initWithSource:dataSource - path:path - arrayElement:NO - fieldTransforms:[NSMutableArray array] - fieldMask:[NSMutableArray array]]; - [context validatePath]; - return context; -} - -- (instancetype)initWithSource:(FSTUserDataSource)dataSource - path:(nullable FSTFieldPath *)path - arrayElement:(BOOL)arrayElement - fieldTransforms:(NSMutableArray *)fieldTransforms - fieldMask:(NSMutableArray *)fieldMask { - if (self = [super init]) { - _dataSource = dataSource; - _path = path; - _arrayElement = arrayElement; - _fieldTransforms = fieldTransforms; - _fieldMask = fieldMask; - } - return self; -} - -- (instancetype)contextForField:(NSString *)fieldName { - FSTParseContext *context = - [[FSTParseContext alloc] initWithSource:self.dataSource - path:[self.path pathByAppendingSegment:fieldName] - arrayElement:NO - fieldTransforms:self.fieldTransforms - fieldMask:self.fieldMask]; - [context validatePathSegment:fieldName]; - return context; -} - -- (instancetype)contextForFieldPath:(FSTFieldPath *)fieldPath { - FSTParseContext *context = - [[FSTParseContext alloc] initWithSource:self.dataSource - path:[self.path pathByAppendingPath:fieldPath] - arrayElement:NO - fieldTransforms:self.fieldTransforms - fieldMask:self.fieldMask]; - [context validatePath]; - return context; -} - -- (instancetype)contextForArrayIndex:(NSUInteger)index { - // TODO(b/34871131): We don't support array paths right now; so make path nil. - return [[FSTParseContext alloc] initWithSource:self.dataSource - path:nil - arrayElement:YES - fieldTransforms:self.fieldTransforms - fieldMask:self.fieldMask]; -} - -/** - * Returns a string that can be appended to error messages indicating what field caused the error. - */ -- (NSString *)fieldDescription { - // TODO(b/34871131): Remove nil check once we have proper paths for fields within arrays. - if (!self.path || self.path.empty) { - return @""; - } else { - return [NSString stringWithFormat:@" (found in field %@)", self.path]; - } -} - -- (BOOL)isWrite { - switch (self.dataSource) { - case FSTUserDataSourceSet: // Falls through. - case FSTUserDataSourceMergeSet: // Falls through. - case FSTUserDataSourceUpdate: - return YES; - case FSTUserDataSourceQueryValue: - return NO; - default: - FSTThrowInvalidArgument(@"Unexpected case for FSTUserDataSource: %d", self.dataSource); - } -} - -- (void)validatePath { - // TODO(b/34871131): Remove nil check once we have proper paths for fields within arrays. - if (self.path == nil) { - return; - } - for (int i = 0; i < self.path.length; i++) { - [self validatePathSegment:[self.path segmentAtIndex:i]]; - } -} - -- (void)validatePathSegment:(NSString *)segment { - if ([self isWrite] && [segment hasPrefix:RESERVED_FIELD_DESIGNATOR] && - [segment hasSuffix:RESERVED_FIELD_DESIGNATOR]) { - FSTThrowInvalidArgument(@"Document fields cannot begin and end with %@%@", - RESERVED_FIELD_DESIGNATOR, [self fieldDescription]); - } -} - -@end - -#pragma mark - FSTDocumentKeyReference - -@implementation FSTDocumentKeyReference - -- (instancetype)initWithKey:(FSTDocumentKey *)key databaseID:(FSTDatabaseID *)databaseID { - self = [super init]; - if (self) { - _key = key; - _databaseID = databaseID; - } - return self; -} - -@end - -#pragma mark - FSTUserDataConverter - -@interface FSTUserDataConverter () -@property(strong, nonatomic, readonly) FSTDatabaseID *databaseID; -@property(strong, nonatomic, readonly) FSTPreConverterBlock preConverter; -@end - -@implementation FSTUserDataConverter - -- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID - preConverter:(FSTPreConverterBlock)preConverter { - self = [super init]; - if (self) { - _databaseID = databaseID; - _preConverter = preConverter; - } - return self; -} - -- (FSTParsedSetData *)parsedMergeData:(id)input { - // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust - // Obj-C to verify the type for us. - if (![input isKindOfClass:[NSDictionary class]]) { - FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary."); - } - - FSTParseContext *context = - [FSTParseContext contextWithSource:FSTUserDataSourceMergeSet path:[FSTFieldPath emptyPath]]; - FSTObjectValue *updateData = (FSTObjectValue *)[self parseData:input context:context]; - - return - [[FSTParsedSetData alloc] initWithData:updateData - fieldMask:[[FSTFieldMask alloc] initWithFields:context.fieldMask] - fieldTransforms:context.fieldTransforms]; -} - -- (FSTParsedSetData *)parsedSetData:(id)input { - // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust - // Obj-C to verify the type for us. - if (![input isKindOfClass:[NSDictionary class]]) { - FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary."); - } - - FSTParseContext *context = - [FSTParseContext contextWithSource:FSTUserDataSourceSet path:[FSTFieldPath emptyPath]]; - FSTObjectValue *updateData = (FSTObjectValue *)[self parseData:input context:context]; - - return [[FSTParsedSetData alloc] initWithData:updateData - fieldMask:nil - fieldTransforms:context.fieldTransforms]; -} - -- (FSTParsedUpdateData *)parsedUpdateData:(id)input { - // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust - // Obj-C to verify the type for us. - if (![input isKindOfClass:[NSDictionary class]]) { - FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary."); - } - - NSDictionary *dict = input; - - NSMutableArray *fieldMaskPaths = [NSMutableArray array]; - __block FSTObjectValue *updateData = [FSTObjectValue objectValue]; - - FSTParseContext *context = - [FSTParseContext contextWithSource:FSTUserDataSourceUpdate path:[FSTFieldPath emptyPath]]; - [dict enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { - FSTFieldPath *path; - - if ([key isKindOfClass:[NSString class]]) { - path = [FIRFieldPath pathWithDotSeparatedString:key].internalValue; - } else if ([key isKindOfClass:[FIRFieldPath class]]) { - path = ((FIRFieldPath *)key).internalValue; - } else { - FSTThrowInvalidArgument( - @"Dictionary keys in updateData: must be NSStrings or FIRFieldPaths."); - } - - value = self.preConverter(value); - if ([value isKindOfClass:[FSTDeleteFieldValue class]]) { - // Add it to the field mask, but don't add anything to updateData. - [fieldMaskPaths addObject:path]; - } else { - FSTFieldValue *_Nullable parsedValue = - [self parseData:value context:[context contextForFieldPath:path]]; - if (parsedValue) { - [fieldMaskPaths addObject:path]; - updateData = [updateData objectBySettingValue:parsedValue forPath:path]; - } - } - }]; - - FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:fieldMaskPaths]; - return [[FSTParsedUpdateData alloc] initWithData:updateData - fieldMask:mask - fieldTransforms:context.fieldTransforms]; -} - -- (FSTFieldValue *)parsedQueryValue:(id)input { - FSTParseContext *context = - [FSTParseContext contextWithSource:FSTUserDataSourceQueryValue path:[FSTFieldPath emptyPath]]; - FSTFieldValue *_Nullable parsed = [self parseData:input context:context]; - FSTAssert(parsed, @"Parsed data should not be nil."); - FSTAssert(context.fieldTransforms.count == 0, @"Field transforms should have been disallowed."); - return parsed; -} - -/** - * Internal helper for parsing user data. - * - * @param input Data to be parsed. - * @param context A context object representing the current path being parsed, the source of the - * data being parsed, etc. - * - * @return The parsed value, or nil if the value was a FieldValue sentinel that should not be - * included in the resulting parsed data. - */ -- (nullable FSTFieldValue *)parseData:(id)input context:(FSTParseContext *)context { - input = self.preConverter(input); - if ([input isKindOfClass:[NSArray class]]) { - // TODO(b/34871131): Include the path containing the array in the error message. - if (context.isArrayElement) { - FSTThrowInvalidArgument(@"Nested arrays are not supported"); - } - NSArray *array = input; - NSMutableArray *result = [NSMutableArray arrayWithCapacity:array.count]; - [array enumerateObjectsUsingBlock:^(id entry, NSUInteger idx, BOOL *stop) { - FSTFieldValue *_Nullable parsedEntry = - [self parseData:entry context:[context contextForArrayIndex:idx]]; - if (!parsedEntry) { - // Just include nulls in the array for fields being replaced with a sentinel. - parsedEntry = [FSTNullValue nullValue]; - } - [result addObject:parsedEntry]; - }]; - // If context.path is nil we are already inside an array and we don't support field mask paths - // more granular than the top-level array. - if (context.path) { - [context.fieldMask addObject:context.path]; - } - return [[FSTArrayValue alloc] initWithValueNoCopy:result]; - - } else if ([input isKindOfClass:[NSDictionary class]]) { - NSDictionary *dict = input; - NSMutableDictionary *result = - [NSMutableDictionary dictionaryWithCapacity:dict.count]; - [dict enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { - FSTFieldValue *_Nullable parsedValue = - [self parseData:value context:[context contextForField:key]]; - if (parsedValue) { - result[key] = parsedValue; - } - }]; - return [[FSTObjectValue alloc] initWithDictionary:result]; - - } else { - // If context.path is null, we are inside an array and we should have already added the root of - // the array to the field mask. - if (context.path) { - [context.fieldMask addObject:context.path]; - } - return [self parseScalarValue:input context:context]; - } -} - -/** - * Helper to parse a scalar value (i.e. not an NSDictionary or NSArray). - * - * Note that it handles all NSNumber values that are encodable as int64_t or doubles - * (depending on the underlying type of the NSNumber). Unsigned integer values are handled though - * any value outside what is representable by int64_t (a signed 64-bit value) will throw an - * exception. - * - * @return The parsed value, or nil if the value was a FieldValue sentinel that should not be - * included in the resulting parsed data. - */ -- (nullable FSTFieldValue *)parseScalarValue:(nullable id)input context:(FSTParseContext *)context { - if (!input || [input isMemberOfClass:[NSNull class]]) { - return [FSTNullValue nullValue]; - - } else if ([input isKindOfClass:[NSNumber class]]) { - // Recover the underlying type of the number, using the method described here: - // http://stackoverflow.com/questions/2518761/get-type-of-nsnumber - const char *cType = [input objCType]; - - // Type Encoding values taken from - // https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/ - // Articles/ocrtTypeEncodings.html - switch (cType[0]) { - case 'q': - return [FSTIntegerValue integerValue:[input longLongValue]]; - - case 'i': // Falls through. - case 's': // Falls through. - case 'l': // Falls through. - case 'I': // Falls through. - case 'S': - // Coerce integer values that aren't long long. Allow unsigned integer types that are - // guaranteed small enough to skip a length check. - return [FSTIntegerValue integerValue:[input longLongValue]]; - - case 'L': // Falls through. - case 'Q': - // Unsigned integers that could be too large. Note that the 'L' (long) case is handled here - // because when compiled for LP64, unsigned long is 64 bits and could overflow int64_t. - { - unsigned long long extended = [input unsignedLongLongValue]; - - if (extended > LLONG_MAX) { - FSTThrowInvalidArgument(@"NSNumber (%llu) is too large%@", - [input unsignedLongLongValue], [context fieldDescription]); - - } else { - return [FSTIntegerValue integerValue:(int64_t)extended]; - } - } - - case 'f': - return [FSTDoubleValue doubleValue:[input doubleValue]]; - - case 'd': - // Double values are already the right type, so just reuse the existing boxed double. - // - // Note that NSNumber already performs NaN normalization to a single shared instance - // so there's no need to treat NaN specially here. - return [FSTDoubleValue doubleValue:[input doubleValue]]; - - case 'B': // Falls through. - case 'c': // Falls through. - case 'C': - // Boolean values are weird. - // - // On arm64, objCType of a BOOL-valued NSNumber will be "c", even though @encode(BOOL) - // returns "B". "c" is the same as @encode(signed char). Unfortunately this means that - // legitimate usage of signed chars is impossible, but this should be rare. - // - // Additionally, for consistency, map unsigned chars to bools in the same way. - return [FSTBooleanValue booleanValue:[input boolValue]]; - - default: - // All documented codes should be handled above, so this shouldn't happen. - FSTCFail(@"Unknown NSNumber objCType %s on %@", cType, input); - } - - } else if ([input isKindOfClass:[NSString class]]) { - return [FSTStringValue stringValue:input]; - - } else if ([input isKindOfClass:[NSDate class]]) { - return [FSTTimestampValue timestampValue:[FSTTimestamp timestampWithDate:input]]; - - } else if ([input isKindOfClass:[FIRGeoPoint class]]) { - return [FSTGeoPointValue geoPointValue:input]; - - } else if ([input isKindOfClass:[NSData class]]) { - return [FSTBlobValue blobValue:input]; - - } else if ([input isKindOfClass:[FSTDocumentKeyReference class]]) { - FSTDocumentKeyReference *reference = input; - if (![reference.databaseID isEqual:self.databaseID]) { - FSTDatabaseID *other = reference.databaseID; - FSTThrowInvalidArgument( - @"Document Reference is for database %@/%@ but should be for database %@/%@%@", - other.projectID, other.databaseID, self.databaseID.projectID, self.databaseID.databaseID, - [context fieldDescription]); - } - return [FSTReferenceValue referenceValue:reference.key databaseID:self.databaseID]; - - } else if ([input isKindOfClass:[FIRFieldValue class]]) { - if ([input isKindOfClass:[FSTDeleteFieldValue class]]) { - if (context.dataSource == FSTUserDataSourceMergeSet) { - return nil; - } else if (context.dataSource == FSTUserDataSourceUpdate) { - FSTAssert(context.path.length > 0, - @"FieldValue.delete() at the top level should have already been handled."); - FSTThrowInvalidArgument( - @"FieldValue.delete() can only appear at the top level of your " - "update data%@", - [context fieldDescription]); - } else { - // We shouldn't encounter delete sentinels for queries or non-merge setData calls. - FSTThrowInvalidArgument( - @"FieldValue.delete() can only be used with updateData() and setData() with " - @"SetOptions.merge()."); - } - } else if ([input isKindOfClass:[FSTServerTimestampFieldValue class]]) { - if (![context isWrite]) { - FSTThrowInvalidArgument( - @"FieldValue.serverTimestamp() can only be used with setData() and updateData()."); - } - if (!context.path) { - FSTThrowInvalidArgument( - @"FieldValue.serverTimestamp() is not currently supported inside arrays%@", - [context fieldDescription]); - } - [context.fieldTransforms - addObject:[[FSTFieldTransform alloc] - initWithPath:context.path - transform:[FSTServerTimestampTransform serverTimestampTransform]]]; - - // Return nil so this value is omitted from the parsed result. - return nil; - } else { - FSTFail(@"Unknown FIRFieldValue type: %@", NSStringFromClass([input class])); - } - - } else { - FSTThrowInvalidArgument(@"Unsupported type: %@%@", NSStringFromClass([input class]), - [context fieldDescription]); - } -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FSTUserDataConverter.mm b/Firestore/Source/API/FSTUserDataConverter.mm new file mode 100644 index 0000000..414aadb --- /dev/null +++ b/Firestore/Source/API/FSTUserDataConverter.mm @@ -0,0 +1,598 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/API/FSTUserDataConverter.h" + +#import "FIRGeoPoint.h" +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRFieldPath+Internal.h" +#import "Firestore/Source/API/FIRFieldValue+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRSetOptions+Internal.h" +#import "Firestore/Source/Core/FSTTimestamp.h" +#import "Firestore/Source/Model/FSTDatabaseID.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const RESERVED_FIELD_DESIGNATOR = @"__"; + +#pragma mark - FSTParsedSetData + +@implementation FSTParsedSetData +- (instancetype)initWithData:(FSTObjectValue *)data + fieldMask:(nullable FSTFieldMask *)fieldMask + fieldTransforms:(NSArray *)fieldTransforms { + self = [super init]; + if (self) { + _data = data; + _fieldMask = fieldMask; + _fieldTransforms = fieldTransforms; + } + return self; +} + +- (NSArray *)mutationsWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition { + NSMutableArray *mutations = [NSMutableArray array]; + if (self.fieldMask) { + [mutations addObject:[[FSTPatchMutation alloc] initWithKey:key + fieldMask:self.fieldMask + value:self.data + precondition:precondition]]; + } else { + [mutations addObject:[[FSTSetMutation alloc] initWithKey:key + value:self.data + precondition:precondition]]; + } + if (self.fieldTransforms.count > 0) { + [mutations addObject:[[FSTTransformMutation alloc] initWithKey:key + fieldTransforms:self.fieldTransforms]]; + } + return mutations; +} + +@end + +#pragma mark - FSTParsedUpdateData + +@implementation FSTParsedUpdateData +- (instancetype)initWithData:(FSTObjectValue *)data + fieldMask:(FSTFieldMask *)fieldMask + fieldTransforms:(NSArray *)fieldTransforms { + self = [super init]; + if (self) { + _data = data; + _fieldMask = fieldMask; + _fieldTransforms = fieldTransforms; + } + return self; +} + +- (NSArray *)mutationsWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition { + NSMutableArray *mutations = [NSMutableArray array]; + [mutations addObject:[[FSTPatchMutation alloc] initWithKey:key + fieldMask:self.fieldMask + value:self.data + precondition:precondition]]; + if (self.fieldTransforms.count > 0) { + [mutations addObject:[[FSTTransformMutation alloc] initWithKey:key + fieldTransforms:self.fieldTransforms]]; + } + return mutations; +} + +@end + +/** + * Represents what type of API method provided the data being parsed; useful for determining which + * error conditions apply during parsing and providing better error messages. + */ +typedef NS_ENUM(NSInteger, FSTUserDataSource) { + FSTUserDataSourceSet, + FSTUserDataSourceMergeSet, + FSTUserDataSourceUpdate, + FSTUserDataSourceQueryValue, // from a where clause or cursor bound. +}; + +#pragma mark - FSTParseContext + +/** + * A "context" object passed around while parsing user data. + */ +@interface FSTParseContext : NSObject +/** The current path being parsed. */ +// TODO(b/34871131): path should never be nil, but we don't support array paths right now. +@property(nonatomic, strong, readonly, nullable) FSTFieldPath *path; + +/** Whether or not this context corresponds to an element of an array. */ +@property(nonatomic, assign, readonly, getter=isArrayElement) BOOL arrayElement; + +/** + * What type of API method provided the data being parsed; useful for determining which error + * conditions apply during parsing and providing better error messages. + */ +@property(nonatomic, assign) FSTUserDataSource dataSource; +@property(nonatomic, strong, readonly) NSMutableArray *fieldTransforms; +@property(nonatomic, strong, readonly) NSMutableArray *fieldMask; + +- (instancetype)init NS_UNAVAILABLE; +/** + * Initializes a FSTParseContext with the given source and path. + * + * @param dataSource Indicates what kind of API method this data came from. + * @param path A path within the object being parsed. This could be an empty path (in which case + * the context represents the root of the data being parsed), or a nonempty path (indicating the + * context represents a nested location within the data). + * + * TODO(b/34871131): We don't support array paths right now, so path can be nil to indicate + * the context represents any location within an array (in which case certain features will not work + * and errors will be somewhat compromised). + */ +- (instancetype)initWithSource:(FSTUserDataSource)dataSource + path:(nullable FSTFieldPath *)path + arrayElement:(BOOL)arrayElement + fieldTransforms:(NSMutableArray *)fieldTransforms + fieldMask:(NSMutableArray *)fieldMask + NS_DESIGNATED_INITIALIZER; + +// Helpers to get a FSTParseContext for a child field. +- (instancetype)contextForField:(NSString *)fieldName; +- (instancetype)contextForFieldPath:(FSTFieldPath *)fieldPath; +- (instancetype)contextForArrayIndex:(NSUInteger)index; + +/** Returns true for the non-query parse contexts (Set, MergeSet and Update) */ +- (BOOL)isWrite; +@end + +@implementation FSTParseContext + ++ (instancetype)contextWithSource:(FSTUserDataSource)dataSource path:(nullable FSTFieldPath *)path { + FSTParseContext *context = [[FSTParseContext alloc] initWithSource:dataSource + path:path + arrayElement:NO + fieldTransforms:[NSMutableArray array] + fieldMask:[NSMutableArray array]]; + [context validatePath]; + return context; +} + +- (instancetype)initWithSource:(FSTUserDataSource)dataSource + path:(nullable FSTFieldPath *)path + arrayElement:(BOOL)arrayElement + fieldTransforms:(NSMutableArray *)fieldTransforms + fieldMask:(NSMutableArray *)fieldMask { + if (self = [super init]) { + _dataSource = dataSource; + _path = path; + _arrayElement = arrayElement; + _fieldTransforms = fieldTransforms; + _fieldMask = fieldMask; + } + return self; +} + +- (instancetype)contextForField:(NSString *)fieldName { + FSTParseContext *context = + [[FSTParseContext alloc] initWithSource:self.dataSource + path:[self.path pathByAppendingSegment:fieldName] + arrayElement:NO + fieldTransforms:self.fieldTransforms + fieldMask:self.fieldMask]; + [context validatePathSegment:fieldName]; + return context; +} + +- (instancetype)contextForFieldPath:(FSTFieldPath *)fieldPath { + FSTParseContext *context = + [[FSTParseContext alloc] initWithSource:self.dataSource + path:[self.path pathByAppendingPath:fieldPath] + arrayElement:NO + fieldTransforms:self.fieldTransforms + fieldMask:self.fieldMask]; + [context validatePath]; + return context; +} + +- (instancetype)contextForArrayIndex:(NSUInteger)index { + // TODO(b/34871131): We don't support array paths right now; so make path nil. + return [[FSTParseContext alloc] initWithSource:self.dataSource + path:nil + arrayElement:YES + fieldTransforms:self.fieldTransforms + fieldMask:self.fieldMask]; +} + +/** + * Returns a string that can be appended to error messages indicating what field caused the error. + */ +- (NSString *)fieldDescription { + // TODO(b/34871131): Remove nil check once we have proper paths for fields within arrays. + if (!self.path || self.path.empty) { + return @""; + } else { + return [NSString stringWithFormat:@" (found in field %@)", self.path]; + } +} + +- (BOOL)isWrite { + switch (self.dataSource) { + case FSTUserDataSourceSet: // Falls through. + case FSTUserDataSourceMergeSet: // Falls through. + case FSTUserDataSourceUpdate: + return YES; + case FSTUserDataSourceQueryValue: + return NO; + default: + FSTThrowInvalidArgument(@"Unexpected case for FSTUserDataSource: %d", self.dataSource); + } +} + +- (void)validatePath { + // TODO(b/34871131): Remove nil check once we have proper paths for fields within arrays. + if (self.path == nil) { + return; + } + for (int i = 0; i < self.path.length; i++) { + [self validatePathSegment:[self.path segmentAtIndex:i]]; + } +} + +- (void)validatePathSegment:(NSString *)segment { + if ([self isWrite] && [segment hasPrefix:RESERVED_FIELD_DESIGNATOR] && + [segment hasSuffix:RESERVED_FIELD_DESIGNATOR]) { + FSTThrowInvalidArgument(@"Document fields cannot begin and end with %@%@", + RESERVED_FIELD_DESIGNATOR, [self fieldDescription]); + } +} + +@end + +#pragma mark - FSTDocumentKeyReference + +@implementation FSTDocumentKeyReference + +- (instancetype)initWithKey:(FSTDocumentKey *)key databaseID:(FSTDatabaseID *)databaseID { + self = [super init]; + if (self) { + _key = key; + _databaseID = databaseID; + } + return self; +} + +@end + +#pragma mark - FSTUserDataConverter + +@interface FSTUserDataConverter () +@property(strong, nonatomic, readonly) FSTDatabaseID *databaseID; +@property(strong, nonatomic, readonly) FSTPreConverterBlock preConverter; +@end + +@implementation FSTUserDataConverter + +- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID + preConverter:(FSTPreConverterBlock)preConverter { + self = [super init]; + if (self) { + _databaseID = databaseID; + _preConverter = preConverter; + } + return self; +} + +- (FSTParsedSetData *)parsedMergeData:(id)input { + // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust + // Obj-C to verify the type for us. + if (![input isKindOfClass:[NSDictionary class]]) { + FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary."); + } + + FSTParseContext *context = + [FSTParseContext contextWithSource:FSTUserDataSourceMergeSet path:[FSTFieldPath emptyPath]]; + FSTObjectValue *updateData = (FSTObjectValue *)[self parseData:input context:context]; + + return + [[FSTParsedSetData alloc] initWithData:updateData + fieldMask:[[FSTFieldMask alloc] initWithFields:context.fieldMask] + fieldTransforms:context.fieldTransforms]; +} + +- (FSTParsedSetData *)parsedSetData:(id)input { + // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust + // Obj-C to verify the type for us. + if (![input isKindOfClass:[NSDictionary class]]) { + FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary."); + } + + FSTParseContext *context = + [FSTParseContext contextWithSource:FSTUserDataSourceSet path:[FSTFieldPath emptyPath]]; + FSTObjectValue *updateData = (FSTObjectValue *)[self parseData:input context:context]; + + return [[FSTParsedSetData alloc] initWithData:updateData + fieldMask:nil + fieldTransforms:context.fieldTransforms]; +} + +- (FSTParsedUpdateData *)parsedUpdateData:(id)input { + // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust + // Obj-C to verify the type for us. + if (![input isKindOfClass:[NSDictionary class]]) { + FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary."); + } + + NSDictionary *dict = input; + + NSMutableArray *fieldMaskPaths = [NSMutableArray array]; + __block FSTObjectValue *updateData = [FSTObjectValue objectValue]; + + FSTParseContext *context = + [FSTParseContext contextWithSource:FSTUserDataSourceUpdate path:[FSTFieldPath emptyPath]]; + [dict enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + FSTFieldPath *path; + + if ([key isKindOfClass:[NSString class]]) { + path = [FIRFieldPath pathWithDotSeparatedString:key].internalValue; + } else if ([key isKindOfClass:[FIRFieldPath class]]) { + path = ((FIRFieldPath *)key).internalValue; + } else { + FSTThrowInvalidArgument( + @"Dictionary keys in updateData: must be NSStrings or FIRFieldPaths."); + } + + value = self.preConverter(value); + if ([value isKindOfClass:[FSTDeleteFieldValue class]]) { + // Add it to the field mask, but don't add anything to updateData. + [fieldMaskPaths addObject:path]; + } else { + FSTFieldValue *_Nullable parsedValue = + [self parseData:value context:[context contextForFieldPath:path]]; + if (parsedValue) { + [fieldMaskPaths addObject:path]; + updateData = [updateData objectBySettingValue:parsedValue forPath:path]; + } + } + }]; + + FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:fieldMaskPaths]; + return [[FSTParsedUpdateData alloc] initWithData:updateData + fieldMask:mask + fieldTransforms:context.fieldTransforms]; +} + +- (FSTFieldValue *)parsedQueryValue:(id)input { + FSTParseContext *context = + [FSTParseContext contextWithSource:FSTUserDataSourceQueryValue path:[FSTFieldPath emptyPath]]; + FSTFieldValue *_Nullable parsed = [self parseData:input context:context]; + FSTAssert(parsed, @"Parsed data should not be nil."); + FSTAssert(context.fieldTransforms.count == 0, @"Field transforms should have been disallowed."); + return parsed; +} + +/** + * Internal helper for parsing user data. + * + * @param input Data to be parsed. + * @param context A context object representing the current path being parsed, the source of the + * data being parsed, etc. + * + * @return The parsed value, or nil if the value was a FieldValue sentinel that should not be + * included in the resulting parsed data. + */ +- (nullable FSTFieldValue *)parseData:(id)input context:(FSTParseContext *)context { + input = self.preConverter(input); + if ([input isKindOfClass:[NSArray class]]) { + // TODO(b/34871131): Include the path containing the array in the error message. + if (context.isArrayElement) { + FSTThrowInvalidArgument(@"Nested arrays are not supported"); + } + NSArray *array = input; + NSMutableArray *result = [NSMutableArray arrayWithCapacity:array.count]; + [array enumerateObjectsUsingBlock:^(id entry, NSUInteger idx, BOOL *stop) { + FSTFieldValue *_Nullable parsedEntry = + [self parseData:entry context:[context contextForArrayIndex:idx]]; + if (!parsedEntry) { + // Just include nulls in the array for fields being replaced with a sentinel. + parsedEntry = [FSTNullValue nullValue]; + } + [result addObject:parsedEntry]; + }]; + // If context.path is nil we are already inside an array and we don't support field mask paths + // more granular than the top-level array. + if (context.path) { + [context.fieldMask addObject:context.path]; + } + return [[FSTArrayValue alloc] initWithValueNoCopy:result]; + + } else if ([input isKindOfClass:[NSDictionary class]]) { + NSDictionary *dict = input; + NSMutableDictionary *result = + [NSMutableDictionary dictionaryWithCapacity:dict.count]; + [dict enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + FSTFieldValue *_Nullable parsedValue = + [self parseData:value context:[context contextForField:key]]; + if (parsedValue) { + result[key] = parsedValue; + } + }]; + return [[FSTObjectValue alloc] initWithDictionary:result]; + + } else { + // If context.path is null, we are inside an array and we should have already added the root of + // the array to the field mask. + if (context.path) { + [context.fieldMask addObject:context.path]; + } + return [self parseScalarValue:input context:context]; + } +} + +/** + * Helper to parse a scalar value (i.e. not an NSDictionary or NSArray). + * + * Note that it handles all NSNumber values that are encodable as int64_t or doubles + * (depending on the underlying type of the NSNumber). Unsigned integer values are handled though + * any value outside what is representable by int64_t (a signed 64-bit value) will throw an + * exception. + * + * @return The parsed value, or nil if the value was a FieldValue sentinel that should not be + * included in the resulting parsed data. + */ +- (nullable FSTFieldValue *)parseScalarValue:(nullable id)input context:(FSTParseContext *)context { + if (!input || [input isMemberOfClass:[NSNull class]]) { + return [FSTNullValue nullValue]; + + } else if ([input isKindOfClass:[NSNumber class]]) { + // Recover the underlying type of the number, using the method described here: + // http://stackoverflow.com/questions/2518761/get-type-of-nsnumber + const char *cType = [input objCType]; + + // Type Encoding values taken from + // https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/ + // Articles/ocrtTypeEncodings.html + switch (cType[0]) { + case 'q': + return [FSTIntegerValue integerValue:[input longLongValue]]; + + case 'i': // Falls through. + case 's': // Falls through. + case 'l': // Falls through. + case 'I': // Falls through. + case 'S': + // Coerce integer values that aren't long long. Allow unsigned integer types that are + // guaranteed small enough to skip a length check. + return [FSTIntegerValue integerValue:[input longLongValue]]; + + case 'L': // Falls through. + case 'Q': + // Unsigned integers that could be too large. Note that the 'L' (long) case is handled here + // because when compiled for LP64, unsigned long is 64 bits and could overflow int64_t. + { + unsigned long long extended = [input unsignedLongLongValue]; + + if (extended > LLONG_MAX) { + FSTThrowInvalidArgument(@"NSNumber (%llu) is too large%@", + [input unsignedLongLongValue], [context fieldDescription]); + + } else { + return [FSTIntegerValue integerValue:(int64_t)extended]; + } + } + + case 'f': + return [FSTDoubleValue doubleValue:[input doubleValue]]; + + case 'd': + // Double values are already the right type, so just reuse the existing boxed double. + // + // Note that NSNumber already performs NaN normalization to a single shared instance + // so there's no need to treat NaN specially here. + return [FSTDoubleValue doubleValue:[input doubleValue]]; + + case 'B': // Falls through. + case 'c': // Falls through. + case 'C': + // Boolean values are weird. + // + // On arm64, objCType of a BOOL-valued NSNumber will be "c", even though @encode(BOOL) + // returns "B". "c" is the same as @encode(signed char). Unfortunately this means that + // legitimate usage of signed chars is impossible, but this should be rare. + // + // Additionally, for consistency, map unsigned chars to bools in the same way. + return [FSTBooleanValue booleanValue:[input boolValue]]; + + default: + // All documented codes should be handled above, so this shouldn't happen. + FSTCFail(@"Unknown NSNumber objCType %s on %@", cType, input); + } + + } else if ([input isKindOfClass:[NSString class]]) { + return [FSTStringValue stringValue:input]; + + } else if ([input isKindOfClass:[NSDate class]]) { + return [FSTTimestampValue timestampValue:[FSTTimestamp timestampWithDate:input]]; + + } else if ([input isKindOfClass:[FIRGeoPoint class]]) { + return [FSTGeoPointValue geoPointValue:input]; + + } else if ([input isKindOfClass:[NSData class]]) { + return [FSTBlobValue blobValue:input]; + + } else if ([input isKindOfClass:[FSTDocumentKeyReference class]]) { + FSTDocumentKeyReference *reference = input; + if (![reference.databaseID isEqual:self.databaseID]) { + FSTDatabaseID *other = reference.databaseID; + FSTThrowInvalidArgument( + @"Document Reference is for database %@/%@ but should be for database %@/%@%@", + other.projectID, other.databaseID, self.databaseID.projectID, self.databaseID.databaseID, + [context fieldDescription]); + } + return [FSTReferenceValue referenceValue:reference.key databaseID:self.databaseID]; + + } else if ([input isKindOfClass:[FIRFieldValue class]]) { + if ([input isKindOfClass:[FSTDeleteFieldValue class]]) { + if (context.dataSource == FSTUserDataSourceMergeSet) { + return nil; + } else if (context.dataSource == FSTUserDataSourceUpdate) { + FSTAssert(context.path.length > 0, + @"FieldValue.delete() at the top level should have already been handled."); + FSTThrowInvalidArgument( + @"FieldValue.delete() can only appear at the top level of your " + "update data%@", + [context fieldDescription]); + } else { + // We shouldn't encounter delete sentinels for queries or non-merge setData calls. + FSTThrowInvalidArgument( + @"FieldValue.delete() can only be used with updateData() and setData() with " + @"SetOptions.merge()."); + } + } else if ([input isKindOfClass:[FSTServerTimestampFieldValue class]]) { + if (![context isWrite]) { + FSTThrowInvalidArgument( + @"FieldValue.serverTimestamp() can only be used with setData() and updateData()."); + } + if (!context.path) { + FSTThrowInvalidArgument( + @"FieldValue.serverTimestamp() is not currently supported inside arrays%@", + [context fieldDescription]); + } + [context.fieldTransforms + addObject:[[FSTFieldTransform alloc] + initWithPath:context.path + transform:[FSTServerTimestampTransform serverTimestampTransform]]]; + + // Return nil so this value is omitted from the parsed result. + return nil; + } else { + FSTFail(@"Unknown FIRFieldValue type: %@", NSStringFromClass([input class])); + } + + } else { + FSTThrowInvalidArgument(@"Unsupported type: %@%@", NSStringFromClass([input class]), + [context fieldDescription]); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Auth/FSTCredentialsProvider.m b/Firestore/Source/Auth/FSTCredentialsProvider.m deleted file mode 100644 index 653d7ff..0000000 --- a/Firestore/Source/Auth/FSTCredentialsProvider.m +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Auth/FSTCredentialsProvider.h" - -#import -#import -#import - -#import "FIRFirestoreErrors.h" -#import "Firestore/Source/Auth/FSTUser.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTClasses.h" -#import "Firestore/Source/Util/FSTDispatchQueue.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTGetTokenResult - -@implementation FSTGetTokenResult -- (instancetype)initWithUser:(FSTUser *)user token:(NSString *_Nullable)token { - if (self = [super init]) { - _user = user; - _token = token; - } - return self; -} -@end - -#pragma mark - FSTFirebaseCredentialsProvider -@interface FSTFirebaseCredentialsProvider () - -@property(nonatomic, strong, readonly) FIRApp *app; - -/** Handle used to stop receiving auth changes once userChangeListener is removed. */ -@property(nonatomic, strong, nullable, readwrite) id authListenerHandle; - -/** The current user as reported to us via our AuthStateDidChangeListener. */ -@property(nonatomic, strong, nonnull, readwrite) FSTUser *currentUser; - -/** - * Counter used to detect if the user changed while a -getTokenForcingRefresh: request was - * outstanding. - */ -@property(nonatomic, assign, readwrite) int userCounter; - -@end - -@implementation FSTFirebaseCredentialsProvider { - FSTVoidUserBlock _userChangeListener; -} - -- (instancetype)initWithApp:(FIRApp *)app { - self = [super init]; - if (self) { - _app = app; - _currentUser = [[FSTUser alloc] initWithUID:[self.app getUID]]; - _userCounter = 0; - - // Register for user changes so that we can internally track the current user. - FSTWeakify(self); - _authListenerHandle = [[NSNotificationCenter defaultCenter] - addObserverForName:FIRAuthStateDidChangeInternalNotification - object:nil - queue:nil - usingBlock:^(NSNotification *notification) { - FSTStrongify(self); - if (self) { - @synchronized(self) { - NSDictionary *userInfo = notification.userInfo; - - // ensure we're only notifiying for the current app. - FIRApp *notifiedApp = - userInfo[FIRAuthStateDidChangeInternalNotificationAppKey]; - if (![self.app isEqual:notifiedApp]) { - return; - } - - NSString *userID = userInfo[FIRAuthStateDidChangeInternalNotificationUIDKey]; - FSTUser *newUser = [[FSTUser alloc] initWithUID:userID]; - if (![newUser isEqual:self.currentUser]) { - self.currentUser = newUser; - self.userCounter++; - FSTVoidUserBlock listenerBlock = self.userChangeListener; - if (listenerBlock) { - listenerBlock(self.currentUser); - } - } - } - } - }]; - } - return self; -} - -- (void)getTokenForcingRefresh:(BOOL)forceRefresh - completion:(FSTVoidGetTokenResultBlock)completion { - FSTAssert(self.authListenerHandle, @"getToken cannot be called after listener removed."); - - // Take note of the current value of the userCounter so that this method can fail (with a - // FIRFirestoreErrorCodeAborted error) if there is a user change while the request is outstanding. - int initialUserCounter = self.userCounter; - - void (^getTokenCallback)(NSString *, NSError *) = - ^(NSString *_Nullable token, NSError *_Nullable error) { - @synchronized(self) { - if (initialUserCounter != self.userCounter) { - // Cancel the request since the user changed while the request was outstanding so the - // response is likely for a previous user (which user, we can't be sure). - NSDictionary *errorInfo = @{@"details" : @"getToken aborted due to user change."}; - NSError *cancelError = [NSError errorWithDomain:FIRFirestoreErrorDomain - code:FIRFirestoreErrorCodeAborted - userInfo:errorInfo]; - completion(nil, cancelError); - } else { - FSTGetTokenResult *result = - [[FSTGetTokenResult alloc] initWithUser:self.currentUser token:token]; - completion(result, error); - } - }; - }; - - [self.app getTokenForcingRefresh:forceRefresh withCallback:getTokenCallback]; -} - -- (void)setUserChangeListener:(nullable FSTVoidUserBlock)block { - @synchronized(self) { - if (block) { - FSTAssert(!_userChangeListener, @"UserChangeListener set twice!"); - - // Fire initial event. - block(self.currentUser); - } else { - FSTAssert(self.authListenerHandle, @"UserChangeListener removed twice!"); - FSTAssert(_userChangeListener, @"UserChangeListener removed without being set!"); - [[NSNotificationCenter defaultCenter] removeObserver:self.authListenerHandle]; - self.authListenerHandle = nil; - } - _userChangeListener = block; - } -} - -- (nullable FSTVoidUserBlock)userChangeListener { - @synchronized(self) { - return _userChangeListener; - } -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Auth/FSTCredentialsProvider.mm b/Firestore/Source/Auth/FSTCredentialsProvider.mm new file mode 100644 index 0000000..653d7ff --- /dev/null +++ b/Firestore/Source/Auth/FSTCredentialsProvider.mm @@ -0,0 +1,164 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Auth/FSTCredentialsProvider.h" + +#import +#import +#import + +#import "FIRFirestoreErrors.h" +#import "Firestore/Source/Auth/FSTUser.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTClasses.h" +#import "Firestore/Source/Util/FSTDispatchQueue.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTGetTokenResult + +@implementation FSTGetTokenResult +- (instancetype)initWithUser:(FSTUser *)user token:(NSString *_Nullable)token { + if (self = [super init]) { + _user = user; + _token = token; + } + return self; +} +@end + +#pragma mark - FSTFirebaseCredentialsProvider +@interface FSTFirebaseCredentialsProvider () + +@property(nonatomic, strong, readonly) FIRApp *app; + +/** Handle used to stop receiving auth changes once userChangeListener is removed. */ +@property(nonatomic, strong, nullable, readwrite) id authListenerHandle; + +/** The current user as reported to us via our AuthStateDidChangeListener. */ +@property(nonatomic, strong, nonnull, readwrite) FSTUser *currentUser; + +/** + * Counter used to detect if the user changed while a -getTokenForcingRefresh: request was + * outstanding. + */ +@property(nonatomic, assign, readwrite) int userCounter; + +@end + +@implementation FSTFirebaseCredentialsProvider { + FSTVoidUserBlock _userChangeListener; +} + +- (instancetype)initWithApp:(FIRApp *)app { + self = [super init]; + if (self) { + _app = app; + _currentUser = [[FSTUser alloc] initWithUID:[self.app getUID]]; + _userCounter = 0; + + // Register for user changes so that we can internally track the current user. + FSTWeakify(self); + _authListenerHandle = [[NSNotificationCenter defaultCenter] + addObserverForName:FIRAuthStateDidChangeInternalNotification + object:nil + queue:nil + usingBlock:^(NSNotification *notification) { + FSTStrongify(self); + if (self) { + @synchronized(self) { + NSDictionary *userInfo = notification.userInfo; + + // ensure we're only notifiying for the current app. + FIRApp *notifiedApp = + userInfo[FIRAuthStateDidChangeInternalNotificationAppKey]; + if (![self.app isEqual:notifiedApp]) { + return; + } + + NSString *userID = userInfo[FIRAuthStateDidChangeInternalNotificationUIDKey]; + FSTUser *newUser = [[FSTUser alloc] initWithUID:userID]; + if (![newUser isEqual:self.currentUser]) { + self.currentUser = newUser; + self.userCounter++; + FSTVoidUserBlock listenerBlock = self.userChangeListener; + if (listenerBlock) { + listenerBlock(self.currentUser); + } + } + } + } + }]; + } + return self; +} + +- (void)getTokenForcingRefresh:(BOOL)forceRefresh + completion:(FSTVoidGetTokenResultBlock)completion { + FSTAssert(self.authListenerHandle, @"getToken cannot be called after listener removed."); + + // Take note of the current value of the userCounter so that this method can fail (with a + // FIRFirestoreErrorCodeAborted error) if there is a user change while the request is outstanding. + int initialUserCounter = self.userCounter; + + void (^getTokenCallback)(NSString *, NSError *) = + ^(NSString *_Nullable token, NSError *_Nullable error) { + @synchronized(self) { + if (initialUserCounter != self.userCounter) { + // Cancel the request since the user changed while the request was outstanding so the + // response is likely for a previous user (which user, we can't be sure). + NSDictionary *errorInfo = @{@"details" : @"getToken aborted due to user change."}; + NSError *cancelError = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeAborted + userInfo:errorInfo]; + completion(nil, cancelError); + } else { + FSTGetTokenResult *result = + [[FSTGetTokenResult alloc] initWithUser:self.currentUser token:token]; + completion(result, error); + } + }; + }; + + [self.app getTokenForcingRefresh:forceRefresh withCallback:getTokenCallback]; +} + +- (void)setUserChangeListener:(nullable FSTVoidUserBlock)block { + @synchronized(self) { + if (block) { + FSTAssert(!_userChangeListener, @"UserChangeListener set twice!"); + + // Fire initial event. + block(self.currentUser); + } else { + FSTAssert(self.authListenerHandle, @"UserChangeListener removed twice!"); + FSTAssert(_userChangeListener, @"UserChangeListener removed without being set!"); + [[NSNotificationCenter defaultCenter] removeObserver:self.authListenerHandle]; + self.authListenerHandle = nil; + } + _userChangeListener = block; + } +} + +- (nullable FSTVoidUserBlock)userChangeListener { + @synchronized(self) { + return _userChangeListener; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Auth/FSTEmptyCredentialsProvider.m b/Firestore/Source/Auth/FSTEmptyCredentialsProvider.m deleted file mode 100644 index e78452a..0000000 --- a/Firestore/Source/Auth/FSTEmptyCredentialsProvider.m +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Auth/FSTEmptyCredentialsProvider.h" - -#import "Firestore/Source/Auth/FSTUser.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTDispatchQueue.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTEmptyCredentialsProvider - -- (void)getTokenForcingRefresh:(BOOL)forceRefresh - completion:(FSTVoidGetTokenResultBlock)completion { - completion(nil, nil); -} - -- (void)setUserChangeListener:(nullable FSTVoidUserBlock)block { - // Since the user never changes, we just need to fire the initial event and don't need to hang - // onto the block. - if (block) { - block([FSTUser unauthenticatedUser]); - } -} - -- (nullable FSTVoidUserBlock)userChangeListener { - // TODO(mikelehen): Implementation omitted for convenience since it's not actually required. - FSTFail(@"Not implemented."); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Auth/FSTEmptyCredentialsProvider.mm b/Firestore/Source/Auth/FSTEmptyCredentialsProvider.mm new file mode 100644 index 0000000..e78452a --- /dev/null +++ b/Firestore/Source/Auth/FSTEmptyCredentialsProvider.mm @@ -0,0 +1,47 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Auth/FSTEmptyCredentialsProvider.h" + +#import "Firestore/Source/Auth/FSTUser.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTDispatchQueue.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTEmptyCredentialsProvider + +- (void)getTokenForcingRefresh:(BOOL)forceRefresh + completion:(FSTVoidGetTokenResultBlock)completion { + completion(nil, nil); +} + +- (void)setUserChangeListener:(nullable FSTVoidUserBlock)block { + // Since the user never changes, we just need to fire the initial event and don't need to hang + // onto the block. + if (block) { + block([FSTUser unauthenticatedUser]); + } +} + +- (nullable FSTVoidUserBlock)userChangeListener { + // TODO(mikelehen): Implementation omitted for convenience since it's not actually required. + FSTFail(@"Not implemented."); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Auth/FSTUser.m b/Firestore/Source/Auth/FSTUser.m deleted file mode 100644 index 605b4e6..0000000 --- a/Firestore/Source/Auth/FSTUser.m +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Auth/FSTUser.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTUser - -@implementation FSTUser - -@dynamic unauthenticated; - -+ (instancetype)unauthenticatedUser { - return [[FSTUser alloc] initWithUID:nil]; -} - -- (instancetype)initWithUID:(NSString *_Nullable)UID { - if (self = [super init]) { - _UID = UID; - } - return self; -} - -- (BOOL)isEqual:(id)object { - if (self == object) { - return YES; - } else if (![object isKindOfClass:[FSTUser class]]) { - return NO; - } else { - FSTUser *other = object; - return (self.isUnauthenticated && other.isUnauthenticated) || - [self.UID isEqualToString:other.UID]; - } -} - -- (NSUInteger)hash { - return [self.UID hash]; -} - -- (id)copyWithZone:(nullable NSZone *)zone { - return self; // since FSTUser objects are immutable -} - -- (NSString *)description { - return [NSString stringWithFormat:@"", self.UID]; -} - -- (BOOL)isUnauthenticated { - return self.UID == nil; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Auth/FSTUser.mm b/Firestore/Source/Auth/FSTUser.mm new file mode 100644 index 0000000..605b4e6 --- /dev/null +++ b/Firestore/Source/Auth/FSTUser.mm @@ -0,0 +1,68 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Auth/FSTUser.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTUser + +@implementation FSTUser + +@dynamic unauthenticated; + ++ (instancetype)unauthenticatedUser { + return [[FSTUser alloc] initWithUID:nil]; +} + +- (instancetype)initWithUID:(NSString *_Nullable)UID { + if (self = [super init]) { + _UID = UID; + } + return self; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } else if (![object isKindOfClass:[FSTUser class]]) { + return NO; + } else { + FSTUser *other = object; + return (self.isUnauthenticated && other.isUnauthenticated) || + [self.UID isEqualToString:other.UID]; + } +} + +- (NSUInteger)hash { + return [self.UID hash]; +} + +- (id)copyWithZone:(nullable NSZone *)zone { + return self; // since FSTUser objects are immutable +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.UID]; +} + +- (BOOL)isUnauthenticated { + return self.UID == nil; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTDatabaseInfo.m b/Firestore/Source/Core/FSTDatabaseInfo.m deleted file mode 100644 index 2dbe61a..0000000 --- a/Firestore/Source/Core/FSTDatabaseInfo.m +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Core/FSTDatabaseInfo.h" - -#import "Firestore/Source/Model/FSTDatabaseID.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTDatabaseInfo - -@implementation FSTDatabaseInfo - -#pragma mark - Constructors - -+ (instancetype)databaseInfoWithDatabaseID:(FSTDatabaseID *)databaseID - persistenceKey:(NSString *)persistenceKey - host:(NSString *)host - sslEnabled:(BOOL)sslEnabled { - return [[FSTDatabaseInfo alloc] initWithDatabaseID:databaseID - persistenceKey:persistenceKey - host:host - sslEnabled:sslEnabled]; -} - -/** - * Designated initializer. - * - * @param databaseID The database in the datastore. - * @param persistenceKey A unique identifier for this Firestore's local storage. Usually derived - * from -[FIRApp appName]. - * @param host The Firestore server hostname. - * @param sslEnabled Whether to use SSL when connecting. - */ -- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID - persistenceKey:(NSString *)persistenceKey - host:(NSString *)host - sslEnabled:(BOOL)sslEnabled { - if (self = [super init]) { - _databaseID = databaseID; - _persistenceKey = [persistenceKey copy]; - _host = [host copy]; - _sslEnabled = sslEnabled; - } - return self; -} - -#pragma mark - NSObject methods - -- (NSString *)description { - return [NSString - stringWithFormat:@"", self.databaseID, self.host]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTDatabaseInfo.mm b/Firestore/Source/Core/FSTDatabaseInfo.mm new file mode 100644 index 0000000..2dbe61a --- /dev/null +++ b/Firestore/Source/Core/FSTDatabaseInfo.mm @@ -0,0 +1,70 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Core/FSTDatabaseInfo.h" + +#import "Firestore/Source/Model/FSTDatabaseID.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTDatabaseInfo + +@implementation FSTDatabaseInfo + +#pragma mark - Constructors + ++ (instancetype)databaseInfoWithDatabaseID:(FSTDatabaseID *)databaseID + persistenceKey:(NSString *)persistenceKey + host:(NSString *)host + sslEnabled:(BOOL)sslEnabled { + return [[FSTDatabaseInfo alloc] initWithDatabaseID:databaseID + persistenceKey:persistenceKey + host:host + sslEnabled:sslEnabled]; +} + +/** + * Designated initializer. + * + * @param databaseID The database in the datastore. + * @param persistenceKey A unique identifier for this Firestore's local storage. Usually derived + * from -[FIRApp appName]. + * @param host The Firestore server hostname. + * @param sslEnabled Whether to use SSL when connecting. + */ +- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID + persistenceKey:(NSString *)persistenceKey + host:(NSString *)host + sslEnabled:(BOOL)sslEnabled { + if (self = [super init]) { + _databaseID = databaseID; + _persistenceKey = [persistenceKey copy]; + _host = [host copy]; + _sslEnabled = sslEnabled; + } + return self; +} + +#pragma mark - NSObject methods + +- (NSString *)description { + return [NSString + stringWithFormat:@"", self.databaseID, self.host]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTEventManager.m b/Firestore/Source/Core/FSTEventManager.m deleted file mode 100644 index bc204a0..0000000 --- a/Firestore/Source/Core/FSTEventManager.m +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Core/FSTEventManager.h" - -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSyncEngine.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTListenOptions - -@implementation FSTListenOptions - -+ (instancetype)defaultOptions { - static FSTListenOptions *defaultOptions; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - defaultOptions = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO - includeDocumentMetadataChanges:NO - waitForSyncWhenOnline:NO]; - }); - return defaultOptions; -} - -- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges - includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges - waitForSyncWhenOnline:(BOOL)waitForSyncWhenOnline { - if (self = [super init]) { - _includeQueryMetadataChanges = includeQueryMetadataChanges; - _includeDocumentMetadataChanges = includeDocumentMetadataChanges; - _waitForSyncWhenOnline = waitForSyncWhenOnline; - } - return self; -} - -- (instancetype)init { - FSTFail(@"FSTListenOptions init not supported"); - return nil; -} - -@end - -#pragma mark - FSTQueryListenersInfo - -/** - * Holds the listeners and the last received ViewSnapshot for a query being tracked by - * EventManager. - */ -@interface FSTQueryListenersInfo : NSObject -@property(nonatomic, strong, nullable, readwrite) FSTViewSnapshot *viewSnapshot; -@property(nonatomic, assign, readwrite) FSTTargetID targetID; -@property(nonatomic, strong, readonly) NSMutableArray *listeners; -@end - -@implementation FSTQueryListenersInfo -- (instancetype)init { - if (self = [super init]) { - _listeners = [NSMutableArray array]; - } - return self; -} - -@end - -#pragma mark - FSTQueryListener - -@interface FSTQueryListener () - -/** The last received view snapshot. */ -@property(nonatomic, strong, nullable) FSTViewSnapshot *snapshot; - -@property(nonatomic, strong, readonly) FSTListenOptions *options; - -/** - * Initial snapshots (e.g. from cache) may not be propagated to the FSTViewSnapshotHandler. - * This flag is set to YES once we've actually raised an event. - */ -@property(nonatomic, assign, readwrite) BOOL raisedInitialEvent; - -/** The last online state this query listener got. */ -@property(nonatomic, assign, readwrite) FSTOnlineState onlineState; - -/** The FSTViewSnapshotHandler associated with this query listener. */ -@property(nonatomic, copy, nullable) FSTViewSnapshotHandler viewSnapshotHandler; - -@end - -@implementation FSTQueryListener - -- (instancetype)initWithQuery:(FSTQuery *)query - options:(FSTListenOptions *)options - viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler { - if (self = [super init]) { - _query = query; - _options = options; - _viewSnapshotHandler = viewSnapshotHandler; - _raisedInitialEvent = NO; - } - return self; -} - -- (void)queryDidChangeViewSnapshot:(FSTViewSnapshot *)snapshot { - FSTAssert(snapshot.documentChanges.count > 0 || snapshot.syncStateChanged, - @"We got a new snapshot with no changes?"); - - if (!self.options.includeDocumentMetadataChanges) { - // Remove the metadata-only changes. - NSMutableArray *changes = [NSMutableArray array]; - for (FSTDocumentViewChange *change in snapshot.documentChanges) { - if (change.type != FSTDocumentViewChangeTypeMetadata) { - [changes addObject:change]; - } - } - snapshot = [[FSTViewSnapshot alloc] initWithQuery:snapshot.query - documents:snapshot.documents - oldDocuments:snapshot.oldDocuments - documentChanges:changes - fromCache:snapshot.fromCache - hasPendingWrites:snapshot.hasPendingWrites - syncStateChanged:snapshot.syncStateChanged]; - } - - if (!self.raisedInitialEvent) { - if ([self shouldRaiseInitialEventForSnapshot:snapshot onlineState:self.onlineState]) { - [self raiseInitialEventForSnapshot:snapshot]; - } - } else if ([self shouldRaiseEventForSnapshot:snapshot]) { - self.viewSnapshotHandler(snapshot, nil); - } - - self.snapshot = snapshot; -} - -- (void)queryDidError:(NSError *)error { - self.viewSnapshotHandler(nil, error); -} - -- (void)applyChangedOnlineState:(FSTOnlineState)onlineState { - self.onlineState = onlineState; - if (self.snapshot && !self.raisedInitialEvent && - [self shouldRaiseInitialEventForSnapshot:self.snapshot onlineState:onlineState]) { - [self raiseInitialEventForSnapshot:self.snapshot]; - } -} - -- (BOOL)shouldRaiseInitialEventForSnapshot:(FSTViewSnapshot *)snapshot - onlineState:(FSTOnlineState)onlineState { - FSTAssert(!self.raisedInitialEvent, - @"Determining whether to raise initial event, but already had first event."); - - // Always raise the first event when we're synced - if (!snapshot.fromCache) { - return YES; - } - - // NOTE: We consider OnlineState.Unknown as online (it should become Failed - // or Online if we wait long enough). - BOOL maybeOnline = onlineState != FSTOnlineStateFailed; - // Don't raise the event if we're online, aren't synced yet (checked - // above) and are waiting for a sync. - if (self.options.waitForSyncWhenOnline && maybeOnline) { - FSTAssert(snapshot.fromCache, @"Waiting for sync, but snapshot is not from cache."); - return NO; - } - - // Raise data from cache if we have any documents or we are offline - return !snapshot.documents.isEmpty || onlineState == FSTOnlineStateFailed; -} - -- (BOOL)shouldRaiseEventForSnapshot:(FSTViewSnapshot *)snapshot { - // We don't need to handle includeDocumentMetadataChanges here because the Metadata only changes - // have already been stripped out if needed. At this point the only changes we will see are the - // ones we should propagate. - if (snapshot.documentChanges.count > 0) { - return YES; - } - - BOOL hasPendingWritesChanged = - self.snapshot && self.snapshot.hasPendingWrites != snapshot.hasPendingWrites; - if (snapshot.syncStateChanged || hasPendingWritesChanged) { - return self.options.includeQueryMetadataChanges; - } - - // Generally we should have hit one of the cases above, but it's possible to get here if there - // were only metadata docChanges and they got stripped out. - return NO; -} - -- (void)raiseInitialEventForSnapshot:(FSTViewSnapshot *)snapshot { - FSTAssert(!self.raisedInitialEvent, @"Trying to raise initial events for second time"); - snapshot = [[FSTViewSnapshot alloc] - initWithQuery:snapshot.query - documents:snapshot.documents - oldDocuments:[FSTDocumentSet documentSetWithComparator:snapshot.query.comparator] - documentChanges:[FSTQueryListener getInitialViewChangesFor:snapshot] - fromCache:snapshot.fromCache - hasPendingWrites:snapshot.hasPendingWrites - syncStateChanged:YES]; - self.raisedInitialEvent = YES; - self.viewSnapshotHandler(snapshot, nil); -} - -+ (NSArray *)getInitialViewChangesFor:(FSTViewSnapshot *)snapshot { - NSMutableArray *result = [NSMutableArray array]; - for (FSTDocument *doc in snapshot.documents.documentEnumerator) { - [result addObject:[FSTDocumentViewChange changeWithDocument:doc - type:FSTDocumentViewChangeTypeAdded]]; - } - return result; -} - -@end - -#pragma mark - FSTEventManager - -@interface FSTEventManager () - -- (instancetype)initWithSyncEngine:(FSTSyncEngine *)syncEngine NS_DESIGNATED_INITIALIZER; - -@property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine; -@property(nonatomic, strong, readonly) - NSMutableDictionary *queries; -@property(nonatomic, assign) FSTOnlineState onlineState; - -@end - -@implementation FSTEventManager - -+ (instancetype)eventManagerWithSyncEngine:(FSTSyncEngine *)syncEngine { - return [[FSTEventManager alloc] initWithSyncEngine:syncEngine]; -} - -- (instancetype)initWithSyncEngine:(FSTSyncEngine *)syncEngine { - if (self = [super init]) { - _syncEngine = syncEngine; - _queries = [NSMutableDictionary dictionary]; - - _syncEngine.delegate = self; - } - return self; -} - -- (FSTTargetID)addListener:(FSTQueryListener *)listener { - FSTQuery *query = listener.query; - BOOL firstListen = NO; - - FSTQueryListenersInfo *queryInfo = self.queries[query]; - if (!queryInfo) { - firstListen = YES; - queryInfo = [[FSTQueryListenersInfo alloc] init]; - self.queries[query] = queryInfo; - } - [queryInfo.listeners addObject:listener]; - - [listener applyChangedOnlineState:self.onlineState]; - - if (queryInfo.viewSnapshot) { - [listener queryDidChangeViewSnapshot:queryInfo.viewSnapshot]; - } - - if (firstListen) { - queryInfo.targetID = [self.syncEngine listenToQuery:query]; - } - return queryInfo.targetID; -} - -- (void)removeListener:(FSTQueryListener *)listener { - FSTQuery *query = listener.query; - BOOL lastListen = NO; - - FSTQueryListenersInfo *queryInfo = self.queries[query]; - if (queryInfo) { - [queryInfo.listeners removeObject:listener]; - lastListen = (queryInfo.listeners.count == 0); - } - - if (lastListen) { - [self.queries removeObjectForKey:query]; - [self.syncEngine stopListeningToQuery:query]; - } -} - -- (void)handleViewSnapshots:(NSArray *)viewSnapshots { - for (FSTViewSnapshot *viewSnapshot in viewSnapshots) { - FSTQuery *query = viewSnapshot.query; - FSTQueryListenersInfo *queryInfo = self.queries[query]; - if (queryInfo) { - for (FSTQueryListener *listener in queryInfo.listeners) { - [listener queryDidChangeViewSnapshot:viewSnapshot]; - } - queryInfo.viewSnapshot = viewSnapshot; - } - } -} - -- (void)handleError:(NSError *)error forQuery:(FSTQuery *)query { - FSTQueryListenersInfo *queryInfo = self.queries[query]; - if (queryInfo) { - for (FSTQueryListener *listener in queryInfo.listeners) { - [listener queryDidError:error]; - } - } - - // Remove all listeners. NOTE: We don't need to call [FSTSyncEngine stopListening] after an error. - [self.queries removeObjectForKey:query]; -} - -- (void)applyChangedOnlineState:(FSTOnlineState)onlineState { - self.onlineState = onlineState; - for (FSTQueryListenersInfo *info in self.queries.objectEnumerator) { - for (FSTQueryListener *listener in info.listeners) { - [listener applyChangedOnlineState:onlineState]; - } - } -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTEventManager.mm b/Firestore/Source/Core/FSTEventManager.mm new file mode 100644 index 0000000..bc204a0 --- /dev/null +++ b/Firestore/Source/Core/FSTEventManager.mm @@ -0,0 +1,335 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Core/FSTEventManager.h" + +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTSyncEngine.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTListenOptions + +@implementation FSTListenOptions + ++ (instancetype)defaultOptions { + static FSTListenOptions *defaultOptions; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + defaultOptions = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO + includeDocumentMetadataChanges:NO + waitForSyncWhenOnline:NO]; + }); + return defaultOptions; +} + +- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges + includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges + waitForSyncWhenOnline:(BOOL)waitForSyncWhenOnline { + if (self = [super init]) { + _includeQueryMetadataChanges = includeQueryMetadataChanges; + _includeDocumentMetadataChanges = includeDocumentMetadataChanges; + _waitForSyncWhenOnline = waitForSyncWhenOnline; + } + return self; +} + +- (instancetype)init { + FSTFail(@"FSTListenOptions init not supported"); + return nil; +} + +@end + +#pragma mark - FSTQueryListenersInfo + +/** + * Holds the listeners and the last received ViewSnapshot for a query being tracked by + * EventManager. + */ +@interface FSTQueryListenersInfo : NSObject +@property(nonatomic, strong, nullable, readwrite) FSTViewSnapshot *viewSnapshot; +@property(nonatomic, assign, readwrite) FSTTargetID targetID; +@property(nonatomic, strong, readonly) NSMutableArray *listeners; +@end + +@implementation FSTQueryListenersInfo +- (instancetype)init { + if (self = [super init]) { + _listeners = [NSMutableArray array]; + } + return self; +} + +@end + +#pragma mark - FSTQueryListener + +@interface FSTQueryListener () + +/** The last received view snapshot. */ +@property(nonatomic, strong, nullable) FSTViewSnapshot *snapshot; + +@property(nonatomic, strong, readonly) FSTListenOptions *options; + +/** + * Initial snapshots (e.g. from cache) may not be propagated to the FSTViewSnapshotHandler. + * This flag is set to YES once we've actually raised an event. + */ +@property(nonatomic, assign, readwrite) BOOL raisedInitialEvent; + +/** The last online state this query listener got. */ +@property(nonatomic, assign, readwrite) FSTOnlineState onlineState; + +/** The FSTViewSnapshotHandler associated with this query listener. */ +@property(nonatomic, copy, nullable) FSTViewSnapshotHandler viewSnapshotHandler; + +@end + +@implementation FSTQueryListener + +- (instancetype)initWithQuery:(FSTQuery *)query + options:(FSTListenOptions *)options + viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler { + if (self = [super init]) { + _query = query; + _options = options; + _viewSnapshotHandler = viewSnapshotHandler; + _raisedInitialEvent = NO; + } + return self; +} + +- (void)queryDidChangeViewSnapshot:(FSTViewSnapshot *)snapshot { + FSTAssert(snapshot.documentChanges.count > 0 || snapshot.syncStateChanged, + @"We got a new snapshot with no changes?"); + + if (!self.options.includeDocumentMetadataChanges) { + // Remove the metadata-only changes. + NSMutableArray *changes = [NSMutableArray array]; + for (FSTDocumentViewChange *change in snapshot.documentChanges) { + if (change.type != FSTDocumentViewChangeTypeMetadata) { + [changes addObject:change]; + } + } + snapshot = [[FSTViewSnapshot alloc] initWithQuery:snapshot.query + documents:snapshot.documents + oldDocuments:snapshot.oldDocuments + documentChanges:changes + fromCache:snapshot.fromCache + hasPendingWrites:snapshot.hasPendingWrites + syncStateChanged:snapshot.syncStateChanged]; + } + + if (!self.raisedInitialEvent) { + if ([self shouldRaiseInitialEventForSnapshot:snapshot onlineState:self.onlineState]) { + [self raiseInitialEventForSnapshot:snapshot]; + } + } else if ([self shouldRaiseEventForSnapshot:snapshot]) { + self.viewSnapshotHandler(snapshot, nil); + } + + self.snapshot = snapshot; +} + +- (void)queryDidError:(NSError *)error { + self.viewSnapshotHandler(nil, error); +} + +- (void)applyChangedOnlineState:(FSTOnlineState)onlineState { + self.onlineState = onlineState; + if (self.snapshot && !self.raisedInitialEvent && + [self shouldRaiseInitialEventForSnapshot:self.snapshot onlineState:onlineState]) { + [self raiseInitialEventForSnapshot:self.snapshot]; + } +} + +- (BOOL)shouldRaiseInitialEventForSnapshot:(FSTViewSnapshot *)snapshot + onlineState:(FSTOnlineState)onlineState { + FSTAssert(!self.raisedInitialEvent, + @"Determining whether to raise initial event, but already had first event."); + + // Always raise the first event when we're synced + if (!snapshot.fromCache) { + return YES; + } + + // NOTE: We consider OnlineState.Unknown as online (it should become Failed + // or Online if we wait long enough). + BOOL maybeOnline = onlineState != FSTOnlineStateFailed; + // Don't raise the event if we're online, aren't synced yet (checked + // above) and are waiting for a sync. + if (self.options.waitForSyncWhenOnline && maybeOnline) { + FSTAssert(snapshot.fromCache, @"Waiting for sync, but snapshot is not from cache."); + return NO; + } + + // Raise data from cache if we have any documents or we are offline + return !snapshot.documents.isEmpty || onlineState == FSTOnlineStateFailed; +} + +- (BOOL)shouldRaiseEventForSnapshot:(FSTViewSnapshot *)snapshot { + // We don't need to handle includeDocumentMetadataChanges here because the Metadata only changes + // have already been stripped out if needed. At this point the only changes we will see are the + // ones we should propagate. + if (snapshot.documentChanges.count > 0) { + return YES; + } + + BOOL hasPendingWritesChanged = + self.snapshot && self.snapshot.hasPendingWrites != snapshot.hasPendingWrites; + if (snapshot.syncStateChanged || hasPendingWritesChanged) { + return self.options.includeQueryMetadataChanges; + } + + // Generally we should have hit one of the cases above, but it's possible to get here if there + // were only metadata docChanges and they got stripped out. + return NO; +} + +- (void)raiseInitialEventForSnapshot:(FSTViewSnapshot *)snapshot { + FSTAssert(!self.raisedInitialEvent, @"Trying to raise initial events for second time"); + snapshot = [[FSTViewSnapshot alloc] + initWithQuery:snapshot.query + documents:snapshot.documents + oldDocuments:[FSTDocumentSet documentSetWithComparator:snapshot.query.comparator] + documentChanges:[FSTQueryListener getInitialViewChangesFor:snapshot] + fromCache:snapshot.fromCache + hasPendingWrites:snapshot.hasPendingWrites + syncStateChanged:YES]; + self.raisedInitialEvent = YES; + self.viewSnapshotHandler(snapshot, nil); +} + ++ (NSArray *)getInitialViewChangesFor:(FSTViewSnapshot *)snapshot { + NSMutableArray *result = [NSMutableArray array]; + for (FSTDocument *doc in snapshot.documents.documentEnumerator) { + [result addObject:[FSTDocumentViewChange changeWithDocument:doc + type:FSTDocumentViewChangeTypeAdded]]; + } + return result; +} + +@end + +#pragma mark - FSTEventManager + +@interface FSTEventManager () + +- (instancetype)initWithSyncEngine:(FSTSyncEngine *)syncEngine NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine; +@property(nonatomic, strong, readonly) + NSMutableDictionary *queries; +@property(nonatomic, assign) FSTOnlineState onlineState; + +@end + +@implementation FSTEventManager + ++ (instancetype)eventManagerWithSyncEngine:(FSTSyncEngine *)syncEngine { + return [[FSTEventManager alloc] initWithSyncEngine:syncEngine]; +} + +- (instancetype)initWithSyncEngine:(FSTSyncEngine *)syncEngine { + if (self = [super init]) { + _syncEngine = syncEngine; + _queries = [NSMutableDictionary dictionary]; + + _syncEngine.delegate = self; + } + return self; +} + +- (FSTTargetID)addListener:(FSTQueryListener *)listener { + FSTQuery *query = listener.query; + BOOL firstListen = NO; + + FSTQueryListenersInfo *queryInfo = self.queries[query]; + if (!queryInfo) { + firstListen = YES; + queryInfo = [[FSTQueryListenersInfo alloc] init]; + self.queries[query] = queryInfo; + } + [queryInfo.listeners addObject:listener]; + + [listener applyChangedOnlineState:self.onlineState]; + + if (queryInfo.viewSnapshot) { + [listener queryDidChangeViewSnapshot:queryInfo.viewSnapshot]; + } + + if (firstListen) { + queryInfo.targetID = [self.syncEngine listenToQuery:query]; + } + return queryInfo.targetID; +} + +- (void)removeListener:(FSTQueryListener *)listener { + FSTQuery *query = listener.query; + BOOL lastListen = NO; + + FSTQueryListenersInfo *queryInfo = self.queries[query]; + if (queryInfo) { + [queryInfo.listeners removeObject:listener]; + lastListen = (queryInfo.listeners.count == 0); + } + + if (lastListen) { + [self.queries removeObjectForKey:query]; + [self.syncEngine stopListeningToQuery:query]; + } +} + +- (void)handleViewSnapshots:(NSArray *)viewSnapshots { + for (FSTViewSnapshot *viewSnapshot in viewSnapshots) { + FSTQuery *query = viewSnapshot.query; + FSTQueryListenersInfo *queryInfo = self.queries[query]; + if (queryInfo) { + for (FSTQueryListener *listener in queryInfo.listeners) { + [listener queryDidChangeViewSnapshot:viewSnapshot]; + } + queryInfo.viewSnapshot = viewSnapshot; + } + } +} + +- (void)handleError:(NSError *)error forQuery:(FSTQuery *)query { + FSTQueryListenersInfo *queryInfo = self.queries[query]; + if (queryInfo) { + for (FSTQueryListener *listener in queryInfo.listeners) { + [listener queryDidError:error]; + } + } + + // Remove all listeners. NOTE: We don't need to call [FSTSyncEngine stopListening] after an error. + [self.queries removeObjectForKey:query]; +} + +- (void)applyChangedOnlineState:(FSTOnlineState)onlineState { + self.onlineState = onlineState; + for (FSTQueryListenersInfo *info in self.queries.objectEnumerator) { + for (FSTQueryListener *listener in info.listeners) { + [listener applyChangedOnlineState:onlineState]; + } + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTFirestoreClient.m b/Firestore/Source/Core/FSTFirestoreClient.m deleted file mode 100644 index fff644d..0000000 --- a/Firestore/Source/Core/FSTFirestoreClient.m +++ /dev/null @@ -1,298 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Core/FSTFirestoreClient.h" - -#import "Firestore/Source/Auth/FSTCredentialsProvider.h" -#import "Firestore/Source/Core/FSTDatabaseInfo.h" -#import "Firestore/Source/Core/FSTEventManager.h" -#import "Firestore/Source/Core/FSTSyncEngine.h" -#import "Firestore/Source/Core/FSTTransaction.h" -#import "Firestore/Source/Local/FSTEagerGarbageCollector.h" -#import "Firestore/Source/Local/FSTLevelDB.h" -#import "Firestore/Source/Local/FSTLocalSerializer.h" -#import "Firestore/Source/Local/FSTLocalStore.h" -#import "Firestore/Source/Local/FSTMemoryPersistence.h" -#import "Firestore/Source/Local/FSTNoOpGarbageCollector.h" -#import "Firestore/Source/Remote/FSTDatastore.h" -#import "Firestore/Source/Remote/FSTRemoteStore.h" -#import "Firestore/Source/Remote/FSTSerializerBeta.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTClasses.h" -#import "Firestore/Source/Util/FSTDispatchQueue.h" -#import "Firestore/Source/Util/FSTLogger.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTFirestoreClient () -- (instancetype)initWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo - usePersistence:(BOOL)usePersistence - credentialsProvider:(id)credentialsProvider - userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue - workerDispatchQueue:(FSTDispatchQueue *)queue NS_DESIGNATED_INITIALIZER; - -@property(nonatomic, strong, readonly) FSTDatabaseInfo *databaseInfo; -@property(nonatomic, strong, readonly) FSTEventManager *eventManager; -@property(nonatomic, strong, readonly) id persistence; -@property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine; -@property(nonatomic, strong, readonly) FSTRemoteStore *remoteStore; -@property(nonatomic, strong, readonly) FSTLocalStore *localStore; - -/** - * Dispatch queue responsible for all of our internal processing. When we get incoming work from - * the user (via public API) or the network (incoming GRPC messages), we should always dispatch - * onto this queue. This ensures our internal data structures are never accessed from multiple - * threads simultaneously. - */ -@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue; - -@property(nonatomic, strong, readonly) id credentialsProvider; - -@end - -@implementation FSTFirestoreClient - -+ (instancetype)clientWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo - usePersistence:(BOOL)usePersistence - credentialsProvider:(id)credentialsProvider - userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue { - return [[FSTFirestoreClient alloc] initWithDatabaseInfo:databaseInfo - usePersistence:usePersistence - credentialsProvider:credentialsProvider - userDispatchQueue:userDispatchQueue - workerDispatchQueue:workerDispatchQueue]; -} - -- (instancetype)initWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo - usePersistence:(BOOL)usePersistence - credentialsProvider:(id)credentialsProvider - userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue { - if (self = [super init]) { - _databaseInfo = databaseInfo; - _credentialsProvider = credentialsProvider; - _userDispatchQueue = userDispatchQueue; - _workerDispatchQueue = workerDispatchQueue; - - dispatch_semaphore_t initialUserAvailable = dispatch_semaphore_create(0); - __block FSTUser *initialUser; - FSTWeakify(self); - _credentialsProvider.userChangeListener = ^(FSTUser *user) { - FSTStrongify(self); - if (self) { - if (!initialUser) { - initialUser = user; - dispatch_semaphore_signal(initialUserAvailable); - } else { - [workerDispatchQueue dispatchAsync:^{ - [self userDidChange:user]; - }]; - } - } - }; - - // Defer initialization until we get the current user from the userChangeListener. This is - // guaranteed to be synchronously dispatched onto our worker queue, so we will be initialized - // before any subsequently queued work runs. - [_workerDispatchQueue dispatchAsync:^{ - dispatch_semaphore_wait(initialUserAvailable, DISPATCH_TIME_FOREVER); - - [self initializeWithUser:initialUser usePersistence:usePersistence]; - }]; - } - return self; -} - -- (void)initializeWithUser:(FSTUser *)user usePersistence:(BOOL)usePersistence { - // Do all of our initialization on our own dispatch queue. - [self.workerDispatchQueue verifyIsCurrentQueue]; - - // Note: The initialization work must all be synchronous (we can't dispatch more work) since - // external write/listen operations could get queued to run before that subsequent work - // completes. - id garbageCollector; - if (usePersistence) { - // TODO(http://b/33384523): For now we just disable garbage collection when persistence is - // enabled. - garbageCollector = [[FSTNoOpGarbageCollector alloc] init]; - - NSString *dir = [FSTLevelDB storageDirectoryForDatabaseInfo:self.databaseInfo - documentsDirectory:[FSTLevelDB documentsDirectory]]; - - FSTSerializerBeta *remoteSerializer = - [[FSTSerializerBeta alloc] initWithDatabaseID:self.databaseInfo.databaseID]; - FSTLocalSerializer *serializer = - [[FSTLocalSerializer alloc] initWithRemoteSerializer:remoteSerializer]; - - _persistence = [[FSTLevelDB alloc] initWithDirectory:dir serializer:serializer]; - } else { - garbageCollector = [[FSTEagerGarbageCollector alloc] init]; - _persistence = [FSTMemoryPersistence persistence]; - } - - NSError *error; - if (![_persistence start:&error]) { - // If local storage fails to start then just throw up our hands: the error is unrecoverable. - // There's nothing an end-user can do and nearly all failures indicate the developer is doing - // something grossly wrong so we should stop them cold in their tracks with a failure they - // can't ignore. - [NSException raise:NSInternalInconsistencyException format:@"Failed to open DB: %@", error]; - } - - _localStore = [[FSTLocalStore alloc] initWithPersistence:_persistence - garbageCollector:garbageCollector - initialUser:user]; - - FSTDatastore *datastore = [FSTDatastore datastoreWithDatabase:self.databaseInfo - workerDispatchQueue:self.workerDispatchQueue - credentials:self.credentialsProvider]; - - _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:datastore]; - - _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore - remoteStore:_remoteStore - initialUser:user]; - - _eventManager = [FSTEventManager eventManagerWithSyncEngine:_syncEngine]; - - // Setup wiring for remote store. - _remoteStore.syncEngine = _syncEngine; - - _remoteStore.onlineStateDelegate = self; - - // NOTE: RemoteStore depends on LocalStore (for persisting stream tokens, refilling mutation - // queue, etc.) so must be started after LocalStore. - [_localStore start]; - [_remoteStore start]; -} - -- (void)userDidChange:(FSTUser *)user { - [self.workerDispatchQueue verifyIsCurrentQueue]; - - FSTLog(@"User Changed: %@", user); - [self.syncEngine userDidChange:user]; -} - -- (void)applyChangedOnlineState:(FSTOnlineState)onlineState { - [self.syncEngine applyChangedOnlineState:onlineState]; - [self.eventManager applyChangedOnlineState:onlineState]; -} - -- (void)disableNetworkWithCompletion:(nullable FSTVoidErrorBlock)completion { - [self.workerDispatchQueue dispatchAsync:^{ - [self.remoteStore disableNetwork]; - if (completion) { - [self.userDispatchQueue dispatchAsync:^{ - completion(nil); - }]; - } - }]; -} - -- (void)enableNetworkWithCompletion:(nullable FSTVoidErrorBlock)completion { - [self.workerDispatchQueue dispatchAsync:^{ - [self.remoteStore enableNetwork]; - if (completion) { - [self.userDispatchQueue dispatchAsync:^{ - completion(nil); - }]; - } - }]; -} - -- (void)shutdownWithCompletion:(nullable FSTVoidErrorBlock)completion { - [self.workerDispatchQueue dispatchAsync:^{ - self.credentialsProvider.userChangeListener = nil; - - [self.remoteStore shutdown]; - [self.localStore shutdown]; - [self.persistence shutdown]; - if (completion) { - [self.userDispatchQueue dispatchAsync:^{ - completion(nil); - }]; - } - }]; -} - -- (FSTQueryListener *)listenToQuery:(FSTQuery *)query - options:(FSTListenOptions *)options - viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler { - FSTQueryListener *listener = [[FSTQueryListener alloc] initWithQuery:query - options:options - viewSnapshotHandler:viewSnapshotHandler]; - - [self.workerDispatchQueue dispatchAsync:^{ - [self.eventManager addListener:listener]; - }]; - - return listener; -} - -- (void)removeListener:(FSTQueryListener *)listener { - [self.workerDispatchQueue dispatchAsync:^{ - [self.eventManager removeListener:listener]; - }]; -} - -- (void)writeMutations:(NSArray *)mutations - completion:(nullable FSTVoidErrorBlock)completion { - [self.workerDispatchQueue dispatchAsync:^{ - if (mutations.count == 0) { - [self.userDispatchQueue dispatchAsync:^{ - completion(nil); - }]; - } else { - [self.syncEngine writeMutations:mutations - completion:^(NSError *error) { - // Dispatch the result back onto the user dispatch queue. - if (completion) { - [self.userDispatchQueue dispatchAsync:^{ - completion(error); - }]; - } - }]; - } - }]; -}; - -- (void)transactionWithRetries:(int)retries - updateBlock:(FSTTransactionBlock)updateBlock - completion:(FSTVoidIDErrorBlock)completion { - [self.workerDispatchQueue dispatchAsync:^{ - [self.syncEngine transactionWithRetries:retries - workerDispatchQueue:self.workerDispatchQueue - updateBlock:updateBlock - completion:^(id _Nullable result, NSError *_Nullable error) { - // Dispatch the result back onto the user dispatch queue. - if (completion) { - [self.userDispatchQueue dispatchAsync:^{ - completion(result, error); - }]; - } - }]; - - }]; -} - -- (FSTDatabaseID *)databaseID { - return self.databaseInfo.databaseID; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTFirestoreClient.mm b/Firestore/Source/Core/FSTFirestoreClient.mm new file mode 100644 index 0000000..fff644d --- /dev/null +++ b/Firestore/Source/Core/FSTFirestoreClient.mm @@ -0,0 +1,298 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Core/FSTFirestoreClient.h" + +#import "Firestore/Source/Auth/FSTCredentialsProvider.h" +#import "Firestore/Source/Core/FSTDatabaseInfo.h" +#import "Firestore/Source/Core/FSTEventManager.h" +#import "Firestore/Source/Core/FSTSyncEngine.h" +#import "Firestore/Source/Core/FSTTransaction.h" +#import "Firestore/Source/Local/FSTEagerGarbageCollector.h" +#import "Firestore/Source/Local/FSTLevelDB.h" +#import "Firestore/Source/Local/FSTLocalSerializer.h" +#import "Firestore/Source/Local/FSTLocalStore.h" +#import "Firestore/Source/Local/FSTMemoryPersistence.h" +#import "Firestore/Source/Local/FSTNoOpGarbageCollector.h" +#import "Firestore/Source/Remote/FSTDatastore.h" +#import "Firestore/Source/Remote/FSTRemoteStore.h" +#import "Firestore/Source/Remote/FSTSerializerBeta.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTClasses.h" +#import "Firestore/Source/Util/FSTDispatchQueue.h" +#import "Firestore/Source/Util/FSTLogger.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTFirestoreClient () +- (instancetype)initWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + usePersistence:(BOOL)usePersistence + credentialsProvider:(id)credentialsProvider + userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue + workerDispatchQueue:(FSTDispatchQueue *)queue NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FSTDatabaseInfo *databaseInfo; +@property(nonatomic, strong, readonly) FSTEventManager *eventManager; +@property(nonatomic, strong, readonly) id persistence; +@property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine; +@property(nonatomic, strong, readonly) FSTRemoteStore *remoteStore; +@property(nonatomic, strong, readonly) FSTLocalStore *localStore; + +/** + * Dispatch queue responsible for all of our internal processing. When we get incoming work from + * the user (via public API) or the network (incoming GRPC messages), we should always dispatch + * onto this queue. This ensures our internal data structures are never accessed from multiple + * threads simultaneously. + */ +@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue; + +@property(nonatomic, strong, readonly) id credentialsProvider; + +@end + +@implementation FSTFirestoreClient + ++ (instancetype)clientWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + usePersistence:(BOOL)usePersistence + credentialsProvider:(id)credentialsProvider + userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue { + return [[FSTFirestoreClient alloc] initWithDatabaseInfo:databaseInfo + usePersistence:usePersistence + credentialsProvider:credentialsProvider + userDispatchQueue:userDispatchQueue + workerDispatchQueue:workerDispatchQueue]; +} + +- (instancetype)initWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + usePersistence:(BOOL)usePersistence + credentialsProvider:(id)credentialsProvider + userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue { + if (self = [super init]) { + _databaseInfo = databaseInfo; + _credentialsProvider = credentialsProvider; + _userDispatchQueue = userDispatchQueue; + _workerDispatchQueue = workerDispatchQueue; + + dispatch_semaphore_t initialUserAvailable = dispatch_semaphore_create(0); + __block FSTUser *initialUser; + FSTWeakify(self); + _credentialsProvider.userChangeListener = ^(FSTUser *user) { + FSTStrongify(self); + if (self) { + if (!initialUser) { + initialUser = user; + dispatch_semaphore_signal(initialUserAvailable); + } else { + [workerDispatchQueue dispatchAsync:^{ + [self userDidChange:user]; + }]; + } + } + }; + + // Defer initialization until we get the current user from the userChangeListener. This is + // guaranteed to be synchronously dispatched onto our worker queue, so we will be initialized + // before any subsequently queued work runs. + [_workerDispatchQueue dispatchAsync:^{ + dispatch_semaphore_wait(initialUserAvailable, DISPATCH_TIME_FOREVER); + + [self initializeWithUser:initialUser usePersistence:usePersistence]; + }]; + } + return self; +} + +- (void)initializeWithUser:(FSTUser *)user usePersistence:(BOOL)usePersistence { + // Do all of our initialization on our own dispatch queue. + [self.workerDispatchQueue verifyIsCurrentQueue]; + + // Note: The initialization work must all be synchronous (we can't dispatch more work) since + // external write/listen operations could get queued to run before that subsequent work + // completes. + id garbageCollector; + if (usePersistence) { + // TODO(http://b/33384523): For now we just disable garbage collection when persistence is + // enabled. + garbageCollector = [[FSTNoOpGarbageCollector alloc] init]; + + NSString *dir = [FSTLevelDB storageDirectoryForDatabaseInfo:self.databaseInfo + documentsDirectory:[FSTLevelDB documentsDirectory]]; + + FSTSerializerBeta *remoteSerializer = + [[FSTSerializerBeta alloc] initWithDatabaseID:self.databaseInfo.databaseID]; + FSTLocalSerializer *serializer = + [[FSTLocalSerializer alloc] initWithRemoteSerializer:remoteSerializer]; + + _persistence = [[FSTLevelDB alloc] initWithDirectory:dir serializer:serializer]; + } else { + garbageCollector = [[FSTEagerGarbageCollector alloc] init]; + _persistence = [FSTMemoryPersistence persistence]; + } + + NSError *error; + if (![_persistence start:&error]) { + // If local storage fails to start then just throw up our hands: the error is unrecoverable. + // There's nothing an end-user can do and nearly all failures indicate the developer is doing + // something grossly wrong so we should stop them cold in their tracks with a failure they + // can't ignore. + [NSException raise:NSInternalInconsistencyException format:@"Failed to open DB: %@", error]; + } + + _localStore = [[FSTLocalStore alloc] initWithPersistence:_persistence + garbageCollector:garbageCollector + initialUser:user]; + + FSTDatastore *datastore = [FSTDatastore datastoreWithDatabase:self.databaseInfo + workerDispatchQueue:self.workerDispatchQueue + credentials:self.credentialsProvider]; + + _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:datastore]; + + _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore + remoteStore:_remoteStore + initialUser:user]; + + _eventManager = [FSTEventManager eventManagerWithSyncEngine:_syncEngine]; + + // Setup wiring for remote store. + _remoteStore.syncEngine = _syncEngine; + + _remoteStore.onlineStateDelegate = self; + + // NOTE: RemoteStore depends on LocalStore (for persisting stream tokens, refilling mutation + // queue, etc.) so must be started after LocalStore. + [_localStore start]; + [_remoteStore start]; +} + +- (void)userDidChange:(FSTUser *)user { + [self.workerDispatchQueue verifyIsCurrentQueue]; + + FSTLog(@"User Changed: %@", user); + [self.syncEngine userDidChange:user]; +} + +- (void)applyChangedOnlineState:(FSTOnlineState)onlineState { + [self.syncEngine applyChangedOnlineState:onlineState]; + [self.eventManager applyChangedOnlineState:onlineState]; +} + +- (void)disableNetworkWithCompletion:(nullable FSTVoidErrorBlock)completion { + [self.workerDispatchQueue dispatchAsync:^{ + [self.remoteStore disableNetwork]; + if (completion) { + [self.userDispatchQueue dispatchAsync:^{ + completion(nil); + }]; + } + }]; +} + +- (void)enableNetworkWithCompletion:(nullable FSTVoidErrorBlock)completion { + [self.workerDispatchQueue dispatchAsync:^{ + [self.remoteStore enableNetwork]; + if (completion) { + [self.userDispatchQueue dispatchAsync:^{ + completion(nil); + }]; + } + }]; +} + +- (void)shutdownWithCompletion:(nullable FSTVoidErrorBlock)completion { + [self.workerDispatchQueue dispatchAsync:^{ + self.credentialsProvider.userChangeListener = nil; + + [self.remoteStore shutdown]; + [self.localStore shutdown]; + [self.persistence shutdown]; + if (completion) { + [self.userDispatchQueue dispatchAsync:^{ + completion(nil); + }]; + } + }]; +} + +- (FSTQueryListener *)listenToQuery:(FSTQuery *)query + options:(FSTListenOptions *)options + viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler { + FSTQueryListener *listener = [[FSTQueryListener alloc] initWithQuery:query + options:options + viewSnapshotHandler:viewSnapshotHandler]; + + [self.workerDispatchQueue dispatchAsync:^{ + [self.eventManager addListener:listener]; + }]; + + return listener; +} + +- (void)removeListener:(FSTQueryListener *)listener { + [self.workerDispatchQueue dispatchAsync:^{ + [self.eventManager removeListener:listener]; + }]; +} + +- (void)writeMutations:(NSArray *)mutations + completion:(nullable FSTVoidErrorBlock)completion { + [self.workerDispatchQueue dispatchAsync:^{ + if (mutations.count == 0) { + [self.userDispatchQueue dispatchAsync:^{ + completion(nil); + }]; + } else { + [self.syncEngine writeMutations:mutations + completion:^(NSError *error) { + // Dispatch the result back onto the user dispatch queue. + if (completion) { + [self.userDispatchQueue dispatchAsync:^{ + completion(error); + }]; + } + }]; + } + }]; +}; + +- (void)transactionWithRetries:(int)retries + updateBlock:(FSTTransactionBlock)updateBlock + completion:(FSTVoidIDErrorBlock)completion { + [self.workerDispatchQueue dispatchAsync:^{ + [self.syncEngine transactionWithRetries:retries + workerDispatchQueue:self.workerDispatchQueue + updateBlock:updateBlock + completion:^(id _Nullable result, NSError *_Nullable error) { + // Dispatch the result back onto the user dispatch queue. + if (completion) { + [self.userDispatchQueue dispatchAsync:^{ + completion(result, error); + }]; + } + }]; + + }]; +} + +- (FSTDatabaseID *)databaseID { + return self.databaseInfo.databaseID; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTListenSequence.m b/Firestore/Source/Core/FSTListenSequence.m deleted file mode 100644 index 6f50d35..0000000 --- a/Firestore/Source/Core/FSTListenSequence.m +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2018 Google - * - * 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 "FSTListenSequence.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTListenSequence - -@interface FSTListenSequence () { - FSTListenSequenceNumber _previousSequenceNumber; -} - -@end - -@implementation FSTListenSequence - -#pragma mark - Constructors - -- (instancetype)initStartingAfter:(FSTListenSequenceNumber)after { - self = [super init]; - if (self) { - _previousSequenceNumber = after; - } - return self; -} - -#pragma mark - Public methods - -- (FSTListenSequenceNumber)next { - _previousSequenceNumber++; - return _previousSequenceNumber; -} - -@end - -NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/Firestore/Source/Core/FSTListenSequence.mm b/Firestore/Source/Core/FSTListenSequence.mm new file mode 100644 index 0000000..6f50d35 --- /dev/null +++ b/Firestore/Source/Core/FSTListenSequence.mm @@ -0,0 +1,50 @@ +/* + * Copyright 2018 Google + * + * 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 "FSTListenSequence.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTListenSequence + +@interface FSTListenSequence () { + FSTListenSequenceNumber _previousSequenceNumber; +} + +@end + +@implementation FSTListenSequence + +#pragma mark - Constructors + +- (instancetype)initStartingAfter:(FSTListenSequenceNumber)after { + self = [super init]; + if (self) { + _previousSequenceNumber = after; + } + return self; +} + +#pragma mark - Public methods + +- (FSTListenSequenceNumber)next { + _previousSequenceNumber++; + return _previousSequenceNumber; +} + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/Firestore/Source/Core/FSTQuery.m b/Firestore/Source/Core/FSTQuery.m deleted file mode 100644 index 13657f7..0000000 --- a/Firestore/Source/Core/FSTQuery.m +++ /dev/null @@ -1,759 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Core/FSTQuery.h" - -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTRelationFilterOperator functions - -NSString *FSTStringFromQueryRelationOperator(FSTRelationFilterOperator filterOperator) { - switch (filterOperator) { - case FSTRelationFilterOperatorLessThan: - return @"<"; - case FSTRelationFilterOperatorLessThanOrEqual: - return @"<="; - case FSTRelationFilterOperatorEqual: - return @"=="; - case FSTRelationFilterOperatorGreaterThanOrEqual: - return @">="; - case FSTRelationFilterOperatorGreaterThan: - return @">"; - default: - FSTCFail(@"Unknown FSTRelationFilterOperator %lu", (unsigned long)filterOperator); - } -} - -#pragma mark - FSTRelationFilter - -@interface FSTRelationFilter () - -/** - * Initializes the receiver relation filter. - * - * @param field A path to a field in the document to filter on. The LHS of the expression. - * @param filterOperator The binary operator to apply. - * @param value A constant value to compare @a field to. The RHS of the expression. - */ -- (instancetype)initWithField:(FSTFieldPath *)field - filterOperator:(FSTRelationFilterOperator)filterOperator - value:(FSTFieldValue *)value NS_DESIGNATED_INITIALIZER; - -/** Returns YES if @a document matches the receiver's constraint. */ -- (BOOL)matchesDocument:(FSTDocument *)document; - -/** - * A canonical string identifying the filter. Two different instances of equivalent filters will - * return the same canonicalID. - */ -- (NSString *)canonicalID; - -@end - -@implementation FSTRelationFilter - -#pragma mark - Constructor methods - -+ (instancetype)filterWithField:(FSTFieldPath *)field - filterOperator:(FSTRelationFilterOperator)filterOperator - value:(FSTFieldValue *)value { - return [[FSTRelationFilter alloc] initWithField:field filterOperator:filterOperator value:value]; -} - -- (instancetype)initWithField:(FSTFieldPath *)field - filterOperator:(FSTRelationFilterOperator)filterOperator - value:(FSTFieldValue *)value { - self = [super init]; - if (self) { - _field = field; - _filterOperator = filterOperator; - _value = value; - } - return self; -} - -#pragma mark - Public Methods - -- (BOOL)isInequality { - return self.filterOperator != FSTRelationFilterOperatorEqual; -} - -#pragma mark - NSObject methods - -- (NSString *)description { - return [NSString stringWithFormat:@"%@ %@ %@", [self.field canonicalString], - FSTStringFromQueryRelationOperator(self.filterOperator), - self.value]; -} - -- (BOOL)isEqual:(id)other { - if (self == other) { - return YES; - } - if (![other isKindOfClass:[FSTRelationFilter class]]) { - return NO; - } - return [self isEqualToFilter:(FSTRelationFilter *)other]; -} - -#pragma mark - Private methods - -- (BOOL)matchesDocument:(FSTDocument *)document { - if ([self.field isKeyFieldPath]) { - FSTAssert([self.value isKindOfClass:[FSTReferenceValue class]], - @"Comparing on key, but filter value not a FSTReferenceValue."); - FSTReferenceValue *refValue = (FSTReferenceValue *)self.value; - NSComparisonResult comparison = FSTDocumentKeyComparator(document.key, refValue.value); - return [self matchesComparison:comparison]; - } else { - return [self matchesValue:[document fieldForPath:self.field]]; - } -} - -- (NSString *)canonicalID { - // TODO(b/37283291): This should be collision robust and avoid relying on |description| methods. - return [NSString stringWithFormat:@"%@%@%@", [self.field canonicalString], - FSTStringFromQueryRelationOperator(self.filterOperator), - [self.value value]]; -} - -- (BOOL)isEqualToFilter:(FSTRelationFilter *)other { - if (self.filterOperator != other.filterOperator) { - return NO; - } - if (![self.field isEqual:other.field]) { - return NO; - } - if (![self.value isEqual:other.value]) { - return NO; - } - return YES; -} - -/** Returns YES if receiver is true with the given value as its LHS. */ -- (BOOL)matchesValue:(FSTFieldValue *)other { - // Only compare types with matching backend order (such as double and int). - return self.value.typeOrder == other.typeOrder && - [self matchesComparison:[other compare:self.value]]; -} - -- (BOOL)matchesComparison:(NSComparisonResult)comparison { - switch (self.filterOperator) { - case FSTRelationFilterOperatorLessThan: - return comparison == NSOrderedAscending; - case FSTRelationFilterOperatorLessThanOrEqual: - return comparison == NSOrderedAscending || comparison == NSOrderedSame; - case FSTRelationFilterOperatorEqual: - return comparison == NSOrderedSame; - case FSTRelationFilterOperatorGreaterThanOrEqual: - return comparison == NSOrderedDescending || comparison == NSOrderedSame; - case FSTRelationFilterOperatorGreaterThan: - return comparison == NSOrderedDescending; - default: - FSTFail(@"Unknown operator: %ld", (long)self.filterOperator); - } -} - -@end - -#pragma mark - FSTNullFilter - -@interface FSTNullFilter () -@property(nonatomic, strong, readonly) FSTFieldPath *field; -@end - -@implementation FSTNullFilter -- (instancetype)initWithField:(FSTFieldPath *)field { - if (self = [super init]) { - _field = field; - } - return self; -} - -- (BOOL)matchesDocument:(FSTDocument *)document { - FSTFieldValue *fieldValue = [document fieldForPath:self.field]; - return fieldValue != nil && [fieldValue isEqual:[FSTNullValue nullValue]]; -} - -- (NSString *)canonicalID { - return [NSString stringWithFormat:@"%@ IS NULL", [self.field canonicalString]]; -} - -- (NSString *)description { - return [self canonicalID]; -} - -- (BOOL)isEqual:(id)other { - if (other == self) return YES; - if (![[other class] isEqual:[self class]]) return NO; - - return [self.field isEqual:((FSTNullFilter *)other).field]; -} - -- (NSUInteger)hash { - return [self.field hash]; -} - -@end - -#pragma mark - FSTNanFilter - -@interface FSTNanFilter () -@property(nonatomic, strong, readonly) FSTFieldPath *field; -@end - -@implementation FSTNanFilter - -- (instancetype)initWithField:(FSTFieldPath *)field { - if (self = [super init]) { - _field = field; - } - return self; -} - -- (BOOL)matchesDocument:(FSTDocument *)document { - FSTFieldValue *fieldValue = [document fieldForPath:self.field]; - return fieldValue != nil && [fieldValue isEqual:[FSTDoubleValue nanValue]]; -} - -- (NSString *)canonicalID { - return [NSString stringWithFormat:@"%@ IS NaN", [self.field canonicalString]]; -} - -- (NSString *)description { - return [self canonicalID]; -} - -- (BOOL)isEqual:(id)other { - if (other == self) return YES; - if (![[other class] isEqual:[self class]]) return NO; - - return [self.field isEqual:((FSTNanFilter *)other).field]; -} - -- (NSUInteger)hash { - return [self.field hash]; -} -@end - -#pragma mark - FSTSortOrder - -@interface FSTSortOrder () - -/** Creates a new sort order with the given field and direction. */ -- (instancetype)initWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending; - -- (NSString *)canonicalID; - -@end - -@implementation FSTSortOrder - -#pragma mark - Constructor methods - -+ (instancetype)sortOrderWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending { - return [[FSTSortOrder alloc] initWithFieldPath:fieldPath ascending:ascending]; -} - -- (instancetype)initWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending { - self = [super init]; - if (self) { - _field = fieldPath; - _ascending = ascending; - } - return self; -} - -#pragma mark - Public methods - -- (NSComparisonResult)compareDocument:(FSTDocument *)document1 toDocument:(FSTDocument *)document2 { - int modifier = self.isAscending ? 1 : -1; - if ([self.field isEqual:[FSTFieldPath keyFieldPath]]) { - return (NSComparisonResult)(modifier * FSTDocumentKeyComparator(document1.key, document2.key)); - } else { - FSTFieldValue *value1 = [document1 fieldForPath:self.field]; - FSTFieldValue *value2 = [document2 fieldForPath:self.field]; - FSTAssert(value1 != nil && value2 != nil, - @"Trying to compare documents on fields that don't exist."); - return modifier * [value1 compare:value2]; - } -} - -- (NSString *)canonicalID { - return [NSString - stringWithFormat:@"%@%@", self.field.canonicalString, self.isAscending ? @"asc" : @"desc"]; -} - -- (BOOL)isEqualToSortOrder:(FSTSortOrder *)other { - return [self.field isEqual:other.field] && self.isAscending == other.isAscending; -} - -#pragma mark - NSObject methods - -- (NSString *)description { - return [NSString stringWithFormat:@"", self.field, - self.ascending ? @"asc" : @"desc"]; -} - -- (BOOL)isEqual:(NSObject *)other { - if (self == other) { - return YES; - } - if (![other isKindOfClass:[FSTSortOrder class]]) { - return NO; - } - return [self isEqualToSortOrder:(FSTSortOrder *)other]; -} - -- (NSUInteger)hash { - return [self.canonicalID hash]; -} - -- (instancetype)copyWithZone:(nullable NSZone *)zone { - return self; -} - -@end - -#pragma mark - FSTBound - -@implementation FSTBound - -- (instancetype)initWithPosition:(NSArray *)position isBefore:(BOOL)isBefore { - if (self = [super init]) { - _position = position; - _before = isBefore; - } - return self; -} - -+ (instancetype)boundWithPosition:(NSArray *)position isBefore:(BOOL)isBefore { - return [[FSTBound alloc] initWithPosition:position isBefore:isBefore]; -} - -- (NSString *)canonicalString { - // TODO(b/29183165): Make this collision robust. - NSMutableString *string = [NSMutableString string]; - if (self.isBefore) { - [string appendString:@"b:"]; - } else { - [string appendString:@"a:"]; - } - for (FSTFieldValue *component in self.position) { - [string appendFormat:@"%@", component]; - } - return string; -} - -- (BOOL)sortsBeforeDocument:(FSTDocument *)document - usingSortOrder:(NSArray *)sortOrder { - FSTAssert(self.position.count <= sortOrder.count, - @"FSTIndexPosition has more components than provided sort order."); - __block NSComparisonResult result = NSOrderedSame; - [self.position enumerateObjectsUsingBlock:^(FSTFieldValue *fieldValue, NSUInteger idx, - BOOL *stop) { - FSTSortOrder *sortOrderComponent = sortOrder[idx]; - NSComparisonResult comparison; - if ([sortOrderComponent.field isEqual:[FSTFieldPath keyFieldPath]]) { - FSTAssert([fieldValue isKindOfClass:[FSTReferenceValue class]], - @"FSTBound has a non-key value where the key path is being used %@", fieldValue); - comparison = [fieldValue.value compare:document.key]; - } else { - FSTFieldValue *docValue = [document fieldForPath:sortOrderComponent.field]; - FSTAssert(docValue != nil, @"Field should exist since document matched the orderBy already."); - comparison = [fieldValue compare:docValue]; - } - - if (!sortOrderComponent.isAscending) { - comparison = comparison * -1; - } - - if (comparison != 0) { - result = comparison; - *stop = YES; - } - }]; - - return self.isBefore ? result <= NSOrderedSame : result < NSOrderedSame; -} - -#pragma mark - NSObject methods - -- (NSString *)description { - return [NSString stringWithFormat:@"", self.position, - self.isBefore ? @"YES" : @"NO"]; -} - -- (BOOL)isEqual:(NSObject *)other { - if (self == other) { - return YES; - } - if (![other isKindOfClass:[FSTBound class]]) { - return NO; - } - - FSTBound *otherBound = (FSTBound *)other; - - return [self.position isEqualToArray:otherBound.position] && self.isBefore == otherBound.isBefore; -} - -- (NSUInteger)hash { - return 31 * self.position.hash + (self.isBefore ? 0 : 1); -} - -- (instancetype)copyWithZone:(nullable NSZone *)zone { - return self; -} - -@end - -#pragma mark - FSTQuery - -@interface FSTQuery () { - // Cached value of the canonicalID property. - NSString *_canonicalID; -} - -/** - * Initializes the receiver with the given query constraints. - * - * @param path The base path of the query. - * @param filters Filters specify which documents to include in the results. - * @param sortOrders The fields and directions to sort the results. - * @param limit If not NSNotFound, only this many results will be returned. - */ -- (instancetype)initWithPath:(FSTResourcePath *)path - filterBy:(NSArray> *)filters - orderBy:(NSArray *)sortOrders - limit:(NSInteger)limit - startAt:(nullable FSTBound *)startAtBound - endAt:(nullable FSTBound *)endAtBound NS_DESIGNATED_INITIALIZER; - -/** A list of fields given to sort by. This does not include the implicit key sort at the end. */ -@property(nonatomic, strong, readonly) NSArray *explicitSortOrders; - -/** The memoized list of sort orders */ -@property(nonatomic, nullable, strong, readwrite) NSArray *memoizedSortOrders; - -@end - -@implementation FSTQuery - -#pragma mark - Constructors - -+ (instancetype)queryWithPath:(FSTResourcePath *)path { - return [[FSTQuery alloc] initWithPath:path - filterBy:@[] - orderBy:@[] - limit:NSNotFound - startAt:nil - endAt:nil]; -} - -- (instancetype)initWithPath:(FSTResourcePath *)path - filterBy:(NSArray> *)filters - orderBy:(NSArray *)sortOrders - limit:(NSInteger)limit - startAt:(nullable FSTBound *)startAtBound - endAt:(nullable FSTBound *)endAtBound { - if (self = [super init]) { - _path = path; - _filters = filters; - _explicitSortOrders = sortOrders; - _limit = limit; - _startAt = startAtBound; - _endAt = endAtBound; - } - return self; -} - -#pragma mark - NSObject methods - -- (NSString *)description { - return [NSString stringWithFormat:@"", self.canonicalID]; -} - -- (BOOL)isEqual:(id)object { - if (self == object) { - return YES; - } - if (![object isKindOfClass:[FSTQuery class]]) { - return NO; - } - return [self isEqualToQuery:(FSTQuery *)object]; -} - -- (NSUInteger)hash { - return [self.canonicalID hash]; -} - -- (instancetype)copyWithZone:(nullable NSZone *)zone { - return self; -} - -#pragma mark - Public methods - -- (NSArray *)sortOrders { - if (self.memoizedSortOrders == nil) { - FSTFieldPath *_Nullable inequalityField = [self inequalityFilterField]; - FSTFieldPath *_Nullable firstSortOrderField = [self firstSortOrderField]; - if (inequalityField && !firstSortOrderField) { - // In order to implicitly add key ordering, we must also add the inequality filter field for - // it to be a valid query. Note that the default inequality field and key ordering is - // ascending. - if ([inequalityField isKeyFieldPath]) { - self.memoizedSortOrders = - @[ [FSTSortOrder sortOrderWithFieldPath:[FSTFieldPath keyFieldPath] ascending:YES] ]; - } else { - self.memoizedSortOrders = @[ - [FSTSortOrder sortOrderWithFieldPath:inequalityField ascending:YES], - [FSTSortOrder sortOrderWithFieldPath:[FSTFieldPath keyFieldPath] ascending:YES] - ]; - } - } else { - FSTAssert(!inequalityField || [inequalityField isEqual:firstSortOrderField], - @"First orderBy %@ should match inequality field %@.", firstSortOrderField, - inequalityField); - - __block BOOL foundKeyOrder = NO; - - NSMutableArray *result = [NSMutableArray array]; - for (FSTSortOrder *sortOrder in self.explicitSortOrders) { - [result addObject:sortOrder]; - if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) { - foundKeyOrder = YES; - } - } - - if (!foundKeyOrder) { - // The direction of the implicit key ordering always matches the direction of the last - // explicit sort order - BOOL lastIsAscending = - self.explicitSortOrders.count > 0 ? self.explicitSortOrders.lastObject.ascending : YES; - [result addObject:[FSTSortOrder sortOrderWithFieldPath:[FSTFieldPath keyFieldPath] - ascending:lastIsAscending]]; - } - - self.memoizedSortOrders = result; - } - } - return self.memoizedSortOrders; -} - -- (instancetype)queryByAddingFilter:(id)filter { - FSTAssert(![FSTDocumentKey isDocumentKey:self.path], @"No filtering allowed for document query"); - - FSTFieldPath *_Nullable newInequalityField = nil; - if ([filter isKindOfClass:[FSTRelationFilter class]] && - [((FSTRelationFilter *)filter)isInequality]) { - newInequalityField = filter.field; - } - FSTFieldPath *_Nullable queryInequalityField = [self inequalityFilterField]; - FSTAssert(!queryInequalityField || !newInequalityField || - [queryInequalityField isEqual:newInequalityField], - @"Query must only have one inequality field."); - - return [[FSTQuery alloc] initWithPath:self.path - filterBy:[self.filters arrayByAddingObject:filter] - orderBy:self.explicitSortOrders - limit:self.limit - startAt:self.startAt - endAt:self.endAt]; -} - -- (instancetype)queryByAddingSortOrder:(FSTSortOrder *)sortOrder { - FSTAssert(![FSTDocumentKey isDocumentKey:self.path], - @"No ordering is allowed for a document query."); - - // TODO(klimt): Validate that the same key isn't added twice. - return [[FSTQuery alloc] initWithPath:self.path - filterBy:self.filters - orderBy:[self.explicitSortOrders arrayByAddingObject:sortOrder] - limit:self.limit - startAt:self.startAt - endAt:self.endAt]; -} - -- (instancetype)queryBySettingLimit:(NSInteger)limit { - return [[FSTQuery alloc] initWithPath:self.path - filterBy:self.filters - orderBy:self.explicitSortOrders - limit:limit - startAt:self.startAt - endAt:self.endAt]; -} - -- (instancetype)queryByAddingStartAt:(FSTBound *)bound { - return [[FSTQuery alloc] initWithPath:self.path - filterBy:self.filters - orderBy:self.explicitSortOrders - limit:self.limit - startAt:bound - endAt:self.endAt]; -} - -- (instancetype)queryByAddingEndAt:(FSTBound *)bound { - return [[FSTQuery alloc] initWithPath:self.path - filterBy:self.filters - orderBy:self.explicitSortOrders - limit:self.limit - startAt:self.startAt - endAt:bound]; -} - -- (BOOL)isDocumentQuery { - return [FSTDocumentKey isDocumentKey:self.path] && self.filters.count == 0; -} - -- (BOOL)matchesDocument:(FSTDocument *)document { - return [self pathMatchesDocument:document] && [self orderByMatchesDocument:document] && - [self filtersMatchDocument:document] && [self boundsMatchDocument:document]; -} - -- (NSComparator)comparator { - return ^NSComparisonResult(id document1, id document2) { - BOOL didCompareOnKeyField = NO; - for (FSTSortOrder *orderBy in self.sortOrders) { - NSComparisonResult comp = [orderBy compareDocument:document1 toDocument:document2]; - if (comp != NSOrderedSame) { - return comp; - } - didCompareOnKeyField = - didCompareOnKeyField || [orderBy.field isEqual:[FSTFieldPath keyFieldPath]]; - } - FSTAssert(didCompareOnKeyField, @"sortOrder of query did not include key ordering"); - return NSOrderedSame; - }; -} - -- (FSTFieldPath *_Nullable)inequalityFilterField { - for (id filter in self.filters) { - if ([filter isKindOfClass:[FSTRelationFilter class]] && - ((FSTRelationFilter *)filter).filterOperator != FSTRelationFilterOperatorEqual) { - return filter.field; - } - } - return nil; -} - -- (FSTFieldPath *_Nullable)firstSortOrderField { - return self.explicitSortOrders.firstObject.field; -} - -#pragma mark - Private properties - -- (NSString *)canonicalID { - if (_canonicalID) { - return _canonicalID; - } - - NSMutableString *canonicalID = [[self.path canonicalString] mutableCopy]; - - // Add filters. - [canonicalID appendString:@"|f:"]; - for (id predicate in self.filters) { - [canonicalID appendFormat:@"%@", [predicate canonicalID]]; - } - - // Add order by. - [canonicalID appendString:@"|ob:"]; - for (FSTSortOrder *orderBy in self.sortOrders) { - [canonicalID appendString:orderBy.canonicalID]; - } - - // Add limit. - if (self.limit != NSNotFound) { - [canonicalID appendFormat:@"|l:%ld", (long)self.limit]; - } - - if (self.startAt) { - [canonicalID appendFormat:@"|lb:%@", self.startAt.canonicalString]; - } - - if (self.endAt) { - [canonicalID appendFormat:@"|ub:%@", self.endAt.canonicalString]; - } - - _canonicalID = canonicalID; - return canonicalID; -} - -#pragma mark - Private methods - -- (BOOL)isEqualToQuery:(FSTQuery *)other { - return [self.path isEqual:other.path] && self.limit == other.limit && - [self.filters isEqual:other.filters] && [self.sortOrders isEqual:other.sortOrders] && - (self.startAt == other.startAt || [self.startAt isEqual:other.startAt]) && - (self.endAt == other.endAt || [self.endAt isEqual:other.endAt]); -} - -/* Returns YES if the document matches the path for the receiver. */ -- (BOOL)pathMatchesDocument:(FSTDocument *)document { - FSTResourcePath *documentPath = document.key.path; - if ([FSTDocumentKey isDocumentKey:self.path]) { - // Exact match for document queries. - return [self.path isEqual:documentPath]; - } else { - // Shallow ancestor queries by default. - return [self.path isPrefixOfPath:documentPath] && self.path.length == documentPath.length - 1; - } -} - -/** - * A document must have a value for every ordering clause in order to show up in the results. - */ -- (BOOL)orderByMatchesDocument:(FSTDocument *)document { - for (FSTSortOrder *orderBy in self.explicitSortOrders) { - FSTFieldPath *fieldPath = orderBy.field; - // order by key always matches - if (![fieldPath isEqual:[FSTFieldPath keyFieldPath]] && - [document fieldForPath:fieldPath] == nil) { - return NO; - } - } - return YES; -} - -/** Returns YES if the document matches all of the filters in the receiver. */ -- (BOOL)filtersMatchDocument:(FSTDocument *)document { - for (id filter in self.filters) { - if (![filter matchesDocument:document]) { - return NO; - } - } - return YES; -} - -- (BOOL)boundsMatchDocument:(FSTDocument *)document { - if (self.startAt && ![self.startAt sortsBeforeDocument:document usingSortOrder:self.sortOrders]) { - return NO; - } - if (self.endAt && [self.endAt sortsBeforeDocument:document usingSortOrder:self.sortOrders]) { - return NO; - } - return YES; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTQuery.mm b/Firestore/Source/Core/FSTQuery.mm new file mode 100644 index 0000000..8c98687 --- /dev/null +++ b/Firestore/Source/Core/FSTQuery.mm @@ -0,0 +1,771 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Core/FSTQuery.h" + +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTRelationFilterOperator functions + +/** + * Returns the reverse order (i.e. Ascending => Descending) etc. + */ +static constexpr NSComparisonResult ReverseOrder(NSComparisonResult result) { + return static_cast(-static_cast(result)); +} + +NSString *FSTStringFromQueryRelationOperator(FSTRelationFilterOperator filterOperator) { + switch (filterOperator) { + case FSTRelationFilterOperatorLessThan: + return @"<"; + case FSTRelationFilterOperatorLessThanOrEqual: + return @"<="; + case FSTRelationFilterOperatorEqual: + return @"=="; + case FSTRelationFilterOperatorGreaterThanOrEqual: + return @">="; + case FSTRelationFilterOperatorGreaterThan: + return @">"; + default: + FSTCFail(@"Unknown FSTRelationFilterOperator %lu", (unsigned long)filterOperator); + } +} + +#pragma mark - FSTRelationFilter + +@interface FSTRelationFilter () + +/** + * Initializes the receiver relation filter. + * + * @param field A path to a field in the document to filter on. The LHS of the expression. + * @param filterOperator The binary operator to apply. + * @param value A constant value to compare @a field to. The RHS of the expression. + */ +- (instancetype)initWithField:(FSTFieldPath *)field + filterOperator:(FSTRelationFilterOperator)filterOperator + value:(FSTFieldValue *)value NS_DESIGNATED_INITIALIZER; + +/** Returns YES if @a document matches the receiver's constraint. */ +- (BOOL)matchesDocument:(FSTDocument *)document; + +/** + * A canonical string identifying the filter. Two different instances of equivalent filters will + * return the same canonicalID. + */ +- (NSString *)canonicalID; + +@end + +@implementation FSTRelationFilter + +#pragma mark - Constructor methods + ++ (instancetype)filterWithField:(FSTFieldPath *)field + filterOperator:(FSTRelationFilterOperator)filterOperator + value:(FSTFieldValue *)value { + return [[FSTRelationFilter alloc] initWithField:field filterOperator:filterOperator value:value]; +} + +- (instancetype)initWithField:(FSTFieldPath *)field + filterOperator:(FSTRelationFilterOperator)filterOperator + value:(FSTFieldValue *)value { + self = [super init]; + if (self) { + _field = field; + _filterOperator = filterOperator; + _value = value; + } + return self; +} + +#pragma mark - Public Methods + +- (BOOL)isInequality { + return self.filterOperator != FSTRelationFilterOperatorEqual; +} + +#pragma mark - NSObject methods + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ %@ %@", [self.field canonicalString], + FSTStringFromQueryRelationOperator(self.filterOperator), + self.value]; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } + if (![other isKindOfClass:[FSTRelationFilter class]]) { + return NO; + } + return [self isEqualToFilter:(FSTRelationFilter *)other]; +} + +#pragma mark - Private methods + +- (BOOL)matchesDocument:(FSTDocument *)document { + if ([self.field isKeyFieldPath]) { + FSTAssert([self.value isKindOfClass:[FSTReferenceValue class]], + @"Comparing on key, but filter value not a FSTReferenceValue."); + FSTReferenceValue *refValue = (FSTReferenceValue *)self.value; + NSComparisonResult comparison = FSTDocumentKeyComparator(document.key, refValue.value); + return [self matchesComparison:comparison]; + } else { + return [self matchesValue:[document fieldForPath:self.field]]; + } +} + +- (NSString *)canonicalID { + // TODO(b/37283291): This should be collision robust and avoid relying on |description| methods. + return [NSString stringWithFormat:@"%@%@%@", [self.field canonicalString], + FSTStringFromQueryRelationOperator(self.filterOperator), + [self.value value]]; +} + +- (BOOL)isEqualToFilter:(FSTRelationFilter *)other { + if (self.filterOperator != other.filterOperator) { + return NO; + } + if (![self.field isEqual:other.field]) { + return NO; + } + if (![self.value isEqual:other.value]) { + return NO; + } + return YES; +} + +/** Returns YES if receiver is true with the given value as its LHS. */ +- (BOOL)matchesValue:(FSTFieldValue *)other { + // Only compare types with matching backend order (such as double and int). + return self.value.typeOrder == other.typeOrder && + [self matchesComparison:[other compare:self.value]]; +} + +- (BOOL)matchesComparison:(NSComparisonResult)comparison { + switch (self.filterOperator) { + case FSTRelationFilterOperatorLessThan: + return comparison == NSOrderedAscending; + case FSTRelationFilterOperatorLessThanOrEqual: + return comparison == NSOrderedAscending || comparison == NSOrderedSame; + case FSTRelationFilterOperatorEqual: + return comparison == NSOrderedSame; + case FSTRelationFilterOperatorGreaterThanOrEqual: + return comparison == NSOrderedDescending || comparison == NSOrderedSame; + case FSTRelationFilterOperatorGreaterThan: + return comparison == NSOrderedDescending; + default: + FSTFail(@"Unknown operator: %ld", (long)self.filterOperator); + } +} + +@end + +#pragma mark - FSTNullFilter + +@interface FSTNullFilter () +@property(nonatomic, strong, readonly) FSTFieldPath *field; +@end + +@implementation FSTNullFilter +- (instancetype)initWithField:(FSTFieldPath *)field { + if (self = [super init]) { + _field = field; + } + return self; +} + +- (BOOL)matchesDocument:(FSTDocument *)document { + FSTFieldValue *fieldValue = [document fieldForPath:self.field]; + return fieldValue != nil && [fieldValue isEqual:[FSTNullValue nullValue]]; +} + +- (NSString *)canonicalID { + return [NSString stringWithFormat:@"%@ IS NULL", [self.field canonicalString]]; +} + +- (NSString *)description { + return [self canonicalID]; +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + return [self.field isEqual:((FSTNullFilter *)other).field]; +} + +- (NSUInteger)hash { + return [self.field hash]; +} + +@end + +#pragma mark - FSTNanFilter + +@interface FSTNanFilter () +@property(nonatomic, strong, readonly) FSTFieldPath *field; +@end + +@implementation FSTNanFilter + +- (instancetype)initWithField:(FSTFieldPath *)field { + if (self = [super init]) { + _field = field; + } + return self; +} + +- (BOOL)matchesDocument:(FSTDocument *)document { + FSTFieldValue *fieldValue = [document fieldForPath:self.field]; + return fieldValue != nil && [fieldValue isEqual:[FSTDoubleValue nanValue]]; +} + +- (NSString *)canonicalID { + return [NSString stringWithFormat:@"%@ IS NaN", [self.field canonicalString]]; +} + +- (NSString *)description { + return [self canonicalID]; +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + return [self.field isEqual:((FSTNanFilter *)other).field]; +} + +- (NSUInteger)hash { + return [self.field hash]; +} +@end + +#pragma mark - FSTSortOrder + +@interface FSTSortOrder () + +/** Creates a new sort order with the given field and direction. */ +- (instancetype)initWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending; + +- (NSString *)canonicalID; + +@end + +@implementation FSTSortOrder + +#pragma mark - Constructor methods + ++ (instancetype)sortOrderWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending { + return [[FSTSortOrder alloc] initWithFieldPath:fieldPath ascending:ascending]; +} + +- (instancetype)initWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending { + self = [super init]; + if (self) { + _field = fieldPath; + _ascending = ascending; + } + return self; +} + +#pragma mark - Public methods + +- (NSComparisonResult)compareDocument:(FSTDocument *)document1 toDocument:(FSTDocument *)document2 { + NSComparisonResult result; + if ([self.field isEqual:[FSTFieldPath keyFieldPath]]) { + result = FSTDocumentKeyComparator(document1.key, document2.key); + } else { + FSTFieldValue *value1 = [document1 fieldForPath:self.field]; + FSTFieldValue *value2 = [document2 fieldForPath:self.field]; + FSTAssert(value1 != nil && value2 != nil, + @"Trying to compare documents on fields that don't exist."); + result = [value1 compare:value2]; + } + if (!self.isAscending) { + result = ReverseOrder(result); + } + return result; +} + +- (NSString *)canonicalID { + return [NSString + stringWithFormat:@"%@%@", self.field.canonicalString, self.isAscending ? @"asc" : @"desc"]; +} + +- (BOOL)isEqualToSortOrder:(FSTSortOrder *)other { + return [self.field isEqual:other.field] && self.isAscending == other.isAscending; +} + +#pragma mark - NSObject methods + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.field, + self.ascending ? @"asc" : @"desc"]; +} + +- (BOOL)isEqual:(NSObject *)other { + if (self == other) { + return YES; + } + if (![other isKindOfClass:[FSTSortOrder class]]) { + return NO; + } + return [self isEqualToSortOrder:(FSTSortOrder *)other]; +} + +- (NSUInteger)hash { + return [self.canonicalID hash]; +} + +- (instancetype)copyWithZone:(nullable NSZone *)zone { + return self; +} + +@end + +#pragma mark - FSTBound + +@implementation FSTBound + +- (instancetype)initWithPosition:(NSArray *)position isBefore:(BOOL)isBefore { + if (self = [super init]) { + _position = position; + _before = isBefore; + } + return self; +} + ++ (instancetype)boundWithPosition:(NSArray *)position isBefore:(BOOL)isBefore { + return [[FSTBound alloc] initWithPosition:position isBefore:isBefore]; +} + +- (NSString *)canonicalString { + // TODO(b/29183165): Make this collision robust. + NSMutableString *string = [NSMutableString string]; + if (self.isBefore) { + [string appendString:@"b:"]; + } else { + [string appendString:@"a:"]; + } + for (FSTFieldValue *component in self.position) { + [string appendFormat:@"%@", component]; + } + return string; +} + +- (BOOL)sortsBeforeDocument:(FSTDocument *)document + usingSortOrder:(NSArray *)sortOrder { + FSTAssert(self.position.count <= sortOrder.count, + @"FSTIndexPosition has more components than provided sort order."); + __block NSComparisonResult result = NSOrderedSame; + [self.position enumerateObjectsUsingBlock:^(FSTFieldValue *fieldValue, NSUInteger idx, + BOOL *stop) { + FSTSortOrder *sortOrderComponent = sortOrder[idx]; + NSComparisonResult comparison; + if ([sortOrderComponent.field isEqual:[FSTFieldPath keyFieldPath]]) { + FSTAssert([fieldValue isKindOfClass:[FSTReferenceValue class]], + @"FSTBound has a non-key value where the key path is being used %@", fieldValue); + FSTReferenceValue *refValue = (FSTReferenceValue *)fieldValue; + comparison = [refValue.value compare:document.key]; + } else { + FSTFieldValue *docValue = [document fieldForPath:sortOrderComponent.field]; + FSTAssert(docValue != nil, @"Field should exist since document matched the orderBy already."); + comparison = [fieldValue compare:docValue]; + } + + if (!sortOrderComponent.isAscending) { + comparison = ReverseOrder(comparison); + } + + if (comparison != 0) { + result = comparison; + *stop = YES; + } + }]; + + return self.isBefore ? result <= NSOrderedSame : result < NSOrderedSame; +} + +#pragma mark - NSObject methods + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.position, + self.isBefore ? @"YES" : @"NO"]; +} + +- (BOOL)isEqual:(NSObject *)other { + if (self == other) { + return YES; + } + if (![other isKindOfClass:[FSTBound class]]) { + return NO; + } + + FSTBound *otherBound = (FSTBound *)other; + + return [self.position isEqualToArray:otherBound.position] && self.isBefore == otherBound.isBefore; +} + +- (NSUInteger)hash { + return 31 * self.position.hash + (self.isBefore ? 0 : 1); +} + +- (instancetype)copyWithZone:(nullable NSZone *)zone { + return self; +} + +@end + +#pragma mark - FSTQuery + +@interface FSTQuery () { + // Cached value of the canonicalID property. + NSString *_canonicalID; +} + +/** + * Initializes the receiver with the given query constraints. + * + * @param path The base path of the query. + * @param filters Filters specify which documents to include in the results. + * @param sortOrders The fields and directions to sort the results. + * @param limit If not NSNotFound, only this many results will be returned. + */ +- (instancetype)initWithPath:(FSTResourcePath *)path + filterBy:(NSArray> *)filters + orderBy:(NSArray *)sortOrders + limit:(NSInteger)limit + startAt:(nullable FSTBound *)startAtBound + endAt:(nullable FSTBound *)endAtBound NS_DESIGNATED_INITIALIZER; + +/** A list of fields given to sort by. This does not include the implicit key sort at the end. */ +@property(nonatomic, strong, readonly) NSArray *explicitSortOrders; + +/** The memoized list of sort orders */ +@property(nonatomic, nullable, strong, readwrite) NSArray *memoizedSortOrders; + +@end + +@implementation FSTQuery + +#pragma mark - Constructors + ++ (instancetype)queryWithPath:(FSTResourcePath *)path { + return [[FSTQuery alloc] initWithPath:path + filterBy:@[] + orderBy:@[] + limit:NSNotFound + startAt:nil + endAt:nil]; +} + +- (instancetype)initWithPath:(FSTResourcePath *)path + filterBy:(NSArray> *)filters + orderBy:(NSArray *)sortOrders + limit:(NSInteger)limit + startAt:(nullable FSTBound *)startAtBound + endAt:(nullable FSTBound *)endAtBound { + if (self = [super init]) { + _path = path; + _filters = filters; + _explicitSortOrders = sortOrders; + _limit = limit; + _startAt = startAtBound; + _endAt = endAtBound; + } + return self; +} + +#pragma mark - NSObject methods + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.canonicalID]; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + if (![object isKindOfClass:[FSTQuery class]]) { + return NO; + } + return [self isEqualToQuery:(FSTQuery *)object]; +} + +- (NSUInteger)hash { + return [self.canonicalID hash]; +} + +- (instancetype)copyWithZone:(nullable NSZone *)zone { + return self; +} + +#pragma mark - Public methods + +- (NSArray *)sortOrders { + if (self.memoizedSortOrders == nil) { + FSTFieldPath *_Nullable inequalityField = [self inequalityFilterField]; + FSTFieldPath *_Nullable firstSortOrderField = [self firstSortOrderField]; + if (inequalityField && !firstSortOrderField) { + // In order to implicitly add key ordering, we must also add the inequality filter field for + // it to be a valid query. Note that the default inequality field and key ordering is + // ascending. + if ([inequalityField isKeyFieldPath]) { + self.memoizedSortOrders = + @[ [FSTSortOrder sortOrderWithFieldPath:[FSTFieldPath keyFieldPath] ascending:YES] ]; + } else { + self.memoizedSortOrders = @[ + [FSTSortOrder sortOrderWithFieldPath:inequalityField ascending:YES], + [FSTSortOrder sortOrderWithFieldPath:[FSTFieldPath keyFieldPath] ascending:YES] + ]; + } + } else { + FSTAssert(!inequalityField || [inequalityField isEqual:firstSortOrderField], + @"First orderBy %@ should match inequality field %@.", firstSortOrderField, + inequalityField); + + __block BOOL foundKeyOrder = NO; + + NSMutableArray *result = [NSMutableArray array]; + for (FSTSortOrder *sortOrder in self.explicitSortOrders) { + [result addObject:sortOrder]; + if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) { + foundKeyOrder = YES; + } + } + + if (!foundKeyOrder) { + // The direction of the implicit key ordering always matches the direction of the last + // explicit sort order + BOOL lastIsAscending = + self.explicitSortOrders.count > 0 ? self.explicitSortOrders.lastObject.ascending : YES; + [result addObject:[FSTSortOrder sortOrderWithFieldPath:[FSTFieldPath keyFieldPath] + ascending:lastIsAscending]]; + } + + self.memoizedSortOrders = result; + } + } + return self.memoizedSortOrders; +} + +- (instancetype)queryByAddingFilter:(id)filter { + FSTAssert(![FSTDocumentKey isDocumentKey:self.path], @"No filtering allowed for document query"); + + FSTFieldPath *_Nullable newInequalityField = nil; + if ([filter isKindOfClass:[FSTRelationFilter class]] && + [((FSTRelationFilter *)filter)isInequality]) { + newInequalityField = filter.field; + } + FSTFieldPath *_Nullable queryInequalityField = [self inequalityFilterField]; + FSTAssert(!queryInequalityField || !newInequalityField || + [queryInequalityField isEqual:newInequalityField], + @"Query must only have one inequality field."); + + return [[FSTQuery alloc] initWithPath:self.path + filterBy:[self.filters arrayByAddingObject:filter] + orderBy:self.explicitSortOrders + limit:self.limit + startAt:self.startAt + endAt:self.endAt]; +} + +- (instancetype)queryByAddingSortOrder:(FSTSortOrder *)sortOrder { + FSTAssert(![FSTDocumentKey isDocumentKey:self.path], + @"No ordering is allowed for a document query."); + + // TODO(klimt): Validate that the same key isn't added twice. + return [[FSTQuery alloc] initWithPath:self.path + filterBy:self.filters + orderBy:[self.explicitSortOrders arrayByAddingObject:sortOrder] + limit:self.limit + startAt:self.startAt + endAt:self.endAt]; +} + +- (instancetype)queryBySettingLimit:(NSInteger)limit { + return [[FSTQuery alloc] initWithPath:self.path + filterBy:self.filters + orderBy:self.explicitSortOrders + limit:limit + startAt:self.startAt + endAt:self.endAt]; +} + +- (instancetype)queryByAddingStartAt:(FSTBound *)bound { + return [[FSTQuery alloc] initWithPath:self.path + filterBy:self.filters + orderBy:self.explicitSortOrders + limit:self.limit + startAt:bound + endAt:self.endAt]; +} + +- (instancetype)queryByAddingEndAt:(FSTBound *)bound { + return [[FSTQuery alloc] initWithPath:self.path + filterBy:self.filters + orderBy:self.explicitSortOrders + limit:self.limit + startAt:self.startAt + endAt:bound]; +} + +- (BOOL)isDocumentQuery { + return [FSTDocumentKey isDocumentKey:self.path] && self.filters.count == 0; +} + +- (BOOL)matchesDocument:(FSTDocument *)document { + return [self pathMatchesDocument:document] && [self orderByMatchesDocument:document] && + [self filtersMatchDocument:document] && [self boundsMatchDocument:document]; +} + +- (NSComparator)comparator { + return ^NSComparisonResult(id document1, id document2) { + BOOL didCompareOnKeyField = NO; + for (FSTSortOrder *orderBy in self.sortOrders) { + NSComparisonResult comp = [orderBy compareDocument:document1 toDocument:document2]; + if (comp != NSOrderedSame) { + return comp; + } + didCompareOnKeyField = + didCompareOnKeyField || [orderBy.field isEqual:[FSTFieldPath keyFieldPath]]; + } + FSTAssert(didCompareOnKeyField, @"sortOrder of query did not include key ordering"); + return NSOrderedSame; + }; +} + +- (FSTFieldPath *_Nullable)inequalityFilterField { + for (id filter in self.filters) { + if ([filter isKindOfClass:[FSTRelationFilter class]] && + ((FSTRelationFilter *)filter).filterOperator != FSTRelationFilterOperatorEqual) { + return filter.field; + } + } + return nil; +} + +- (FSTFieldPath *_Nullable)firstSortOrderField { + return self.explicitSortOrders.firstObject.field; +} + +#pragma mark - Private properties + +- (NSString *)canonicalID { + if (_canonicalID) { + return _canonicalID; + } + + NSMutableString *canonicalID = [[self.path canonicalString] mutableCopy]; + + // Add filters. + [canonicalID appendString:@"|f:"]; + for (id predicate in self.filters) { + [canonicalID appendFormat:@"%@", [predicate canonicalID]]; + } + + // Add order by. + [canonicalID appendString:@"|ob:"]; + for (FSTSortOrder *orderBy in self.sortOrders) { + [canonicalID appendString:orderBy.canonicalID]; + } + + // Add limit. + if (self.limit != NSNotFound) { + [canonicalID appendFormat:@"|l:%ld", (long)self.limit]; + } + + if (self.startAt) { + [canonicalID appendFormat:@"|lb:%@", self.startAt.canonicalString]; + } + + if (self.endAt) { + [canonicalID appendFormat:@"|ub:%@", self.endAt.canonicalString]; + } + + _canonicalID = canonicalID; + return canonicalID; +} + +#pragma mark - Private methods + +- (BOOL)isEqualToQuery:(FSTQuery *)other { + return [self.path isEqual:other.path] && self.limit == other.limit && + [self.filters isEqual:other.filters] && [self.sortOrders isEqual:other.sortOrders] && + (self.startAt == other.startAt || [self.startAt isEqual:other.startAt]) && + (self.endAt == other.endAt || [self.endAt isEqual:other.endAt]); +} + +/* Returns YES if the document matches the path for the receiver. */ +- (BOOL)pathMatchesDocument:(FSTDocument *)document { + FSTResourcePath *documentPath = document.key.path; + if ([FSTDocumentKey isDocumentKey:self.path]) { + // Exact match for document queries. + return [self.path isEqual:documentPath]; + } else { + // Shallow ancestor queries by default. + return [self.path isPrefixOfPath:documentPath] && self.path.length == documentPath.length - 1; + } +} + +/** + * A document must have a value for every ordering clause in order to show up in the results. + */ +- (BOOL)orderByMatchesDocument:(FSTDocument *)document { + for (FSTSortOrder *orderBy in self.explicitSortOrders) { + FSTFieldPath *fieldPath = orderBy.field; + // order by key always matches + if (![fieldPath isEqual:[FSTFieldPath keyFieldPath]] && + [document fieldForPath:fieldPath] == nil) { + return NO; + } + } + return YES; +} + +/** Returns YES if the document matches all of the filters in the receiver. */ +- (BOOL)filtersMatchDocument:(FSTDocument *)document { + for (id filter in self.filters) { + if (![filter matchesDocument:document]) { + return NO; + } + } + return YES; +} + +- (BOOL)boundsMatchDocument:(FSTDocument *)document { + if (self.startAt && ![self.startAt sortsBeforeDocument:document usingSortOrder:self.sortOrders]) { + return NO; + } + if (self.endAt && [self.endAt sortsBeforeDocument:document usingSortOrder:self.sortOrders]) { + return NO; + } + return YES; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTSnapshotVersion.m b/Firestore/Source/Core/FSTSnapshotVersion.m deleted file mode 100644 index 980ae52..0000000 --- a/Firestore/Source/Core/FSTSnapshotVersion.m +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Core/FSTSnapshotVersion.h" - -#import "Firestore/Source/Core/FSTTimestamp.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTSnapshotVersion - -+ (instancetype)noVersion { - static FSTSnapshotVersion *min; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:0 nanos:0]; - min = [FSTSnapshotVersion versionWithTimestamp:timestamp]; - }); - return min; -} - -+ (instancetype)versionWithTimestamp:(FSTTimestamp *)timestamp { - return [[FSTSnapshotVersion alloc] initWithTimestamp:timestamp]; -} - -- (instancetype)initWithTimestamp:(FSTTimestamp *)timestamp { - self = [super init]; - if (self) { - _timestamp = timestamp; - } - return self; -} - -#pragma mark - NSObject methods - -- (BOOL)isEqual:(id)object { - if (self == object) { - return YES; - } - if (![object isKindOfClass:[FSTSnapshotVersion class]]) { - return NO; - } - return [self.timestamp isEqual:((FSTSnapshotVersion *)object).timestamp]; -} - -- (NSUInteger)hash { - return self.timestamp.hash; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"", self.timestamp]; -} - -- (id)copyWithZone:(NSZone *_Nullable)zone { - // Implements NSCopying without actually copying because timestamps are immutable. - return self; -} - -#pragma mark - Public methods - -- (NSComparisonResult)compare:(FSTSnapshotVersion *)other { - return [self.timestamp compare:other.timestamp]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTSnapshotVersion.mm b/Firestore/Source/Core/FSTSnapshotVersion.mm new file mode 100644 index 0000000..980ae52 --- /dev/null +++ b/Firestore/Source/Core/FSTSnapshotVersion.mm @@ -0,0 +1,80 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Core/FSTSnapshotVersion.h" + +#import "Firestore/Source/Core/FSTTimestamp.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTSnapshotVersion + ++ (instancetype)noVersion { + static FSTSnapshotVersion *min; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:0 nanos:0]; + min = [FSTSnapshotVersion versionWithTimestamp:timestamp]; + }); + return min; +} + ++ (instancetype)versionWithTimestamp:(FSTTimestamp *)timestamp { + return [[FSTSnapshotVersion alloc] initWithTimestamp:timestamp]; +} + +- (instancetype)initWithTimestamp:(FSTTimestamp *)timestamp { + self = [super init]; + if (self) { + _timestamp = timestamp; + } + return self; +} + +#pragma mark - NSObject methods + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + if (![object isKindOfClass:[FSTSnapshotVersion class]]) { + return NO; + } + return [self.timestamp isEqual:((FSTSnapshotVersion *)object).timestamp]; +} + +- (NSUInteger)hash { + return self.timestamp.hash; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.timestamp]; +} + +- (id)copyWithZone:(NSZone *_Nullable)zone { + // Implements NSCopying without actually copying because timestamps are immutable. + return self; +} + +#pragma mark - Public methods + +- (NSComparisonResult)compare:(FSTSnapshotVersion *)other { + return [self.timestamp compare:other.timestamp]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTransaction.m b/Firestore/Source/Core/FSTTransaction.m deleted file mode 100644 index c4c5f27..0000000 --- a/Firestore/Source/Core/FSTTransaction.m +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Core/FSTTransaction.h" - -#import - -#import "FIRFirestoreErrors.h" -#import "FIRSetOptions.h" -#import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTDocumentKeySet.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Remote/FSTDatastore.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTTransaction - -@interface FSTTransaction () -@property(nonatomic, strong, readonly) FSTDatastore *datastore; -@property(nonatomic, strong, readonly) - NSMutableDictionary *readVersions; -@property(nonatomic, strong, readonly) NSMutableArray *mutations; -@property(nonatomic, assign) BOOL commitCalled; -/** - * An error that may have occurred as a consequence of a write. If set, needs to be raised in the - * completion handler instead of trying to commit. - */ -@property(nonatomic, strong, nullable) NSError *lastWriteError; -@end - -@implementation FSTTransaction - -+ (instancetype)transactionWithDatastore:(FSTDatastore *)datastore { - return [[FSTTransaction alloc] initWithDatastore:datastore]; -} - -- (instancetype)initWithDatastore:(FSTDatastore *)datastore { - self = [super init]; - if (self) { - _datastore = datastore; - _readVersions = [NSMutableDictionary dictionary]; - _mutations = [NSMutableArray array]; - _commitCalled = NO; - } - return self; -} - -/** - * Every time a document is read, this should be called to record its version. If we read two - * different versions of the same document, this will return an error through its out parameter. - * When the transaction is committed, the versions recorded will be set as preconditions on the - * writes sent to the backend. - */ -- (BOOL)recordVersionForDocument:(FSTMaybeDocument *)doc error:(NSError **)error { - FSTAssert(error != nil, @"nil error parameter"); - *error = nil; - FSTSnapshotVersion *docVersion = doc.version; - if ([doc isKindOfClass:[FSTDeletedDocument class]]) { - // For deleted docs, we must record an explicit no version to build the right precondition - // when writing. - docVersion = [FSTSnapshotVersion noVersion]; - } - FSTSnapshotVersion *existingVersion = self.readVersions[doc.key]; - if (existingVersion) { - if (error) { - *error = - [NSError errorWithDomain:FIRFirestoreErrorDomain - code:FIRFirestoreErrorCodeFailedPrecondition - userInfo:@{ - NSLocalizedDescriptionKey : - @"A document cannot be read twice within a single transaction." - }]; - } - return NO; - } else { - self.readVersions[doc.key] = docVersion; - return YES; - } -} - -- (void)lookupDocumentsForKeys:(NSArray *)keys - completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion { - [self ensureCommitNotCalled]; - if (self.mutations.count) { - FSTThrowInvalidUsage(@"FIRIllegalStateException", - @"All reads in a transaction must be done before any writes."); - } - [self.datastore - lookupDocuments:keys - completion:^(NSArray *_Nullable documents, NSError *_Nullable error) { - if (error) { - completion(nil, error); - return; - } - for (FSTMaybeDocument *doc in documents) { - NSError *recordError = nil; - if (![self recordVersionForDocument:doc error:&recordError]) { - completion(nil, recordError); - return; - } - } - completion(documents, nil); - }]; -} - -/** Stores mutations to be written when commitWithCompletion is called. */ -- (void)writeMutations:(NSArray *)mutations { - [self ensureCommitNotCalled]; - [self.mutations addObjectsFromArray:mutations]; -} - -/** - * Returns version of this doc when it was read in this transaction as a precondition, or no - * precondition if it was not read. - */ -- (FSTPrecondition *)preconditionForDocumentKey:(FSTDocumentKey *)key { - FSTSnapshotVersion *_Nullable snapshotVersion = self.readVersions[key]; - if (snapshotVersion) { - return [FSTPrecondition preconditionWithUpdateTime:snapshotVersion]; - } else { - return [FSTPrecondition none]; - } -} - -/** - * Returns the precondition for a document if the operation is an update, based on the provided - * UpdateOptions. Will return nil if an error occurred, in which case it sets the error parameter. - */ -- (nullable FSTPrecondition *)preconditionForUpdateWithDocumentKey:(FSTDocumentKey *)key - error:(NSError **)error { - FSTSnapshotVersion *_Nullable version = self.readVersions[key]; - if (version && [version isEqual:[FSTSnapshotVersion noVersion]]) { - // The document was read, but doesn't exist. - // Return an error because the precondition is impossible - if (error) { - *error = [NSError - errorWithDomain:FIRFirestoreErrorDomain - code:FIRFirestoreErrorCodeAborted - userInfo:@{ - NSLocalizedDescriptionKey : @"Can't update a document that doesn't exist." - }]; - } - return nil; - } else if (version) { - // Document exists, just base precondition on document update time. - return [FSTPrecondition preconditionWithUpdateTime:version]; - } else { - // Document was not read, so we just use the preconditions for an update. - return [FSTPrecondition preconditionWithExists:YES]; - } -} - -- (void)setData:(FSTParsedSetData *)data forDocument:(FSTDocumentKey *)key { - [self writeMutations:[data mutationsWithKey:key - precondition:[self preconditionForDocumentKey:key]]]; -} - -- (void)updateData:(FSTParsedUpdateData *)data forDocument:(FSTDocumentKey *)key { - NSError *error = nil; - FSTPrecondition *_Nullable precondition = - [self preconditionForUpdateWithDocumentKey:key error:&error]; - if (precondition) { - [self writeMutations:[data mutationsWithKey:key precondition:precondition]]; - } else { - FSTAssert(error, @"Got nil precondition, but error was not set"); - self.lastWriteError = error; - } -} - -- (void)deleteDocument:(FSTDocumentKey *)key { - [self writeMutations:@[ [[FSTDeleteMutation alloc] - initWithKey:key - precondition:[self preconditionForDocumentKey:key]] ]]; - // Since the delete will be applied before all following writes, we need to ensure that the - // precondition for the next write will be exists: false. - self.readVersions[key] = [FSTSnapshotVersion noVersion]; -} - -- (void)commitWithCompletion:(FSTVoidErrorBlock)completion { - [self ensureCommitNotCalled]; - // Once commitWithCompletion is called once, mark this object so it can't be used again. - self.commitCalled = YES; - - // If there was an error writing, raise that error now - if (self.lastWriteError) { - completion(self.lastWriteError); - return; - } - - // Make a list of read documents that haven't been written. - __block FSTDocumentKeySet *unwritten = [FSTDocumentKeySet keySet]; - [self.readVersions enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, - FSTSnapshotVersion *version, BOOL *stop) { - unwritten = [unwritten setByAddingObject:key]; - }]; - // For each mutation, note that the doc was written. - for (FSTMutation *mutation in self.mutations) { - unwritten = [unwritten setByRemovingObject:mutation.key]; - } - if (unwritten.count) { - // TODO(klimt): This is a temporary restriction, until "verify" is supported on the backend. - completion([NSError - errorWithDomain:FIRFirestoreErrorDomain - code:FIRFirestoreErrorCodeFailedPrecondition - userInfo:@{ - NSLocalizedDescriptionKey : @"Every document read in a transaction must also be " - @"written in that transaction." - }]); - } else { - [self.datastore commitMutations:self.mutations - completion:^(NSError *_Nullable error) { - if (error) { - completion(error); - } else { - completion(nil); - } - }]; - } -} - -- (void)ensureCommitNotCalled { - if (self.commitCalled) { - FSTThrowInvalidUsage( - @"FIRIllegalStateException", - @"A transaction object cannot be used after its update block has completed."); - } -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTransaction.mm b/Firestore/Source/Core/FSTTransaction.mm new file mode 100644 index 0000000..f97888a --- /dev/null +++ b/Firestore/Source/Core/FSTTransaction.mm @@ -0,0 +1,250 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Core/FSTTransaction.h" + +#import + +#import "FIRFirestoreErrors.h" +#import "FIRSetOptions.h" +#import "Firestore/Source/API/FSTUserDataConverter.h" +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTDocumentKeySet.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Remote/FSTDatastore.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTTransaction + +@interface FSTTransaction () +@property(nonatomic, strong, readonly) FSTDatastore *datastore; +@property(nonatomic, strong, readonly) + NSMutableDictionary *readVersions; +@property(nonatomic, strong, readonly) NSMutableArray *mutations; +@property(nonatomic, assign) BOOL commitCalled; +/** + * An error that may have occurred as a consequence of a write. If set, needs to be raised in the + * completion handler instead of trying to commit. + */ +@property(nonatomic, strong, nullable) NSError *lastWriteError; +@end + +@implementation FSTTransaction + ++ (instancetype)transactionWithDatastore:(FSTDatastore *)datastore { + return [[FSTTransaction alloc] initWithDatastore:datastore]; +} + +- (instancetype)initWithDatastore:(FSTDatastore *)datastore { + self = [super init]; + if (self) { + _datastore = datastore; + _readVersions = [NSMutableDictionary dictionary]; + _mutations = [NSMutableArray array]; + _commitCalled = NO; + } + return self; +} + +/** + * Every time a document is read, this should be called to record its version. If we read two + * different versions of the same document, this will return an error through its out parameter. + * When the transaction is committed, the versions recorded will be set as preconditions on the + * writes sent to the backend. + */ +- (BOOL)recordVersionForDocument:(FSTMaybeDocument *)doc error:(NSError **)error { + FSTAssert(error != nil, @"nil error parameter"); + *error = nil; + FSTSnapshotVersion *docVersion = doc.version; + if ([doc isKindOfClass:[FSTDeletedDocument class]]) { + // For deleted docs, we must record an explicit no version to build the right precondition + // when writing. + docVersion = [FSTSnapshotVersion noVersion]; + } + FSTSnapshotVersion *existingVersion = self.readVersions[doc.key]; + if (existingVersion) { + if (error) { + *error = + [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeFailedPrecondition + userInfo:@{ + NSLocalizedDescriptionKey : + @"A document cannot be read twice within a single transaction." + }]; + } + return NO; + } else { + self.readVersions[doc.key] = docVersion; + return YES; + } +} + +- (void)lookupDocumentsForKeys:(NSArray *)keys + completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion { + [self ensureCommitNotCalled]; + if (self.mutations.count) { + FSTThrowInvalidUsage(@"FIRIllegalStateException", + @"All reads in a transaction must be done before any writes."); + } + [self.datastore lookupDocuments:keys + completion:^(NSArray *_Nullable documents, + NSError *_Nullable error) { + if (error) { + completion(nil, error); + return; + } + for (FSTMaybeDocument *doc in documents) { + NSError *recordError = nil; + if (![self recordVersionForDocument:doc error:&recordError]) { + completion(nil, recordError); + return; + } + } + completion(documents, nil); + }]; +} + +/** Stores mutations to be written when commitWithCompletion is called. */ +- (void)writeMutations:(NSArray *)mutations { + [self ensureCommitNotCalled]; + [self.mutations addObjectsFromArray:mutations]; +} + +/** + * Returns version of this doc when it was read in this transaction as a precondition, or no + * precondition if it was not read. + */ +- (FSTPrecondition *)preconditionForDocumentKey:(FSTDocumentKey *)key { + FSTSnapshotVersion *_Nullable snapshotVersion = self.readVersions[key]; + if (snapshotVersion) { + return [FSTPrecondition preconditionWithUpdateTime:snapshotVersion]; + } else { + return [FSTPrecondition none]; + } +} + +/** + * Returns the precondition for a document if the operation is an update, based on the provided + * UpdateOptions. Will return nil if an error occurred, in which case it sets the error parameter. + */ +- (nullable FSTPrecondition *)preconditionForUpdateWithDocumentKey:(FSTDocumentKey *)key + error:(NSError **)error { + FSTSnapshotVersion *_Nullable version = self.readVersions[key]; + if (version && [version isEqual:[FSTSnapshotVersion noVersion]]) { + // The document was read, but doesn't exist. + // Return an error because the precondition is impossible + if (error) { + *error = [NSError + errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeAborted + userInfo:@{ + NSLocalizedDescriptionKey : @"Can't update a document that doesn't exist." + }]; + } + return nil; + } else if (version) { + // Document exists, just base precondition on document update time. + return [FSTPrecondition preconditionWithUpdateTime:version]; + } else { + // Document was not read, so we just use the preconditions for an update. + return [FSTPrecondition preconditionWithExists:YES]; + } +} + +- (void)setData:(FSTParsedSetData *)data forDocument:(FSTDocumentKey *)key { + [self writeMutations:[data mutationsWithKey:key + precondition:[self preconditionForDocumentKey:key]]]; +} + +- (void)updateData:(FSTParsedUpdateData *)data forDocument:(FSTDocumentKey *)key { + NSError *error = nil; + FSTPrecondition *_Nullable precondition = + [self preconditionForUpdateWithDocumentKey:key error:&error]; + if (precondition) { + [self writeMutations:[data mutationsWithKey:key precondition:precondition]]; + } else { + FSTAssert(error, @"Got nil precondition, but error was not set"); + self.lastWriteError = error; + } +} + +- (void)deleteDocument:(FSTDocumentKey *)key { + [self writeMutations:@[ [[FSTDeleteMutation alloc] + initWithKey:key + precondition:[self preconditionForDocumentKey:key]] ]]; + // Since the delete will be applied before all following writes, we need to ensure that the + // precondition for the next write will be exists: false. + self.readVersions[key] = [FSTSnapshotVersion noVersion]; +} + +- (void)commitWithCompletion:(FSTVoidErrorBlock)completion { + [self ensureCommitNotCalled]; + // Once commitWithCompletion is called once, mark this object so it can't be used again. + self.commitCalled = YES; + + // If there was an error writing, raise that error now + if (self.lastWriteError) { + completion(self.lastWriteError); + return; + } + + // Make a list of read documents that haven't been written. + __block FSTDocumentKeySet *unwritten = [FSTDocumentKeySet keySet]; + [self.readVersions enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, + FSTSnapshotVersion *version, BOOL *stop) { + unwritten = [unwritten setByAddingObject:key]; + }]; + // For each mutation, note that the doc was written. + for (FSTMutation *mutation in self.mutations) { + unwritten = [unwritten setByRemovingObject:mutation.key]; + } + if (unwritten.count) { + // TODO(klimt): This is a temporary restriction, until "verify" is supported on the backend. + completion([NSError + errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeFailedPrecondition + userInfo:@{ + NSLocalizedDescriptionKey : @"Every document read in a transaction must also be " + @"written in that transaction." + }]); + } else { + [self.datastore commitMutations:self.mutations + completion:^(NSError *_Nullable error) { + if (error) { + completion(error); + } else { + completion(nil); + } + }]; + } +} + +- (void)ensureCommitNotCalled { + if (self.commitCalled) { + FSTThrowInvalidUsage( + @"FIRIllegalStateException", + @"A transaction object cannot be used after its update block has completed."); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTView.m b/Firestore/Source/Core/FSTView.m deleted file mode 100644 index d6b4558..0000000 --- a/Firestore/Source/Core/FSTView.m +++ /dev/null @@ -1,479 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Core/FSTView.h" - -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTViewDocumentChanges - -/** The result of applying a set of doc changes to a view. */ -@interface FSTViewDocumentChanges () - -- (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet - changeSet:(FSTDocumentViewChangeSet *)changeSet - needsRefill:(BOOL)needsRefill - mutatedKeys:(FSTDocumentKeySet *)mutatedKeys NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FSTViewDocumentChanges - -- (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet - changeSet:(FSTDocumentViewChangeSet *)changeSet - needsRefill:(BOOL)needsRefill - mutatedKeys:(FSTDocumentKeySet *)mutatedKeys { - self = [super init]; - if (self) { - _documentSet = documentSet; - _changeSet = changeSet; - _needsRefill = needsRefill; - _mutatedKeys = mutatedKeys; - } - return self; -} - -@end - -#pragma mark - FSTLimboDocumentChange - -@interface FSTLimboDocumentChange () - -+ (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key; - -- (instancetype)initWithType:(FSTLimboDocumentChangeType)type - key:(FSTDocumentKey *)key NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FSTLimboDocumentChange - -+ (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key { - return [[FSTLimboDocumentChange alloc] initWithType:type key:key]; -} - -- (instancetype)initWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key { - self = [super init]; - if (self) { - _type = type; - _key = key; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (self == other) { - return YES; - } - if (![other isKindOfClass:[FSTLimboDocumentChange class]]) { - return NO; - } - FSTLimboDocumentChange *otherChange = (FSTLimboDocumentChange *)other; - return self.type == otherChange.type && [self.key isEqual:otherChange.key]; -} - -- (NSUInteger)hash { - NSUInteger hash = self.type; - hash = hash * 31u + [self.key hash]; - return hash; -} - -@end - -#pragma mark - FSTViewChange - -@interface FSTViewChange () - -+ (FSTViewChange *)changeWithSnapshot:(nullable FSTViewSnapshot *)snapshot - limboChanges:(NSArray *)limboChanges; - -- (instancetype)initWithSnapshot:(nullable FSTViewSnapshot *)snapshot - limboChanges:(NSArray *)limboChanges - NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FSTViewChange - -+ (FSTViewChange *)changeWithSnapshot:(nullable FSTViewSnapshot *)snapshot - limboChanges:(NSArray *)limboChanges { - return [[self alloc] initWithSnapshot:snapshot limboChanges:limboChanges]; -} - -- (instancetype)initWithSnapshot:(nullable FSTViewSnapshot *)snapshot - limboChanges:(NSArray *)limboChanges { - self = [super init]; - if (self) { - _snapshot = snapshot; - _limboChanges = limboChanges; - } - return self; -} - -@end - -#pragma mark - FSTView - -static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChangeType c1, - FSTDocumentViewChangeType c2); - -@interface FSTView () - -@property(nonatomic, strong, readonly) FSTQuery *query; - -@property(nonatomic, assign) FSTSyncState syncState; - -/** - * A flag whether the view is current with the backend. A view is considered current after it - * has seen the current flag from the backend and did not lose consistency within the watch stream - * (e.g. because of an existence filter mismatch). - */ -@property(nonatomic, assign, getter=isCurrent) BOOL current; - -@property(nonatomic, strong) FSTDocumentSet *documentSet; - -/** Documents included in the remote target. */ -@property(nonatomic, strong) FSTDocumentKeySet *syncedDocuments; - -/** Documents in the view but not in the remote target */ -@property(nonatomic, strong) FSTDocumentKeySet *limboDocuments; - -/** Document Keys that have local changes. */ -@property(nonatomic, strong) FSTDocumentKeySet *mutatedKeys; - -@end - -@implementation FSTView - -- (instancetype)initWithQuery:(FSTQuery *)query - remoteDocuments:(nonnull FSTDocumentKeySet *)remoteDocuments { - self = [super init]; - if (self) { - _query = query; - _documentSet = [FSTDocumentSet documentSetWithComparator:query.comparator]; - _syncedDocuments = remoteDocuments; - _limboDocuments = [FSTDocumentKeySet keySet]; - _mutatedKeys = [FSTDocumentKeySet keySet]; - } - return self; -} - -- (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges { - return [self computeChangesWithDocuments:docChanges previousChanges:nil]; -} - -- (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges - previousChanges: - (nullable FSTViewDocumentChanges *)previousChanges { - FSTDocumentViewChangeSet *changeSet = - previousChanges ? previousChanges.changeSet : [FSTDocumentViewChangeSet changeSet]; - FSTDocumentSet *oldDocumentSet = previousChanges ? previousChanges.documentSet : self.documentSet; - - __block FSTDocumentKeySet *newMutatedKeys = - previousChanges ? previousChanges.mutatedKeys : self.mutatedKeys; - __block FSTDocumentSet *newDocumentSet = oldDocumentSet; - __block BOOL needsRefill = NO; - - // Track the last doc in a (full) limit. This is necessary, because some update (a delete, or an - // update moving a doc past the old limit) might mean there is some other document in the local - // cache that either should come (1) between the old last limit doc and the new last document, - // in the case of updates, or (2) after the new last document, in the case of deletes. So we - // keep this doc at the old limit to compare the updates to. - // - // Note that this should never get used in a refill (when previousChanges is set), because there - // will only be adds -- no deletes or updates. - FSTDocument *_Nullable lastDocInLimit = - (self.query.limit && oldDocumentSet.count == self.query.limit) ? oldDocumentSet.lastDocument - : nil; - - [docChanges enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, - FSTMaybeDocument *maybeNewDoc, BOOL *stop) { - FSTDocument *_Nullable oldDoc = [oldDocumentSet documentForKey:key]; - FSTDocument *_Nullable newDoc = nil; - if ([maybeNewDoc isKindOfClass:[FSTDocument class]]) { - newDoc = (FSTDocument *)maybeNewDoc; - } - if (newDoc) { - FSTAssert([key isEqual:newDoc.key], @"Mismatching key in document changes: %@ != %@", key, - newDoc.key); - if (![self.query matchesDocument:newDoc]) { - newDoc = nil; - } - } - if (newDoc) { - newDocumentSet = [newDocumentSet documentSetByAddingDocument:newDoc]; - if (newDoc.hasLocalMutations) { - newMutatedKeys = [newMutatedKeys setByAddingObject:key]; - } else { - newMutatedKeys = [newMutatedKeys setByRemovingObject:key]; - } - } else { - newDocumentSet = [newDocumentSet documentSetByRemovingKey:key]; - newMutatedKeys = [newMutatedKeys setByRemovingObject:key]; - } - - // Calculate change - if (oldDoc && newDoc) { - BOOL docsEqual = [oldDoc.data isEqual:newDoc.data]; - if (!docsEqual || oldDoc.hasLocalMutations != newDoc.hasLocalMutations) { - // only report a change if document actually changed. - if (docsEqual) { - [changeSet addChange:[FSTDocumentViewChange - changeWithDocument:newDoc - type:FSTDocumentViewChangeTypeMetadata]]; - } else { - [changeSet addChange:[FSTDocumentViewChange - changeWithDocument:newDoc - type:FSTDocumentViewChangeTypeModified]]; - } - - if (lastDocInLimit && self.query.comparator(newDoc, lastDocInLimit) > 0) { - // This doc moved from inside the limit to after the limit. That means there may be some - // doc in the local cache that's actually less than this one. - needsRefill = YES; - } - } - } else if (!oldDoc && newDoc) { - [changeSet - addChange:[FSTDocumentViewChange changeWithDocument:newDoc - type:FSTDocumentViewChangeTypeAdded]]; - } else if (oldDoc && !newDoc) { - [changeSet - addChange:[FSTDocumentViewChange changeWithDocument:oldDoc - type:FSTDocumentViewChangeTypeRemoved]]; - if (lastDocInLimit) { - // A doc was removed from a full limit query. We'll need to re-query from the local cache - // to see if we know about some other doc that should be in the results. - needsRefill = YES; - } - } - }]; - if (self.query.limit) { - // TODO(klimt): Make DocumentSet size be constant time. - while (newDocumentSet.count > self.query.limit) { - FSTDocument *oldDoc = [newDocumentSet lastDocument]; - newDocumentSet = [newDocumentSet documentSetByRemovingKey:oldDoc.key]; - [changeSet - addChange:[FSTDocumentViewChange changeWithDocument:oldDoc - type:FSTDocumentViewChangeTypeRemoved]]; - } - } - - FSTAssert(!needsRefill || !previousChanges, - @"View was refilled using docs that themselves needed refilling."); - - return [[FSTViewDocumentChanges alloc] initWithDocumentSet:newDocumentSet - changeSet:changeSet - needsRefill:needsRefill - mutatedKeys:newMutatedKeys]; -} - -- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges { - return [self applyChangesToDocuments:docChanges targetChange:nil]; -} - -- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges - targetChange:(nullable FSTTargetChange *)targetChange { - FSTAssert(!docChanges.needsRefill, @"Cannot apply changes that need a refill"); - - FSTDocumentSet *oldDocuments = self.documentSet; - self.documentSet = docChanges.documentSet; - self.mutatedKeys = docChanges.mutatedKeys; - - // Sort changes based on type and query comparator. - NSArray *changes = [docChanges.changeSet changes]; - changes = [changes sortedArrayUsingComparator:^NSComparisonResult(FSTDocumentViewChange *c1, - FSTDocumentViewChange *c2) { - NSComparisonResult typeComparison = FSTCompareDocumentViewChangeTypes(c1.type, c2.type); - if (typeComparison != NSOrderedSame) { - return typeComparison; - } - return self.query.comparator(c1.document, c2.document); - }]; - [self applyTargetChange:targetChange]; - NSArray *limboChanges = [self updateLimboDocuments]; - BOOL synced = self.limboDocuments.count == 0 && self.isCurrent; - FSTSyncState newSyncState = synced ? FSTSyncStateSynced : FSTSyncStateLocal; - BOOL syncStateChanged = newSyncState != self.syncState; - self.syncState = newSyncState; - - if (changes.count == 0 && !syncStateChanged) { - // No changes. - return [FSTViewChange changeWithSnapshot:nil limboChanges:limboChanges]; - } else { - FSTViewSnapshot *snapshot = - [[FSTViewSnapshot alloc] initWithQuery:self.query - documents:docChanges.documentSet - oldDocuments:oldDocuments - documentChanges:changes - fromCache:newSyncState == FSTSyncStateLocal - hasPendingWrites:!docChanges.mutatedKeys.isEmpty - syncStateChanged:syncStateChanged]; - - return [FSTViewChange changeWithSnapshot:snapshot limboChanges:limboChanges]; - } -} - -- (FSTViewChange *)applyChangedOnlineState:(FSTOnlineState)onlineState { - if (self.isCurrent && onlineState == FSTOnlineStateFailed) { - // If we're offline, set `current` to NO and then call applyChanges to refresh our syncState - // and generate an FSTViewChange as appropriate. We are guaranteed to get a new FSTTargetChange - // that sets `current` back to YES once the client is back online. - self.current = NO; - return - [self applyChangesToDocuments:[[FSTViewDocumentChanges alloc] - initWithDocumentSet:self.documentSet - changeSet:[FSTDocumentViewChangeSet changeSet] - needsRefill:NO - mutatedKeys:self.mutatedKeys]]; - } else { - // No effect, just return a no-op FSTViewChange. - return [[FSTViewChange alloc] initWithSnapshot:nil limboChanges:@[]]; - } -} - -#pragma mark - Private methods - -/** Returns whether the doc for the given key should be in limbo. */ -- (BOOL)shouldBeLimboDocumentKey:(FSTDocumentKey *)key { - // If the remote end says it's part of this query, it's not in limbo. - if ([self.syncedDocuments containsObject:key]) { - return NO; - } - // The local store doesn't think it's a result, so it shouldn't be in limbo. - if (![self.documentSet containsKey:key]) { - return NO; - } - // If there are local changes to the doc, they might explain why the server doesn't know that it's - // part of the query. So don't put it in limbo. - // TODO(klimt): Ideally, we would only consider changes that might actually affect this specific - // query. - if ([self.documentSet documentForKey:key].hasLocalMutations) { - return NO; - } - // Everything else is in limbo. - return YES; -} - -/** - * Updates syncedDocuments and current based on the given change. - */ -- (void)applyTargetChange:(nullable FSTTargetChange *)targetChange { - if (targetChange) { - FSTTargetMapping *targetMapping = targetChange.mapping; - if ([targetMapping isKindOfClass:[FSTResetMapping class]]) { - self.syncedDocuments = ((FSTResetMapping *)targetMapping).documents; - } else if ([targetMapping isKindOfClass:[FSTUpdateMapping class]]) { - [((FSTUpdateMapping *)targetMapping).addedDocuments - enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { - self.syncedDocuments = [self.syncedDocuments setByAddingObject:key]; - }]; - [((FSTUpdateMapping *)targetMapping).removedDocuments - enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { - self.syncedDocuments = [self.syncedDocuments setByRemovingObject:key]; - }]; - } - - switch (targetChange.currentStatusUpdate) { - case FSTCurrentStatusUpdateMarkCurrent: - self.current = YES; - break; - case FSTCurrentStatusUpdateMarkNotCurrent: - self.current = NO; - break; - case FSTCurrentStatusUpdateNone: - break; - } - } -} - -/** Updates limboDocuments and returns any changes as FSTLimboDocumentChanges. */ -- (NSArray *)updateLimboDocuments { - // We can only determine limbo documents when we're in-sync with the server. - if (!self.isCurrent) { - return @[]; - } - - // TODO(klimt): Do this incrementally so that it's not quadratic when updating many documents. - FSTDocumentKeySet *oldLimboDocuments = self.limboDocuments; - self.limboDocuments = [FSTDocumentKeySet keySet]; - for (FSTDocument *doc in self.documentSet.documentEnumerator) { - if ([self shouldBeLimboDocumentKey:doc.key]) { - self.limboDocuments = [self.limboDocuments setByAddingObject:doc.key]; - } - } - - // Diff the new limbo docs with the old limbo docs. - NSMutableArray *changes = - [NSMutableArray arrayWithCapacity:(oldLimboDocuments.count + self.limboDocuments.count)]; - [oldLimboDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { - if (![self.limboDocuments containsObject:key]) { - [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved - key:key]]; - } - }]; - [self.limboDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { - if (![oldLimboDocuments containsObject:key]) { - [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded - key:key]]; - } - }]; - return changes; -} - -@end - -static inline int DocumentViewChangeTypePosition(FSTDocumentViewChangeType changeType) { - switch (changeType) { - case FSTDocumentViewChangeTypeRemoved: - return 0; - case FSTDocumentViewChangeTypeAdded: - return 1; - case FSTDocumentViewChangeTypeModified: - return 2; - case FSTDocumentViewChangeTypeMetadata: - // A metadata change is converted to a modified change at the public API layer. Since we sort - // by document key and then change type, metadata and modified changes must be sorted - // equivalently. - return 2; - default: - FSTCFail(@"Unknown FSTDocumentViewChangeType %lu", (unsigned long)changeType); - } -} - -static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChangeType c1, - FSTDocumentViewChangeType c2) { - int pos1 = DocumentViewChangeTypePosition(c1); - int pos2 = DocumentViewChangeTypePosition(c2); - if (pos1 == pos2) { - return NSOrderedSame; - } else if (pos1 < pos2) { - return NSOrderedAscending; - } else { - return NSOrderedDescending; - } -} - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTView.mm b/Firestore/Source/Core/FSTView.mm new file mode 100644 index 0000000..d6b4558 --- /dev/null +++ b/Firestore/Source/Core/FSTView.mm @@ -0,0 +1,479 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Core/FSTView.h" + +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTViewSnapshot.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Remote/FSTRemoteEvent.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTViewDocumentChanges + +/** The result of applying a set of doc changes to a view. */ +@interface FSTViewDocumentChanges () + +- (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet + changeSet:(FSTDocumentViewChangeSet *)changeSet + needsRefill:(BOOL)needsRefill + mutatedKeys:(FSTDocumentKeySet *)mutatedKeys NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTViewDocumentChanges + +- (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet + changeSet:(FSTDocumentViewChangeSet *)changeSet + needsRefill:(BOOL)needsRefill + mutatedKeys:(FSTDocumentKeySet *)mutatedKeys { + self = [super init]; + if (self) { + _documentSet = documentSet; + _changeSet = changeSet; + _needsRefill = needsRefill; + _mutatedKeys = mutatedKeys; + } + return self; +} + +@end + +#pragma mark - FSTLimboDocumentChange + +@interface FSTLimboDocumentChange () + ++ (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key; + +- (instancetype)initWithType:(FSTLimboDocumentChangeType)type + key:(FSTDocumentKey *)key NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTLimboDocumentChange + ++ (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key { + return [[FSTLimboDocumentChange alloc] initWithType:type key:key]; +} + +- (instancetype)initWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key { + self = [super init]; + if (self) { + _type = type; + _key = key; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } + if (![other isKindOfClass:[FSTLimboDocumentChange class]]) { + return NO; + } + FSTLimboDocumentChange *otherChange = (FSTLimboDocumentChange *)other; + return self.type == otherChange.type && [self.key isEqual:otherChange.key]; +} + +- (NSUInteger)hash { + NSUInteger hash = self.type; + hash = hash * 31u + [self.key hash]; + return hash; +} + +@end + +#pragma mark - FSTViewChange + +@interface FSTViewChange () + ++ (FSTViewChange *)changeWithSnapshot:(nullable FSTViewSnapshot *)snapshot + limboChanges:(NSArray *)limboChanges; + +- (instancetype)initWithSnapshot:(nullable FSTViewSnapshot *)snapshot + limboChanges:(NSArray *)limboChanges + NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTViewChange + ++ (FSTViewChange *)changeWithSnapshot:(nullable FSTViewSnapshot *)snapshot + limboChanges:(NSArray *)limboChanges { + return [[self alloc] initWithSnapshot:snapshot limboChanges:limboChanges]; +} + +- (instancetype)initWithSnapshot:(nullable FSTViewSnapshot *)snapshot + limboChanges:(NSArray *)limboChanges { + self = [super init]; + if (self) { + _snapshot = snapshot; + _limboChanges = limboChanges; + } + return self; +} + +@end + +#pragma mark - FSTView + +static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChangeType c1, + FSTDocumentViewChangeType c2); + +@interface FSTView () + +@property(nonatomic, strong, readonly) FSTQuery *query; + +@property(nonatomic, assign) FSTSyncState syncState; + +/** + * A flag whether the view is current with the backend. A view is considered current after it + * has seen the current flag from the backend and did not lose consistency within the watch stream + * (e.g. because of an existence filter mismatch). + */ +@property(nonatomic, assign, getter=isCurrent) BOOL current; + +@property(nonatomic, strong) FSTDocumentSet *documentSet; + +/** Documents included in the remote target. */ +@property(nonatomic, strong) FSTDocumentKeySet *syncedDocuments; + +/** Documents in the view but not in the remote target */ +@property(nonatomic, strong) FSTDocumentKeySet *limboDocuments; + +/** Document Keys that have local changes. */ +@property(nonatomic, strong) FSTDocumentKeySet *mutatedKeys; + +@end + +@implementation FSTView + +- (instancetype)initWithQuery:(FSTQuery *)query + remoteDocuments:(nonnull FSTDocumentKeySet *)remoteDocuments { + self = [super init]; + if (self) { + _query = query; + _documentSet = [FSTDocumentSet documentSetWithComparator:query.comparator]; + _syncedDocuments = remoteDocuments; + _limboDocuments = [FSTDocumentKeySet keySet]; + _mutatedKeys = [FSTDocumentKeySet keySet]; + } + return self; +} + +- (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges { + return [self computeChangesWithDocuments:docChanges previousChanges:nil]; +} + +- (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges + previousChanges: + (nullable FSTViewDocumentChanges *)previousChanges { + FSTDocumentViewChangeSet *changeSet = + previousChanges ? previousChanges.changeSet : [FSTDocumentViewChangeSet changeSet]; + FSTDocumentSet *oldDocumentSet = previousChanges ? previousChanges.documentSet : self.documentSet; + + __block FSTDocumentKeySet *newMutatedKeys = + previousChanges ? previousChanges.mutatedKeys : self.mutatedKeys; + __block FSTDocumentSet *newDocumentSet = oldDocumentSet; + __block BOOL needsRefill = NO; + + // Track the last doc in a (full) limit. This is necessary, because some update (a delete, or an + // update moving a doc past the old limit) might mean there is some other document in the local + // cache that either should come (1) between the old last limit doc and the new last document, + // in the case of updates, or (2) after the new last document, in the case of deletes. So we + // keep this doc at the old limit to compare the updates to. + // + // Note that this should never get used in a refill (when previousChanges is set), because there + // will only be adds -- no deletes or updates. + FSTDocument *_Nullable lastDocInLimit = + (self.query.limit && oldDocumentSet.count == self.query.limit) ? oldDocumentSet.lastDocument + : nil; + + [docChanges enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, + FSTMaybeDocument *maybeNewDoc, BOOL *stop) { + FSTDocument *_Nullable oldDoc = [oldDocumentSet documentForKey:key]; + FSTDocument *_Nullable newDoc = nil; + if ([maybeNewDoc isKindOfClass:[FSTDocument class]]) { + newDoc = (FSTDocument *)maybeNewDoc; + } + if (newDoc) { + FSTAssert([key isEqual:newDoc.key], @"Mismatching key in document changes: %@ != %@", key, + newDoc.key); + if (![self.query matchesDocument:newDoc]) { + newDoc = nil; + } + } + if (newDoc) { + newDocumentSet = [newDocumentSet documentSetByAddingDocument:newDoc]; + if (newDoc.hasLocalMutations) { + newMutatedKeys = [newMutatedKeys setByAddingObject:key]; + } else { + newMutatedKeys = [newMutatedKeys setByRemovingObject:key]; + } + } else { + newDocumentSet = [newDocumentSet documentSetByRemovingKey:key]; + newMutatedKeys = [newMutatedKeys setByRemovingObject:key]; + } + + // Calculate change + if (oldDoc && newDoc) { + BOOL docsEqual = [oldDoc.data isEqual:newDoc.data]; + if (!docsEqual || oldDoc.hasLocalMutations != newDoc.hasLocalMutations) { + // only report a change if document actually changed. + if (docsEqual) { + [changeSet addChange:[FSTDocumentViewChange + changeWithDocument:newDoc + type:FSTDocumentViewChangeTypeMetadata]]; + } else { + [changeSet addChange:[FSTDocumentViewChange + changeWithDocument:newDoc + type:FSTDocumentViewChangeTypeModified]]; + } + + if (lastDocInLimit && self.query.comparator(newDoc, lastDocInLimit) > 0) { + // This doc moved from inside the limit to after the limit. That means there may be some + // doc in the local cache that's actually less than this one. + needsRefill = YES; + } + } + } else if (!oldDoc && newDoc) { + [changeSet + addChange:[FSTDocumentViewChange changeWithDocument:newDoc + type:FSTDocumentViewChangeTypeAdded]]; + } else if (oldDoc && !newDoc) { + [changeSet + addChange:[FSTDocumentViewChange changeWithDocument:oldDoc + type:FSTDocumentViewChangeTypeRemoved]]; + if (lastDocInLimit) { + // A doc was removed from a full limit query. We'll need to re-query from the local cache + // to see if we know about some other doc that should be in the results. + needsRefill = YES; + } + } + }]; + if (self.query.limit) { + // TODO(klimt): Make DocumentSet size be constant time. + while (newDocumentSet.count > self.query.limit) { + FSTDocument *oldDoc = [newDocumentSet lastDocument]; + newDocumentSet = [newDocumentSet documentSetByRemovingKey:oldDoc.key]; + [changeSet + addChange:[FSTDocumentViewChange changeWithDocument:oldDoc + type:FSTDocumentViewChangeTypeRemoved]]; + } + } + + FSTAssert(!needsRefill || !previousChanges, + @"View was refilled using docs that themselves needed refilling."); + + return [[FSTViewDocumentChanges alloc] initWithDocumentSet:newDocumentSet + changeSet:changeSet + needsRefill:needsRefill + mutatedKeys:newMutatedKeys]; +} + +- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges { + return [self applyChangesToDocuments:docChanges targetChange:nil]; +} + +- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges + targetChange:(nullable FSTTargetChange *)targetChange { + FSTAssert(!docChanges.needsRefill, @"Cannot apply changes that need a refill"); + + FSTDocumentSet *oldDocuments = self.documentSet; + self.documentSet = docChanges.documentSet; + self.mutatedKeys = docChanges.mutatedKeys; + + // Sort changes based on type and query comparator. + NSArray *changes = [docChanges.changeSet changes]; + changes = [changes sortedArrayUsingComparator:^NSComparisonResult(FSTDocumentViewChange *c1, + FSTDocumentViewChange *c2) { + NSComparisonResult typeComparison = FSTCompareDocumentViewChangeTypes(c1.type, c2.type); + if (typeComparison != NSOrderedSame) { + return typeComparison; + } + return self.query.comparator(c1.document, c2.document); + }]; + [self applyTargetChange:targetChange]; + NSArray *limboChanges = [self updateLimboDocuments]; + BOOL synced = self.limboDocuments.count == 0 && self.isCurrent; + FSTSyncState newSyncState = synced ? FSTSyncStateSynced : FSTSyncStateLocal; + BOOL syncStateChanged = newSyncState != self.syncState; + self.syncState = newSyncState; + + if (changes.count == 0 && !syncStateChanged) { + // No changes. + return [FSTViewChange changeWithSnapshot:nil limboChanges:limboChanges]; + } else { + FSTViewSnapshot *snapshot = + [[FSTViewSnapshot alloc] initWithQuery:self.query + documents:docChanges.documentSet + oldDocuments:oldDocuments + documentChanges:changes + fromCache:newSyncState == FSTSyncStateLocal + hasPendingWrites:!docChanges.mutatedKeys.isEmpty + syncStateChanged:syncStateChanged]; + + return [FSTViewChange changeWithSnapshot:snapshot limboChanges:limboChanges]; + } +} + +- (FSTViewChange *)applyChangedOnlineState:(FSTOnlineState)onlineState { + if (self.isCurrent && onlineState == FSTOnlineStateFailed) { + // If we're offline, set `current` to NO and then call applyChanges to refresh our syncState + // and generate an FSTViewChange as appropriate. We are guaranteed to get a new FSTTargetChange + // that sets `current` back to YES once the client is back online. + self.current = NO; + return + [self applyChangesToDocuments:[[FSTViewDocumentChanges alloc] + initWithDocumentSet:self.documentSet + changeSet:[FSTDocumentViewChangeSet changeSet] + needsRefill:NO + mutatedKeys:self.mutatedKeys]]; + } else { + // No effect, just return a no-op FSTViewChange. + return [[FSTViewChange alloc] initWithSnapshot:nil limboChanges:@[]]; + } +} + +#pragma mark - Private methods + +/** Returns whether the doc for the given key should be in limbo. */ +- (BOOL)shouldBeLimboDocumentKey:(FSTDocumentKey *)key { + // If the remote end says it's part of this query, it's not in limbo. + if ([self.syncedDocuments containsObject:key]) { + return NO; + } + // The local store doesn't think it's a result, so it shouldn't be in limbo. + if (![self.documentSet containsKey:key]) { + return NO; + } + // If there are local changes to the doc, they might explain why the server doesn't know that it's + // part of the query. So don't put it in limbo. + // TODO(klimt): Ideally, we would only consider changes that might actually affect this specific + // query. + if ([self.documentSet documentForKey:key].hasLocalMutations) { + return NO; + } + // Everything else is in limbo. + return YES; +} + +/** + * Updates syncedDocuments and current based on the given change. + */ +- (void)applyTargetChange:(nullable FSTTargetChange *)targetChange { + if (targetChange) { + FSTTargetMapping *targetMapping = targetChange.mapping; + if ([targetMapping isKindOfClass:[FSTResetMapping class]]) { + self.syncedDocuments = ((FSTResetMapping *)targetMapping).documents; + } else if ([targetMapping isKindOfClass:[FSTUpdateMapping class]]) { + [((FSTUpdateMapping *)targetMapping).addedDocuments + enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + self.syncedDocuments = [self.syncedDocuments setByAddingObject:key]; + }]; + [((FSTUpdateMapping *)targetMapping).removedDocuments + enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + self.syncedDocuments = [self.syncedDocuments setByRemovingObject:key]; + }]; + } + + switch (targetChange.currentStatusUpdate) { + case FSTCurrentStatusUpdateMarkCurrent: + self.current = YES; + break; + case FSTCurrentStatusUpdateMarkNotCurrent: + self.current = NO; + break; + case FSTCurrentStatusUpdateNone: + break; + } + } +} + +/** Updates limboDocuments and returns any changes as FSTLimboDocumentChanges. */ +- (NSArray *)updateLimboDocuments { + // We can only determine limbo documents when we're in-sync with the server. + if (!self.isCurrent) { + return @[]; + } + + // TODO(klimt): Do this incrementally so that it's not quadratic when updating many documents. + FSTDocumentKeySet *oldLimboDocuments = self.limboDocuments; + self.limboDocuments = [FSTDocumentKeySet keySet]; + for (FSTDocument *doc in self.documentSet.documentEnumerator) { + if ([self shouldBeLimboDocumentKey:doc.key]) { + self.limboDocuments = [self.limboDocuments setByAddingObject:doc.key]; + } + } + + // Diff the new limbo docs with the old limbo docs. + NSMutableArray *changes = + [NSMutableArray arrayWithCapacity:(oldLimboDocuments.count + self.limboDocuments.count)]; + [oldLimboDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + if (![self.limboDocuments containsObject:key]) { + [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved + key:key]]; + } + }]; + [self.limboDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + if (![oldLimboDocuments containsObject:key]) { + [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded + key:key]]; + } + }]; + return changes; +} + +@end + +static inline int DocumentViewChangeTypePosition(FSTDocumentViewChangeType changeType) { + switch (changeType) { + case FSTDocumentViewChangeTypeRemoved: + return 0; + case FSTDocumentViewChangeTypeAdded: + return 1; + case FSTDocumentViewChangeTypeModified: + return 2; + case FSTDocumentViewChangeTypeMetadata: + // A metadata change is converted to a modified change at the public API layer. Since we sort + // by document key and then change type, metadata and modified changes must be sorted + // equivalently. + return 2; + default: + FSTCFail(@"Unknown FSTDocumentViewChangeType %lu", (unsigned long)changeType); + } +} + +static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChangeType c1, + FSTDocumentViewChangeType c2) { + int pos1 = DocumentViewChangeTypePosition(c1); + int pos2 = DocumentViewChangeTypePosition(c2); + if (pos1 == pos2) { + return NSOrderedSame; + } else if (pos1 < pos2) { + return NSOrderedAscending; + } else { + return NSOrderedDescending; + } +} + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTViewSnapshot.m b/Firestore/Source/Core/FSTViewSnapshot.m deleted file mode 100644 index e60b785..0000000 --- a/Firestore/Source/Core/FSTViewSnapshot.m +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Core/FSTViewSnapshot.h" - -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/third_party/Immutable/FSTImmutableSortedDictionary.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTDocumentViewChange - -@interface FSTDocumentViewChange () - -+ (instancetype)changeWithDocument:(FSTDocument *)document type:(FSTDocumentViewChangeType)type; - -- (instancetype)initWithDocument:(FSTDocument *)document - type:(FSTDocumentViewChangeType)type NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FSTDocumentViewChange - -+ (instancetype)changeWithDocument:(FSTDocument *)document type:(FSTDocumentViewChangeType)type { - return [[FSTDocumentViewChange alloc] initWithDocument:document type:type]; -} - -- (instancetype)initWithDocument:(FSTDocument *)document type:(FSTDocumentViewChangeType)type { - self = [super init]; - if (self) { - _document = document; - _type = type; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (self == other) { - return YES; - } - if (![other isKindOfClass:[FSTDocumentViewChange class]]) { - return NO; - } - FSTDocumentViewChange *otherChange = (FSTDocumentViewChange *)other; - return [self.document isEqual:otherChange.document] && self.type == otherChange.type; -} - -- (NSString *)description { - return [NSString - stringWithFormat:@"", (long)self.type, self.document]; -} - -@end - -#pragma mark - FSTDocumentViewChangeSet - -@interface FSTDocumentViewChangeSet () - -/** The set of all changes tracked so far, with redundant changes merged. */ -@property(nonatomic, strong) - FSTImmutableSortedDictionary *changeMap; - -@end - -@implementation FSTDocumentViewChangeSet - -+ (instancetype)changeSet { - return [[FSTDocumentViewChangeSet alloc] init]; -} - -- (instancetype)init { - self = [super init]; - if (self) { - _changeMap = [FSTImmutableSortedDictionary dictionaryWithComparator:FSTDocumentKeyComparator]; - } - return self; -} - -- (NSString *)description { - return [self.changeMap description]; -} - -- (void)addChange:(FSTDocumentViewChange *)change { - FSTDocumentKey *key = change.document.key; - FSTDocumentViewChange *oldChange = [self.changeMap objectForKey:key]; - if (!oldChange) { - self.changeMap = [self.changeMap dictionaryBySettingObject:change forKey:key]; - return; - } - - // Merge the new change with the existing change. - if (change.type != FSTDocumentViewChangeTypeAdded && - oldChange.type == FSTDocumentViewChangeTypeMetadata) { - self.changeMap = [self.changeMap dictionaryBySettingObject:change forKey:key]; - - } else if (change.type == FSTDocumentViewChangeTypeMetadata && - oldChange.type != FSTDocumentViewChangeTypeRemoved) { - FSTDocumentViewChange *newChange = - [FSTDocumentViewChange changeWithDocument:change.document type:oldChange.type]; - self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; - - } else if (change.type == FSTDocumentViewChangeTypeModified && - oldChange.type == FSTDocumentViewChangeTypeModified) { - FSTDocumentViewChange *newChange = - [FSTDocumentViewChange changeWithDocument:change.document - type:FSTDocumentViewChangeTypeModified]; - self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; - } else if (change.type == FSTDocumentViewChangeTypeModified && - oldChange.type == FSTDocumentViewChangeTypeAdded) { - FSTDocumentViewChange *newChange = - [FSTDocumentViewChange changeWithDocument:change.document - type:FSTDocumentViewChangeTypeAdded]; - self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; - } else if (change.type == FSTDocumentViewChangeTypeRemoved && - oldChange.type == FSTDocumentViewChangeTypeAdded) { - self.changeMap = [self.changeMap dictionaryByRemovingObjectForKey:key]; - } else if (change.type == FSTDocumentViewChangeTypeRemoved && - oldChange.type == FSTDocumentViewChangeTypeModified) { - FSTDocumentViewChange *newChange = - [FSTDocumentViewChange changeWithDocument:oldChange.document - type:FSTDocumentViewChangeTypeRemoved]; - self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; - } else if (change.type == FSTDocumentViewChangeTypeAdded && - oldChange.type == FSTDocumentViewChangeTypeRemoved) { - FSTDocumentViewChange *newChange = - [FSTDocumentViewChange changeWithDocument:change.document - type:FSTDocumentViewChangeTypeModified]; - self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; - } else { - // This includes these cases, which don't make sense: - // Added -> Added - // Removed -> Removed - // Modified -> Added - // Removed -> Modified - // Metadata -> Added - // Removed -> Metadata - FSTFail(@"Unsupported combination of changes: %ld after %ld", (long)change.type, - (long)oldChange.type); - } -} - -- (NSArray *)changes { - NSMutableArray *changes = [NSMutableArray array]; - [self.changeMap enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, - FSTDocumentViewChange *change, BOOL *stop) { - [changes addObject:change]; - }]; - return changes; -} - -@end - -#pragma mark - FSTViewSnapshot - -@implementation FSTViewSnapshot - -- (instancetype)initWithQuery:(FSTQuery *)query - documents:(FSTDocumentSet *)documents - oldDocuments:(FSTDocumentSet *)oldDocuments - documentChanges:(NSArray *)documentChanges - fromCache:(BOOL)fromCache - hasPendingWrites:(BOOL)hasPendingWrites - syncStateChanged:(BOOL)syncStateChanged { - self = [super init]; - if (self) { - _query = query; - _documents = documents; - _oldDocuments = oldDocuments; - _documentChanges = documentChanges; - _fromCache = fromCache; - _hasPendingWrites = hasPendingWrites; - _syncStateChanged = syncStateChanged; - } - return self; -} - -- (NSString *)description { - return [NSString stringWithFormat: - @"", - self.query, self.documents, self.oldDocuments, self.documentChanges, - (self.fromCache ? @"YES" : @"NO"), (self.hasPendingWrites ? @"YES" : @"NO"), - (self.syncStateChanged ? @"YES" : @"NO")]; -} - -- (BOOL)isEqual:(id)object { - if (self == object) { - return YES; - } else if (![object isKindOfClass:[FSTViewSnapshot class]]) { - return NO; - } - - FSTViewSnapshot *other = object; - return [self.query isEqual:other.query] && [self.documents isEqual:other.documents] && - [self.oldDocuments isEqual:other.oldDocuments] && - [self.documentChanges isEqualToArray:other.documentChanges] && - self.fromCache == other.fromCache && self.hasPendingWrites == other.hasPendingWrites && - self.syncStateChanged == other.syncStateChanged; -} - -- (NSUInteger)hash { - NSUInteger result = [self.query hash]; - result = 31 * result + [self.documents hash]; - result = 31 * result + [self.oldDocuments hash]; - result = 31 * result + [self.documentChanges hash]; - result = 31 * result + (self.fromCache ? 1231 : 1237); - result = 31 * result + (self.hasPendingWrites ? 1231 : 1237); - result = 31 * result + (self.syncStateChanged ? 1231 : 1237); - return result; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTViewSnapshot.mm b/Firestore/Source/Core/FSTViewSnapshot.mm new file mode 100644 index 0000000..e60b785 --- /dev/null +++ b/Firestore/Source/Core/FSTViewSnapshot.mm @@ -0,0 +1,231 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Core/FSTViewSnapshot.h" + +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/third_party/Immutable/FSTImmutableSortedDictionary.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTDocumentViewChange + +@interface FSTDocumentViewChange () + ++ (instancetype)changeWithDocument:(FSTDocument *)document type:(FSTDocumentViewChangeType)type; + +- (instancetype)initWithDocument:(FSTDocument *)document + type:(FSTDocumentViewChangeType)type NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTDocumentViewChange + ++ (instancetype)changeWithDocument:(FSTDocument *)document type:(FSTDocumentViewChangeType)type { + return [[FSTDocumentViewChange alloc] initWithDocument:document type:type]; +} + +- (instancetype)initWithDocument:(FSTDocument *)document type:(FSTDocumentViewChangeType)type { + self = [super init]; + if (self) { + _document = document; + _type = type; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } + if (![other isKindOfClass:[FSTDocumentViewChange class]]) { + return NO; + } + FSTDocumentViewChange *otherChange = (FSTDocumentViewChange *)other; + return [self.document isEqual:otherChange.document] && self.type == otherChange.type; +} + +- (NSString *)description { + return [NSString + stringWithFormat:@"", (long)self.type, self.document]; +} + +@end + +#pragma mark - FSTDocumentViewChangeSet + +@interface FSTDocumentViewChangeSet () + +/** The set of all changes tracked so far, with redundant changes merged. */ +@property(nonatomic, strong) + FSTImmutableSortedDictionary *changeMap; + +@end + +@implementation FSTDocumentViewChangeSet + ++ (instancetype)changeSet { + return [[FSTDocumentViewChangeSet alloc] init]; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _changeMap = [FSTImmutableSortedDictionary dictionaryWithComparator:FSTDocumentKeyComparator]; + } + return self; +} + +- (NSString *)description { + return [self.changeMap description]; +} + +- (void)addChange:(FSTDocumentViewChange *)change { + FSTDocumentKey *key = change.document.key; + FSTDocumentViewChange *oldChange = [self.changeMap objectForKey:key]; + if (!oldChange) { + self.changeMap = [self.changeMap dictionaryBySettingObject:change forKey:key]; + return; + } + + // Merge the new change with the existing change. + if (change.type != FSTDocumentViewChangeTypeAdded && + oldChange.type == FSTDocumentViewChangeTypeMetadata) { + self.changeMap = [self.changeMap dictionaryBySettingObject:change forKey:key]; + + } else if (change.type == FSTDocumentViewChangeTypeMetadata && + oldChange.type != FSTDocumentViewChangeTypeRemoved) { + FSTDocumentViewChange *newChange = + [FSTDocumentViewChange changeWithDocument:change.document type:oldChange.type]; + self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; + + } else if (change.type == FSTDocumentViewChangeTypeModified && + oldChange.type == FSTDocumentViewChangeTypeModified) { + FSTDocumentViewChange *newChange = + [FSTDocumentViewChange changeWithDocument:change.document + type:FSTDocumentViewChangeTypeModified]; + self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; + } else if (change.type == FSTDocumentViewChangeTypeModified && + oldChange.type == FSTDocumentViewChangeTypeAdded) { + FSTDocumentViewChange *newChange = + [FSTDocumentViewChange changeWithDocument:change.document + type:FSTDocumentViewChangeTypeAdded]; + self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; + } else if (change.type == FSTDocumentViewChangeTypeRemoved && + oldChange.type == FSTDocumentViewChangeTypeAdded) { + self.changeMap = [self.changeMap dictionaryByRemovingObjectForKey:key]; + } else if (change.type == FSTDocumentViewChangeTypeRemoved && + oldChange.type == FSTDocumentViewChangeTypeModified) { + FSTDocumentViewChange *newChange = + [FSTDocumentViewChange changeWithDocument:oldChange.document + type:FSTDocumentViewChangeTypeRemoved]; + self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; + } else if (change.type == FSTDocumentViewChangeTypeAdded && + oldChange.type == FSTDocumentViewChangeTypeRemoved) { + FSTDocumentViewChange *newChange = + [FSTDocumentViewChange changeWithDocument:change.document + type:FSTDocumentViewChangeTypeModified]; + self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; + } else { + // This includes these cases, which don't make sense: + // Added -> Added + // Removed -> Removed + // Modified -> Added + // Removed -> Modified + // Metadata -> Added + // Removed -> Metadata + FSTFail(@"Unsupported combination of changes: %ld after %ld", (long)change.type, + (long)oldChange.type); + } +} + +- (NSArray *)changes { + NSMutableArray *changes = [NSMutableArray array]; + [self.changeMap enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, + FSTDocumentViewChange *change, BOOL *stop) { + [changes addObject:change]; + }]; + return changes; +} + +@end + +#pragma mark - FSTViewSnapshot + +@implementation FSTViewSnapshot + +- (instancetype)initWithQuery:(FSTQuery *)query + documents:(FSTDocumentSet *)documents + oldDocuments:(FSTDocumentSet *)oldDocuments + documentChanges:(NSArray *)documentChanges + fromCache:(BOOL)fromCache + hasPendingWrites:(BOOL)hasPendingWrites + syncStateChanged:(BOOL)syncStateChanged { + self = [super init]; + if (self) { + _query = query; + _documents = documents; + _oldDocuments = oldDocuments; + _documentChanges = documentChanges; + _fromCache = fromCache; + _hasPendingWrites = hasPendingWrites; + _syncStateChanged = syncStateChanged; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat: + @"", + self.query, self.documents, self.oldDocuments, self.documentChanges, + (self.fromCache ? @"YES" : @"NO"), (self.hasPendingWrites ? @"YES" : @"NO"), + (self.syncStateChanged ? @"YES" : @"NO")]; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } else if (![object isKindOfClass:[FSTViewSnapshot class]]) { + return NO; + } + + FSTViewSnapshot *other = object; + return [self.query isEqual:other.query] && [self.documents isEqual:other.documents] && + [self.oldDocuments isEqual:other.oldDocuments] && + [self.documentChanges isEqualToArray:other.documentChanges] && + self.fromCache == other.fromCache && self.hasPendingWrites == other.hasPendingWrites && + self.syncStateChanged == other.syncStateChanged; +} + +- (NSUInteger)hash { + NSUInteger result = [self.query hash]; + result = 31 * result + [self.documents hash]; + result = 31 * result + [self.oldDocuments hash]; + result = 31 * result + [self.documentChanges hash]; + result = 31 * result + (self.fromCache ? 1231 : 1237); + result = 31 * result + (self.hasPendingWrites ? 1231 : 1237); + result = 31 * result + (self.syncStateChanged ? 1231 : 1237); + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTEagerGarbageCollector.m b/Firestore/Source/Local/FSTEagerGarbageCollector.m deleted file mode 100644 index 77a577e..0000000 --- a/Firestore/Source/Local/FSTEagerGarbageCollector.m +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTEagerGarbageCollector.h" - -#import "Firestore/Source/Model/FSTDocumentKey.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTMultiReferenceSet - -@interface FSTEagerGarbageCollector () - -/** The garbage collectible sources to double-check during garbage collection. */ -@property(nonatomic, strong, readonly) NSMutableArray> *sources; - -/** A set of potentially garbage keys. */ -@property(nonatomic, strong, readonly) NSMutableSet *potentialGarbage; - -@end - -@implementation FSTEagerGarbageCollector - -- (instancetype)init { - self = [super init]; - if (self) { - _sources = [NSMutableArray array]; - _potentialGarbage = [[NSMutableSet alloc] init]; - } - return self; -} - -- (BOOL)isEager { - return YES; -} - -- (void)addGarbageSource:(id)garbageSource { - [self.sources addObject:garbageSource]; - garbageSource.garbageCollector = self; -} - -- (void)removeGarbageSource:(id)garbageSource { - [self.sources removeObject:garbageSource]; - garbageSource.garbageCollector = nil; -} - -- (void)addPotentialGarbageKey:(FSTDocumentKey *)key { - [self.potentialGarbage addObject:key]; -} - -- (NSMutableSet *)collectGarbage { - NSMutableArray> *sources = self.sources; - - NSMutableSet *actualGarbage = [NSMutableSet set]; - for (FSTDocumentKey *key in self.potentialGarbage) { - BOOL isGarbage = YES; - for (id source in sources) { - if ([source containsKey:key]) { - isGarbage = NO; - break; - } - } - - if (isGarbage) { - [actualGarbage addObject:key]; - } - } - - // Clear locally retained potential keys and returned confirmed garbage. - [self.potentialGarbage removeAllObjects]; - return actualGarbage; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTEagerGarbageCollector.mm b/Firestore/Source/Local/FSTEagerGarbageCollector.mm new file mode 100644 index 0000000..77a577e --- /dev/null +++ b/Firestore/Source/Local/FSTEagerGarbageCollector.mm @@ -0,0 +1,89 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTEagerGarbageCollector.h" + +#import "Firestore/Source/Model/FSTDocumentKey.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTMultiReferenceSet + +@interface FSTEagerGarbageCollector () + +/** The garbage collectible sources to double-check during garbage collection. */ +@property(nonatomic, strong, readonly) NSMutableArray> *sources; + +/** A set of potentially garbage keys. */ +@property(nonatomic, strong, readonly) NSMutableSet *potentialGarbage; + +@end + +@implementation FSTEagerGarbageCollector + +- (instancetype)init { + self = [super init]; + if (self) { + _sources = [NSMutableArray array]; + _potentialGarbage = [[NSMutableSet alloc] init]; + } + return self; +} + +- (BOOL)isEager { + return YES; +} + +- (void)addGarbageSource:(id)garbageSource { + [self.sources addObject:garbageSource]; + garbageSource.garbageCollector = self; +} + +- (void)removeGarbageSource:(id)garbageSource { + [self.sources removeObject:garbageSource]; + garbageSource.garbageCollector = nil; +} + +- (void)addPotentialGarbageKey:(FSTDocumentKey *)key { + [self.potentialGarbage addObject:key]; +} + +- (NSMutableSet *)collectGarbage { + NSMutableArray> *sources = self.sources; + + NSMutableSet *actualGarbage = [NSMutableSet set]; + for (FSTDocumentKey *key in self.potentialGarbage) { + BOOL isGarbage = YES; + for (id source in sources) { + if ([source containsKey:key]) { + isGarbage = NO; + break; + } + } + + if (isGarbage) { + [actualGarbage addObject:key]; + } + } + + // Clear locally retained potential keys and returned confirmed garbage. + [self.potentialGarbage removeAllObjects]; + return actualGarbage; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDB.h b/Firestore/Source/Local/FSTLevelDB.h index 6819116..520557a 100644 --- a/Firestore/Source/Local/FSTLevelDB.h +++ b/Firestore/Source/Local/FSTLevelDB.h @@ -16,17 +16,10 @@ #import -#import "Firestore/Source/Local/FSTPersistence.h" - -#ifdef __cplusplus #include -namespace leveldb { -class DB; -class ReadOptions; -class Status; -} -#endif +#import "Firestore/Source/Local/FSTPersistence.h" +#include "leveldb/db.h" @class FSTDatabaseInfo; @class FSTLocalSerializer; @@ -69,7 +62,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (BOOL)start:(NSError **)error; -#ifdef __cplusplus // What follows is the Objective-C++ extension to the API. /** * @return A standard set of read options @@ -103,8 +95,6 @@ NS_ASSUME_NONNULL_BEGIN /** The native db pointer, allocated during start. */ @property(nonatomic, assign, readonly) std::shared_ptr ptr; -#endif - @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBKey.h b/Firestore/Source/Local/FSTLevelDBKey.h index e5e7fbb..f3f4bcf 100644 --- a/Firestore/Source/Local/FSTLevelDBKey.h +++ b/Firestore/Source/Local/FSTLevelDBKey.h @@ -14,10 +14,6 @@ * limitations under the License. */ -#ifndef __cplusplus -#error "FSTLevelDBKey is Objective-C++ and can only be included from .mm files" -#endif - #import #import "Firestore/Source/Core/FSTTypes.h" diff --git a/Firestore/Source/Local/FSTLevelDBMigrations.h b/Firestore/Source/Local/FSTLevelDBMigrations.h index 46c7c93..24fb5c8 100644 --- a/Firestore/Source/Local/FSTLevelDBMigrations.h +++ b/Firestore/Source/Local/FSTLevelDBMigrations.h @@ -15,14 +15,10 @@ */ #import -#include -#ifdef __cplusplus +#include -namespace leveldb { -class DB; -} -#endif +#include "leveldb/db.h" NS_ASSUME_NONNULL_BEGIN diff --git a/Firestore/Source/Local/FSTLevelDBMutationQueue.h b/Firestore/Source/Local/FSTLevelDBMutationQueue.h index dd2ed4f..cc05db7 100644 --- a/Firestore/Source/Local/FSTLevelDBMutationQueue.h +++ b/Firestore/Source/Local/FSTLevelDBMutationQueue.h @@ -16,15 +16,10 @@ #import -#import "Firestore/Source/Local/FSTMutationQueue.h" - -#ifdef __cplusplus #include -namespace leveldb { -class DB; -} -#endif +#import "Firestore/Source/Local/FSTMutationQueue.h" +#include "leveldb/db.h" @class FSTLevelDB; @class FSTLocalSerializer; @@ -41,7 +36,6 @@ NS_ASSUME_NONNULL_BEGIN /** The garbage collector to notify about potential garbage keys. */ @property(nonatomic, weak, readwrite, nullable) id garbageCollector; -#ifdef __cplusplus /** * Creates a new mutation queue for the given user, in the given LevelDB. * @@ -57,7 +51,6 @@ NS_ASSUME_NONNULL_BEGIN * returns 0. Note that batch IDs are global. */ + (FSTBatchID)loadNextBatchIDFromDB:(std::shared_ptr)db; -#endif @end diff --git a/Firestore/Source/Local/FSTLevelDBQueryCache.h b/Firestore/Source/Local/FSTLevelDBQueryCache.h index 67c6575..1f6fbd4 100644 --- a/Firestore/Source/Local/FSTLevelDBQueryCache.h +++ b/Firestore/Source/Local/FSTLevelDBQueryCache.h @@ -16,38 +16,30 @@ #import -#import "Firestore/Source/Local/FSTQueryCache.h" - -#ifdef __cplusplus #include -namespace leveldb { -class DB; -} -#endif +#import "Firestore/Source/Local/FSTQueryCache.h" +#include "leveldb/db.h" @class FSTLocalSerializer; -@protocol FSTGarbageCollector; @class FSTPBTargetGlobal; +@protocol FSTGarbageCollector; NS_ASSUME_NONNULL_BEGIN /** Cached Queries backed by LevelDB. */ @interface FSTLevelDBQueryCache : NSObject -#ifdef __cplusplus /** * Retrieves the global singleton metadata row from the given database, if it exists. */ + (nullable FSTPBTargetGlobal *)readTargetMetadataFromDB:(std::shared_ptr)db; -#endif - (instancetype)init NS_UNAVAILABLE; /** The garbage collector to notify about potential garbage keys. */ @property(nonatomic, weak, readwrite, nullable) id garbageCollector; -#ifdef __cplusplus /** * Creates a new query cache in the given LevelDB. * @@ -55,7 +47,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (instancetype)initWithDB:(std::shared_ptr)db serializer:(FSTLocalSerializer *)serializer NS_DESIGNATED_INITIALIZER; -#endif @end diff --git a/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.h b/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.h index 1da3cca..20942e2 100644 --- a/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.h +++ b/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.h @@ -16,15 +16,10 @@ #import -#import "Firestore/Source/Local/FSTRemoteDocumentCache.h" - -#ifdef __cplusplus #include -namespace leveldb { -class DB; -} -#endif +#import "Firestore/Source/Local/FSTRemoteDocumentCache.h" +#include "leveldb/db.h" @class FSTLocalSerializer; @@ -35,7 +30,6 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; -#ifdef __cplusplus /** * Creates a new remote documents cache in the given leveldb. * @@ -43,7 +37,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (instancetype)initWithDB:(std::shared_ptr)db serializer:(FSTLocalSerializer *)serializer NS_DESIGNATED_INITIALIZER; -#endif @end diff --git a/Firestore/Source/Local/FSTLocalDocumentsView.m b/Firestore/Source/Local/FSTLocalDocumentsView.m deleted file mode 100644 index 0e88958..0000000 --- a/Firestore/Source/Local/FSTLocalDocumentsView.m +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTLocalDocumentsView.h" - -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Local/FSTMutationQueue.h" -#import "Firestore/Source/Local/FSTRemoteDocumentCache.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentDictionary.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTLocalDocumentsView () -- (instancetype)initWithRemoteDocumentCache:(id)remoteDocumentCache - mutationQueue:(id)mutationQueue - NS_DESIGNATED_INITIALIZER; -@property(nonatomic, strong, readonly) id remoteDocumentCache; -@property(nonatomic, strong, readonly) id mutationQueue; -@end - -@implementation FSTLocalDocumentsView - -+ (instancetype)viewWithRemoteDocumentCache:(id)remoteDocumentCache - mutationQueue:(id)mutationQueue { - return [[FSTLocalDocumentsView alloc] initWithRemoteDocumentCache:remoteDocumentCache - mutationQueue:mutationQueue]; -} - -- (instancetype)initWithRemoteDocumentCache:(id)remoteDocumentCache - mutationQueue:(id)mutationQueue { - if (self = [super init]) { - _remoteDocumentCache = remoteDocumentCache; - _mutationQueue = mutationQueue; - } - return self; -} - -- (nullable FSTMaybeDocument *)documentForKey:(FSTDocumentKey *)key { - FSTMaybeDocument *_Nullable remoteDoc = [self.remoteDocumentCache entryForKey:key]; - return [self localDocument:remoteDoc key:key]; -} - -- (FSTMaybeDocumentDictionary *)documentsForKeys:(FSTDocumentKeySet *)keys { - FSTMaybeDocumentDictionary *results = [FSTMaybeDocumentDictionary maybeDocumentDictionary]; - for (FSTDocumentKey *key in keys.objectEnumerator) { - // TODO(mikelehen): PERF: Consider fetching all remote documents at once rather than one-by-one. - FSTMaybeDocument *maybeDoc = [self documentForKey:key]; - // TODO(http://b/32275378): Don't conflate missing / deleted. - if (!maybeDoc) { - maybeDoc = [FSTDeletedDocument documentWithKey:key version:[FSTSnapshotVersion noVersion]]; - } - results = [results dictionaryBySettingObject:maybeDoc forKey:key]; - } - return results; -} - -- (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query { - if ([FSTDocumentKey isDocumentKey:query.path]) { - return [self documentsMatchingDocumentQuery:query.path]; - } else { - return [self documentsMatchingCollectionQuery:query]; - } -} - -- (FSTDocumentDictionary *)documentsMatchingDocumentQuery:(FSTResourcePath *)docPath { - FSTDocumentDictionary *result = [FSTDocumentDictionary documentDictionary]; - // Just do a simple document lookup. - FSTMaybeDocument *doc = [self documentForKey:[FSTDocumentKey keyWithPath:docPath]]; - if ([doc isKindOfClass:[FSTDocument class]]) { - result = [result dictionaryBySettingObject:(FSTDocument *)doc forKey:doc.key]; - } - return result; -} - -- (FSTDocumentDictionary *)documentsMatchingCollectionQuery:(FSTQuery *)query { - // Query the remote documents and overlay mutations. - // TODO(mikelehen): There may be significant overlap between the mutations affecting these - // remote documents and the allMutationBatchesAffectingQuery mutations. Consider optimizing. - __block FSTDocumentDictionary *results = [self.remoteDocumentCache documentsMatchingQuery:query]; - results = [self localDocuments:results]; - - // Now use the mutation queue to discover any other documents that may match the query after - // applying mutations. - FSTDocumentKeySet *matchingKeys = [FSTDocumentKeySet keySet]; - NSArray *matchingMutationBatches = - [self.mutationQueue allMutationBatchesAffectingQuery:query]; - for (FSTMutationBatch *batch in matchingMutationBatches) { - for (FSTMutation *mutation in batch.mutations) { - // TODO(mikelehen): PERF: Check if this mutation actually affects the query to reduce work. - - // If the key is already in the results, we can skip it. - if (![results containsKey:mutation.key]) { - matchingKeys = [matchingKeys setByAddingObject:mutation.key]; - } - } - } - - // Now add in results for the matchingKeys. - for (FSTDocumentKey *key in matchingKeys.objectEnumerator) { - FSTMaybeDocument *doc = [self documentForKey:key]; - if ([doc isKindOfClass:[FSTDocument class]]) { - results = [results dictionaryBySettingObject:(FSTDocument *)doc forKey:key]; - } - } - - // Finally, filter out any documents that don't actually match the query. Note that the extra - // reference here prevents ARC from deallocating the initial unfiltered results while we're - // enumerating them. - FSTDocumentDictionary *unfiltered = results; - [unfiltered - enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, FSTDocument *doc, BOOL *stop) { - if (![query matchesDocument:doc]) { - results = [results dictionaryByRemovingObjectForKey:key]; - } - }]; - - return results; -} - -/** - * Takes a remote document and applies local mutations to generate the local view of the - * document. - * - * @param document The base remote document to apply mutations to. - * @param documentKey The key of the document (necessary when remoteDocument is nil). - */ -- (nullable FSTMaybeDocument *)localDocument:(nullable FSTMaybeDocument *)document - key:(FSTDocumentKey *)documentKey { - NSArray *batches = - [self.mutationQueue allMutationBatchesAffectingDocumentKey:documentKey]; - for (FSTMutationBatch *batch in batches) { - document = [batch applyTo:document documentKey:documentKey]; - } - - return document; -} - -/** - * Takes a set of remote documents and applies local mutations to generate the local view of - * the documents. - * - * @param documents The base remote documents to apply mutations to. - * @return The local view of the documents. - */ -- (FSTDocumentDictionary *)localDocuments:(FSTDocumentDictionary *)documents { - __block FSTDocumentDictionary *result = documents; - [documents enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, FSTDocument *remoteDocument, - BOOL *stop) { - FSTMaybeDocument *mutatedDoc = [self localDocument:remoteDocument key:key]; - if ([mutatedDoc isKindOfClass:[FSTDeletedDocument class]]) { - result = [result dictionaryByRemovingObjectForKey:key]; - } else if ([mutatedDoc isKindOfClass:[FSTDocument class]]) { - result = [result dictionaryBySettingObject:(FSTDocument *)mutatedDoc forKey:key]; - } else { - FSTFail(@"Unknown document: %@", mutatedDoc); - } - }]; - return result; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalDocumentsView.mm b/Firestore/Source/Local/FSTLocalDocumentsView.mm new file mode 100644 index 0000000..0e88958 --- /dev/null +++ b/Firestore/Source/Local/FSTLocalDocumentsView.mm @@ -0,0 +1,182 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTLocalDocumentsView.h" + +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Local/FSTMutationQueue.h" +#import "Firestore/Source/Local/FSTRemoteDocumentCache.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentDictionary.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Model/FSTMutationBatch.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTLocalDocumentsView () +- (instancetype)initWithRemoteDocumentCache:(id)remoteDocumentCache + mutationQueue:(id)mutationQueue + NS_DESIGNATED_INITIALIZER; +@property(nonatomic, strong, readonly) id remoteDocumentCache; +@property(nonatomic, strong, readonly) id mutationQueue; +@end + +@implementation FSTLocalDocumentsView + ++ (instancetype)viewWithRemoteDocumentCache:(id)remoteDocumentCache + mutationQueue:(id)mutationQueue { + return [[FSTLocalDocumentsView alloc] initWithRemoteDocumentCache:remoteDocumentCache + mutationQueue:mutationQueue]; +} + +- (instancetype)initWithRemoteDocumentCache:(id)remoteDocumentCache + mutationQueue:(id)mutationQueue { + if (self = [super init]) { + _remoteDocumentCache = remoteDocumentCache; + _mutationQueue = mutationQueue; + } + return self; +} + +- (nullable FSTMaybeDocument *)documentForKey:(FSTDocumentKey *)key { + FSTMaybeDocument *_Nullable remoteDoc = [self.remoteDocumentCache entryForKey:key]; + return [self localDocument:remoteDoc key:key]; +} + +- (FSTMaybeDocumentDictionary *)documentsForKeys:(FSTDocumentKeySet *)keys { + FSTMaybeDocumentDictionary *results = [FSTMaybeDocumentDictionary maybeDocumentDictionary]; + for (FSTDocumentKey *key in keys.objectEnumerator) { + // TODO(mikelehen): PERF: Consider fetching all remote documents at once rather than one-by-one. + FSTMaybeDocument *maybeDoc = [self documentForKey:key]; + // TODO(http://b/32275378): Don't conflate missing / deleted. + if (!maybeDoc) { + maybeDoc = [FSTDeletedDocument documentWithKey:key version:[FSTSnapshotVersion noVersion]]; + } + results = [results dictionaryBySettingObject:maybeDoc forKey:key]; + } + return results; +} + +- (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query { + if ([FSTDocumentKey isDocumentKey:query.path]) { + return [self documentsMatchingDocumentQuery:query.path]; + } else { + return [self documentsMatchingCollectionQuery:query]; + } +} + +- (FSTDocumentDictionary *)documentsMatchingDocumentQuery:(FSTResourcePath *)docPath { + FSTDocumentDictionary *result = [FSTDocumentDictionary documentDictionary]; + // Just do a simple document lookup. + FSTMaybeDocument *doc = [self documentForKey:[FSTDocumentKey keyWithPath:docPath]]; + if ([doc isKindOfClass:[FSTDocument class]]) { + result = [result dictionaryBySettingObject:(FSTDocument *)doc forKey:doc.key]; + } + return result; +} + +- (FSTDocumentDictionary *)documentsMatchingCollectionQuery:(FSTQuery *)query { + // Query the remote documents and overlay mutations. + // TODO(mikelehen): There may be significant overlap between the mutations affecting these + // remote documents and the allMutationBatchesAffectingQuery mutations. Consider optimizing. + __block FSTDocumentDictionary *results = [self.remoteDocumentCache documentsMatchingQuery:query]; + results = [self localDocuments:results]; + + // Now use the mutation queue to discover any other documents that may match the query after + // applying mutations. + FSTDocumentKeySet *matchingKeys = [FSTDocumentKeySet keySet]; + NSArray *matchingMutationBatches = + [self.mutationQueue allMutationBatchesAffectingQuery:query]; + for (FSTMutationBatch *batch in matchingMutationBatches) { + for (FSTMutation *mutation in batch.mutations) { + // TODO(mikelehen): PERF: Check if this mutation actually affects the query to reduce work. + + // If the key is already in the results, we can skip it. + if (![results containsKey:mutation.key]) { + matchingKeys = [matchingKeys setByAddingObject:mutation.key]; + } + } + } + + // Now add in results for the matchingKeys. + for (FSTDocumentKey *key in matchingKeys.objectEnumerator) { + FSTMaybeDocument *doc = [self documentForKey:key]; + if ([doc isKindOfClass:[FSTDocument class]]) { + results = [results dictionaryBySettingObject:(FSTDocument *)doc forKey:key]; + } + } + + // Finally, filter out any documents that don't actually match the query. Note that the extra + // reference here prevents ARC from deallocating the initial unfiltered results while we're + // enumerating them. + FSTDocumentDictionary *unfiltered = results; + [unfiltered + enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, FSTDocument *doc, BOOL *stop) { + if (![query matchesDocument:doc]) { + results = [results dictionaryByRemovingObjectForKey:key]; + } + }]; + + return results; +} + +/** + * Takes a remote document and applies local mutations to generate the local view of the + * document. + * + * @param document The base remote document to apply mutations to. + * @param documentKey The key of the document (necessary when remoteDocument is nil). + */ +- (nullable FSTMaybeDocument *)localDocument:(nullable FSTMaybeDocument *)document + key:(FSTDocumentKey *)documentKey { + NSArray *batches = + [self.mutationQueue allMutationBatchesAffectingDocumentKey:documentKey]; + for (FSTMutationBatch *batch in batches) { + document = [batch applyTo:document documentKey:documentKey]; + } + + return document; +} + +/** + * Takes a set of remote documents and applies local mutations to generate the local view of + * the documents. + * + * @param documents The base remote documents to apply mutations to. + * @return The local view of the documents. + */ +- (FSTDocumentDictionary *)localDocuments:(FSTDocumentDictionary *)documents { + __block FSTDocumentDictionary *result = documents; + [documents enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, FSTDocument *remoteDocument, + BOOL *stop) { + FSTMaybeDocument *mutatedDoc = [self localDocument:remoteDocument key:key]; + if ([mutatedDoc isKindOfClass:[FSTDeletedDocument class]]) { + result = [result dictionaryByRemovingObjectForKey:key]; + } else if ([mutatedDoc isKindOfClass:[FSTDocument class]]) { + result = [result dictionaryBySettingObject:(FSTDocument *)mutatedDoc forKey:key]; + } else { + FSTFail(@"Unknown document: %@", mutatedDoc); + } + }]; + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalSerializer.m b/Firestore/Source/Local/FSTLocalSerializer.m deleted file mode 100644 index 82aec4d..0000000 --- a/Firestore/Source/Local/FSTLocalSerializer.m +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTLocalSerializer.h" - -#import "Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h" -#import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" -#import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h" -#import "Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Remote/FSTSerializerBeta.h" -#import "Firestore/Source/Util/FSTAssert.h" - -@interface FSTLocalSerializer () - -@property(nonatomic, strong, readonly) FSTSerializerBeta *remoteSerializer; - -@end - -/** Serializer for values stored in the LocalStore. */ -@implementation FSTLocalSerializer - -- (instancetype)initWithRemoteSerializer:(FSTSerializerBeta *)remoteSerializer { - self = [super init]; - if (self) { - _remoteSerializer = remoteSerializer; - } - return self; -} - -- (FSTPBMaybeDocument *)encodedMaybeDocument:(FSTMaybeDocument *)document { - FSTPBMaybeDocument *proto = [FSTPBMaybeDocument message]; - - if ([document isKindOfClass:[FSTDeletedDocument class]]) { - proto.noDocument = [self encodedDeletedDocument:(FSTDeletedDocument *)document]; - } else if ([document isKindOfClass:[FSTDocument class]]) { - proto.document = [self encodedDocument:(FSTDocument *)document]; - } else { - FSTFail(@"Unknown document type %@", NSStringFromClass([document class])); - } - - return proto; -} - -- (FSTMaybeDocument *)decodedMaybeDocument:(FSTPBMaybeDocument *)proto { - switch (proto.documentTypeOneOfCase) { - case FSTPBMaybeDocument_DocumentType_OneOfCase_Document: - return [self decodedDocument:proto.document]; - - case FSTPBMaybeDocument_DocumentType_OneOfCase_NoDocument: - return [self decodedDeletedDocument:proto.noDocument]; - - default: - FSTFail(@"Unknown MaybeDocument %@", proto); - } -} - -/** - * Encodes a Document for local storage. This differs from the v1beta1 RPC serializer for - * Documents in that it preserves the updateTime, which is considered an output only value by the - * server. - */ -- (GCFSDocument *)encodedDocument:(FSTDocument *)document { - FSTSerializerBeta *remoteSerializer = self.remoteSerializer; - - GCFSDocument *proto = [GCFSDocument message]; - proto.name = [remoteSerializer encodedDocumentKey:document.key]; - proto.fields = [remoteSerializer encodedFields:document.data]; - proto.updateTime = [remoteSerializer encodedVersion:document.version]; - - return proto; -} - -/** Decodes a Document proto to the equivalent model. */ -- (FSTDocument *)decodedDocument:(GCFSDocument *)document { - FSTSerializerBeta *remoteSerializer = self.remoteSerializer; - - FSTObjectValue *data = [remoteSerializer decodedFields:document.fields]; - FSTDocumentKey *key = [remoteSerializer decodedDocumentKey:document.name]; - FSTSnapshotVersion *version = [remoteSerializer decodedVersion:document.updateTime]; - return [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO]; -} - -/** Encodes a NoDocument value to the equivalent proto. */ -- (FSTPBNoDocument *)encodedDeletedDocument:(FSTDeletedDocument *)document { - FSTSerializerBeta *remoteSerializer = self.remoteSerializer; - - FSTPBNoDocument *proto = [FSTPBNoDocument message]; - proto.name = [remoteSerializer encodedDocumentKey:document.key]; - proto.readTime = [remoteSerializer encodedVersion:document.version]; - return proto; -} - -/** Decodes a NoDocument proto to the equivalent model. */ -- (FSTDeletedDocument *)decodedDeletedDocument:(FSTPBNoDocument *)proto { - FSTSerializerBeta *remoteSerializer = self.remoteSerializer; - - FSTDocumentKey *key = [remoteSerializer decodedDocumentKey:proto.name]; - FSTSnapshotVersion *version = [remoteSerializer decodedVersion:proto.readTime]; - return [FSTDeletedDocument documentWithKey:key version:version]; -} - -- (FSTPBWriteBatch *)encodedMutationBatch:(FSTMutationBatch *)batch { - FSTSerializerBeta *remoteSerializer = self.remoteSerializer; - - FSTPBWriteBatch *proto = [FSTPBWriteBatch message]; - proto.batchId = batch.batchID; - proto.localWriteTime = [remoteSerializer encodedTimestamp:batch.localWriteTime]; - - NSMutableArray *writes = proto.writesArray; - for (FSTMutation *mutation in batch.mutations) { - [writes addObject:[remoteSerializer encodedMutation:mutation]]; - } - return proto; -} - -- (FSTMutationBatch *)decodedMutationBatch:(FSTPBWriteBatch *)batch { - FSTSerializerBeta *remoteSerializer = self.remoteSerializer; - - int batchID = batch.batchId; - NSMutableArray *mutations = [NSMutableArray array]; - for (GCFSWrite *write in batch.writesArray) { - [mutations addObject:[remoteSerializer decodedMutation:write]]; - } - - FSTTimestamp *localWriteTime = [remoteSerializer decodedTimestamp:batch.localWriteTime]; - - return [[FSTMutationBatch alloc] initWithBatchID:batchID - localWriteTime:localWriteTime - mutations:mutations]; -} - -- (FSTPBTarget *)encodedQueryData:(FSTQueryData *)queryData { - FSTSerializerBeta *remoteSerializer = self.remoteSerializer; - - FSTAssert(queryData.purpose == FSTQueryPurposeListen, - @"only queries with purpose %lu may be stored, got %lu", - (unsigned long)FSTQueryPurposeListen, (unsigned long)queryData.purpose); - - FSTPBTarget *proto = [FSTPBTarget message]; - proto.targetId = queryData.targetID; - proto.lastListenSequenceNumber = queryData.sequenceNumber; - proto.snapshotVersion = [remoteSerializer encodedVersion:queryData.snapshotVersion]; - proto.resumeToken = queryData.resumeToken; - - FSTQuery *query = queryData.query; - if ([query isDocumentQuery]) { - proto.documents = [remoteSerializer encodedDocumentsTarget:query]; - } else { - proto.query = [remoteSerializer encodedQueryTarget:query]; - } - - return proto; -} - -- (FSTQueryData *)decodedQueryData:(FSTPBTarget *)target { - FSTSerializerBeta *remoteSerializer = self.remoteSerializer; - - FSTTargetID targetID = target.targetId; - FSTListenSequenceNumber sequenceNumber = target.lastListenSequenceNumber; - FSTSnapshotVersion *version = [remoteSerializer decodedVersion:target.snapshotVersion]; - NSData *resumeToken = target.resumeToken; - - FSTQuery *query; - switch (target.targetTypeOneOfCase) { - case FSTPBTarget_TargetType_OneOfCase_Documents: - query = [remoteSerializer decodedQueryFromDocumentsTarget:target.documents]; - break; - - case FSTPBTarget_TargetType_OneOfCase_Query: - query = [remoteSerializer decodedQueryFromQueryTarget:target.query]; - break; - - default: - FSTFail(@"Unknown Target.targetType %" PRId32, target.targetTypeOneOfCase); - } - - return [[FSTQueryData alloc] initWithQuery:query - targetID:targetID - listenSequenceNumber:sequenceNumber - purpose:FSTQueryPurposeListen - snapshotVersion:version - resumeToken:resumeToken]; -} - -- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version { - return [self.remoteSerializer encodedVersion:version]; -} - -- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version { - return [self.remoteSerializer decodedVersion:version]; -} - -@end diff --git a/Firestore/Source/Local/FSTLocalSerializer.mm b/Firestore/Source/Local/FSTLocalSerializer.mm new file mode 100644 index 0000000..c531c77 --- /dev/null +++ b/Firestore/Source/Local/FSTLocalSerializer.mm @@ -0,0 +1,213 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTLocalSerializer.h" + +#include + +#import "Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h" +#import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" +#import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h" +#import "Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Local/FSTQueryData.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTMutationBatch.h" +#import "Firestore/Source/Remote/FSTSerializerBeta.h" +#import "Firestore/Source/Util/FSTAssert.h" + +@interface FSTLocalSerializer () + +@property(nonatomic, strong, readonly) FSTSerializerBeta *remoteSerializer; + +@end + +/** Serializer for values stored in the LocalStore. */ +@implementation FSTLocalSerializer + +- (instancetype)initWithRemoteSerializer:(FSTSerializerBeta *)remoteSerializer { + self = [super init]; + if (self) { + _remoteSerializer = remoteSerializer; + } + return self; +} + +- (FSTPBMaybeDocument *)encodedMaybeDocument:(FSTMaybeDocument *)document { + FSTPBMaybeDocument *proto = [FSTPBMaybeDocument message]; + + if ([document isKindOfClass:[FSTDeletedDocument class]]) { + proto.noDocument = [self encodedDeletedDocument:(FSTDeletedDocument *)document]; + } else if ([document isKindOfClass:[FSTDocument class]]) { + proto.document = [self encodedDocument:(FSTDocument *)document]; + } else { + FSTFail(@"Unknown document type %@", NSStringFromClass([document class])); + } + + return proto; +} + +- (FSTMaybeDocument *)decodedMaybeDocument:(FSTPBMaybeDocument *)proto { + switch (proto.documentTypeOneOfCase) { + case FSTPBMaybeDocument_DocumentType_OneOfCase_Document: + return [self decodedDocument:proto.document]; + + case FSTPBMaybeDocument_DocumentType_OneOfCase_NoDocument: + return [self decodedDeletedDocument:proto.noDocument]; + + default: + FSTFail(@"Unknown MaybeDocument %@", proto); + } +} + +/** + * Encodes a Document for local storage. This differs from the v1beta1 RPC serializer for + * Documents in that it preserves the updateTime, which is considered an output only value by the + * server. + */ +- (GCFSDocument *)encodedDocument:(FSTDocument *)document { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + GCFSDocument *proto = [GCFSDocument message]; + proto.name = [remoteSerializer encodedDocumentKey:document.key]; + proto.fields = [remoteSerializer encodedFields:document.data]; + proto.updateTime = [remoteSerializer encodedVersion:document.version]; + + return proto; +} + +/** Decodes a Document proto to the equivalent model. */ +- (FSTDocument *)decodedDocument:(GCFSDocument *)document { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + FSTObjectValue *data = [remoteSerializer decodedFields:document.fields]; + FSTDocumentKey *key = [remoteSerializer decodedDocumentKey:document.name]; + FSTSnapshotVersion *version = [remoteSerializer decodedVersion:document.updateTime]; + return [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO]; +} + +/** Encodes a NoDocument value to the equivalent proto. */ +- (FSTPBNoDocument *)encodedDeletedDocument:(FSTDeletedDocument *)document { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + FSTPBNoDocument *proto = [FSTPBNoDocument message]; + proto.name = [remoteSerializer encodedDocumentKey:document.key]; + proto.readTime = [remoteSerializer encodedVersion:document.version]; + return proto; +} + +/** Decodes a NoDocument proto to the equivalent model. */ +- (FSTDeletedDocument *)decodedDeletedDocument:(FSTPBNoDocument *)proto { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + FSTDocumentKey *key = [remoteSerializer decodedDocumentKey:proto.name]; + FSTSnapshotVersion *version = [remoteSerializer decodedVersion:proto.readTime]; + return [FSTDeletedDocument documentWithKey:key version:version]; +} + +- (FSTPBWriteBatch *)encodedMutationBatch:(FSTMutationBatch *)batch { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + FSTPBWriteBatch *proto = [FSTPBWriteBatch message]; + proto.batchId = batch.batchID; + proto.localWriteTime = [remoteSerializer encodedTimestamp:batch.localWriteTime]; + + NSMutableArray *writes = proto.writesArray; + for (FSTMutation *mutation in batch.mutations) { + [writes addObject:[remoteSerializer encodedMutation:mutation]]; + } + return proto; +} + +- (FSTMutationBatch *)decodedMutationBatch:(FSTPBWriteBatch *)batch { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + int batchID = batch.batchId; + NSMutableArray *mutations = [NSMutableArray array]; + for (GCFSWrite *write in batch.writesArray) { + [mutations addObject:[remoteSerializer decodedMutation:write]]; + } + + FSTTimestamp *localWriteTime = [remoteSerializer decodedTimestamp:batch.localWriteTime]; + + return [[FSTMutationBatch alloc] initWithBatchID:batchID + localWriteTime:localWriteTime + mutations:mutations]; +} + +- (FSTPBTarget *)encodedQueryData:(FSTQueryData *)queryData { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + FSTAssert(queryData.purpose == FSTQueryPurposeListen, + @"only queries with purpose %lu may be stored, got %lu", + (unsigned long)FSTQueryPurposeListen, (unsigned long)queryData.purpose); + + FSTPBTarget *proto = [FSTPBTarget message]; + proto.targetId = queryData.targetID; + proto.lastListenSequenceNumber = queryData.sequenceNumber; + proto.snapshotVersion = [remoteSerializer encodedVersion:queryData.snapshotVersion]; + proto.resumeToken = queryData.resumeToken; + + FSTQuery *query = queryData.query; + if ([query isDocumentQuery]) { + proto.documents = [remoteSerializer encodedDocumentsTarget:query]; + } else { + proto.query = [remoteSerializer encodedQueryTarget:query]; + } + + return proto; +} + +- (FSTQueryData *)decodedQueryData:(FSTPBTarget *)target { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + FSTTargetID targetID = target.targetId; + FSTListenSequenceNumber sequenceNumber = target.lastListenSequenceNumber; + FSTSnapshotVersion *version = [remoteSerializer decodedVersion:target.snapshotVersion]; + NSData *resumeToken = target.resumeToken; + + FSTQuery *query; + switch (target.targetTypeOneOfCase) { + case FSTPBTarget_TargetType_OneOfCase_Documents: + query = [remoteSerializer decodedQueryFromDocumentsTarget:target.documents]; + break; + + case FSTPBTarget_TargetType_OneOfCase_Query: + query = [remoteSerializer decodedQueryFromQueryTarget:target.query]; + break; + + default: + FSTFail(@"Unknown Target.targetType %" PRId32, target.targetTypeOneOfCase); + } + + return [[FSTQueryData alloc] initWithQuery:query + targetID:targetID + listenSequenceNumber:sequenceNumber + purpose:FSTQueryPurposeListen + snapshotVersion:version + resumeToken:resumeToken]; +} + +- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version { + return [self.remoteSerializer encodedVersion:version]; +} + +- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version { + return [self.remoteSerializer decodedVersion:version]; +} + +@end diff --git a/Firestore/Source/Local/FSTLocalViewChanges.m b/Firestore/Source/Local/FSTLocalViewChanges.m deleted file mode 100644 index 9a7f445..0000000 --- a/Firestore/Source/Local/FSTLocalViewChanges.m +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTLocalViewChanges.h" - -#import "Firestore/Source/Core/FSTViewSnapshot.h" -#import "Firestore/Source/Model/FSTDocument.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTLocalViewChanges () -- (instancetype)initWithQuery:(FSTQuery *)query - addedKeys:(FSTDocumentKeySet *)addedKeys - removedKeys:(FSTDocumentKeySet *)removedKeys NS_DESIGNATED_INITIALIZER; -@end - -@implementation FSTLocalViewChanges - -+ (instancetype)changesForViewSnapshot:(FSTViewSnapshot *)viewSnapshot { - FSTDocumentKeySet *addedKeys = [FSTDocumentKeySet keySet]; - FSTDocumentKeySet *removedKeys = [FSTDocumentKeySet keySet]; - - for (FSTDocumentViewChange *docChange in viewSnapshot.documentChanges) { - switch (docChange.type) { - case FSTDocumentViewChangeTypeAdded: - addedKeys = [addedKeys setByAddingObject:docChange.document.key]; - break; - - case FSTDocumentViewChangeTypeRemoved: - removedKeys = [removedKeys setByAddingObject:docChange.document.key]; - break; - - default: - // Do nothing. - break; - } - } - - return [self changesForQuery:viewSnapshot.query addedKeys:addedKeys removedKeys:removedKeys]; -} - -+ (instancetype)changesForQuery:(FSTQuery *)query - addedKeys:(FSTDocumentKeySet *)addedKeys - removedKeys:(FSTDocumentKeySet *)removedKeys { - return - [[FSTLocalViewChanges alloc] initWithQuery:query addedKeys:addedKeys removedKeys:removedKeys]; -} - -- (instancetype)initWithQuery:(FSTQuery *)query - addedKeys:(FSTDocumentKeySet *)addedKeys - removedKeys:(FSTDocumentKeySet *)removedKeys { - self = [super init]; - if (self) { - _query = query; - _addedKeys = addedKeys; - _removedKeys = removedKeys; - } - return self; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalViewChanges.mm b/Firestore/Source/Local/FSTLocalViewChanges.mm new file mode 100644 index 0000000..9a7f445 --- /dev/null +++ b/Firestore/Source/Local/FSTLocalViewChanges.mm @@ -0,0 +1,76 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTLocalViewChanges.h" + +#import "Firestore/Source/Core/FSTViewSnapshot.h" +#import "Firestore/Source/Model/FSTDocument.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTLocalViewChanges () +- (instancetype)initWithQuery:(FSTQuery *)query + addedKeys:(FSTDocumentKeySet *)addedKeys + removedKeys:(FSTDocumentKeySet *)removedKeys NS_DESIGNATED_INITIALIZER; +@end + +@implementation FSTLocalViewChanges + ++ (instancetype)changesForViewSnapshot:(FSTViewSnapshot *)viewSnapshot { + FSTDocumentKeySet *addedKeys = [FSTDocumentKeySet keySet]; + FSTDocumentKeySet *removedKeys = [FSTDocumentKeySet keySet]; + + for (FSTDocumentViewChange *docChange in viewSnapshot.documentChanges) { + switch (docChange.type) { + case FSTDocumentViewChangeTypeAdded: + addedKeys = [addedKeys setByAddingObject:docChange.document.key]; + break; + + case FSTDocumentViewChangeTypeRemoved: + removedKeys = [removedKeys setByAddingObject:docChange.document.key]; + break; + + default: + // Do nothing. + break; + } + } + + return [self changesForQuery:viewSnapshot.query addedKeys:addedKeys removedKeys:removedKeys]; +} + ++ (instancetype)changesForQuery:(FSTQuery *)query + addedKeys:(FSTDocumentKeySet *)addedKeys + removedKeys:(FSTDocumentKeySet *)removedKeys { + return + [[FSTLocalViewChanges alloc] initWithQuery:query addedKeys:addedKeys removedKeys:removedKeys]; +} + +- (instancetype)initWithQuery:(FSTQuery *)query + addedKeys:(FSTDocumentKeySet *)addedKeys + removedKeys:(FSTDocumentKeySet *)removedKeys { + self = [super init]; + if (self) { + _query = query; + _addedKeys = addedKeys; + _removedKeys = removedKeys; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalWriteResult.m b/Firestore/Source/Local/FSTLocalWriteResult.m deleted file mode 100644 index c1753fe..0000000 --- a/Firestore/Source/Local/FSTLocalWriteResult.m +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTLocalWriteResult.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTLocalWriteResult () -- (instancetype)initWithBatchID:(FSTBatchID)batchID - changes:(FSTMaybeDocumentDictionary *)changes NS_DESIGNATED_INITIALIZER; -@end - -@implementation FSTLocalWriteResult - -+ (instancetype)resultForBatchID:(FSTBatchID)batchID changes:(FSTMaybeDocumentDictionary *)changes { - return [[FSTLocalWriteResult alloc] initWithBatchID:batchID changes:changes]; -} - -- (instancetype)initWithBatchID:(FSTBatchID)batchID changes:(FSTMaybeDocumentDictionary *)changes { - self = [super init]; - if (self) { - _batchID = batchID; - _changes = changes; - } - return self; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalWriteResult.mm b/Firestore/Source/Local/FSTLocalWriteResult.mm new file mode 100644 index 0000000..c1753fe --- /dev/null +++ b/Firestore/Source/Local/FSTLocalWriteResult.mm @@ -0,0 +1,43 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTLocalWriteResult.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTLocalWriteResult () +- (instancetype)initWithBatchID:(FSTBatchID)batchID + changes:(FSTMaybeDocumentDictionary *)changes NS_DESIGNATED_INITIALIZER; +@end + +@implementation FSTLocalWriteResult + ++ (instancetype)resultForBatchID:(FSTBatchID)batchID changes:(FSTMaybeDocumentDictionary *)changes { + return [[FSTLocalWriteResult alloc] initWithBatchID:batchID changes:changes]; +} + +- (instancetype)initWithBatchID:(FSTBatchID)batchID changes:(FSTMaybeDocumentDictionary *)changes { + self = [super init]; + if (self) { + _batchID = batchID; + _changes = changes; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryPersistence.m b/Firestore/Source/Local/FSTMemoryPersistence.m deleted file mode 100644 index e301820..0000000 --- a/Firestore/Source/Local/FSTMemoryPersistence.m +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTMemoryPersistence.h" - -#import "Firestore/Source/Auth/FSTUser.h" -#import "Firestore/Source/Local/FSTMemoryMutationQueue.h" -#import "Firestore/Source/Local/FSTMemoryQueryCache.h" -#import "Firestore/Source/Local/FSTMemoryRemoteDocumentCache.h" -#import "Firestore/Source/Local/FSTWriteGroup.h" -#import "Firestore/Source/Local/FSTWriteGroupTracker.h" -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTMemoryPersistence () -@property(nonatomic, strong, nonnull) FSTWriteGroupTracker *writeGroupTracker; -@property(nonatomic, strong, nonnull) - NSMutableDictionary> *mutationQueues; -@property(nonatomic, assign, getter=isStarted) BOOL started; -@end - -@implementation FSTMemoryPersistence { - /** - * The FSTQueryCache representing the persisted cache of queries. - * - * Note that this is retained here to make it easier to write tests affecting both the in-memory - * and LevelDB-backed persistence layers. Tests can create a new FSTLocalStore wrapping this - * FSTPersistence instance and this will make the in-memory persistence layer behave as if it - * were actually persisting values. - */ - FSTMemoryQueryCache *_queryCache; - - /** The FSTRemoteDocumentCache representing the persisted cache of remote documents. */ - FSTMemoryRemoteDocumentCache *_remoteDocumentCache; -} - -+ (instancetype)persistence { - return [[FSTMemoryPersistence alloc] init]; -} - -- (instancetype)init { - if (self = [super init]) { - _writeGroupTracker = [FSTWriteGroupTracker tracker]; - _queryCache = [[FSTMemoryQueryCache alloc] init]; - _remoteDocumentCache = [[FSTMemoryRemoteDocumentCache alloc] init]; - _mutationQueues = [NSMutableDictionary dictionary]; - } - return self; -} - -- (BOOL)start:(NSError **)error { - // No durable state to read on startup. - FSTAssert(!self.isStarted, @"FSTMemoryPersistence double-started!"); - self.started = YES; - return YES; -} - -- (void)shutdown { - // No durable state to ensure is closed on shutdown. - FSTAssert(self.isStarted, @"FSTMemoryPersistence shutdown without start!"); - self.started = NO; -} - -- (id)mutationQueueForUser:(FSTUser *)user { - id queue = self.mutationQueues[user]; - if (!queue) { - queue = [FSTMemoryMutationQueue mutationQueue]; - self.mutationQueues[user] = queue; - } - return queue; -} - -- (id)queryCache { - return _queryCache; -} - -- (id)remoteDocumentCache { - return _remoteDocumentCache; -} - -- (FSTWriteGroup *)startGroupWithAction:(NSString *)action { - return [self.writeGroupTracker startGroupWithAction:action]; -} - -- (void)commitGroup:(FSTWriteGroup *)group { - [self.writeGroupTracker endGroup:group]; - - FSTAssert(group.isEmpty, @"Memory persistence shouldn't use write groups: %@", group.action); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryPersistence.mm b/Firestore/Source/Local/FSTMemoryPersistence.mm new file mode 100644 index 0000000..e301820 --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryPersistence.mm @@ -0,0 +1,107 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTMemoryPersistence.h" + +#import "Firestore/Source/Auth/FSTUser.h" +#import "Firestore/Source/Local/FSTMemoryMutationQueue.h" +#import "Firestore/Source/Local/FSTMemoryQueryCache.h" +#import "Firestore/Source/Local/FSTMemoryRemoteDocumentCache.h" +#import "Firestore/Source/Local/FSTWriteGroup.h" +#import "Firestore/Source/Local/FSTWriteGroupTracker.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMemoryPersistence () +@property(nonatomic, strong, nonnull) FSTWriteGroupTracker *writeGroupTracker; +@property(nonatomic, strong, nonnull) + NSMutableDictionary> *mutationQueues; +@property(nonatomic, assign, getter=isStarted) BOOL started; +@end + +@implementation FSTMemoryPersistence { + /** + * The FSTQueryCache representing the persisted cache of queries. + * + * Note that this is retained here to make it easier to write tests affecting both the in-memory + * and LevelDB-backed persistence layers. Tests can create a new FSTLocalStore wrapping this + * FSTPersistence instance and this will make the in-memory persistence layer behave as if it + * were actually persisting values. + */ + FSTMemoryQueryCache *_queryCache; + + /** The FSTRemoteDocumentCache representing the persisted cache of remote documents. */ + FSTMemoryRemoteDocumentCache *_remoteDocumentCache; +} + ++ (instancetype)persistence { + return [[FSTMemoryPersistence alloc] init]; +} + +- (instancetype)init { + if (self = [super init]) { + _writeGroupTracker = [FSTWriteGroupTracker tracker]; + _queryCache = [[FSTMemoryQueryCache alloc] init]; + _remoteDocumentCache = [[FSTMemoryRemoteDocumentCache alloc] init]; + _mutationQueues = [NSMutableDictionary dictionary]; + } + return self; +} + +- (BOOL)start:(NSError **)error { + // No durable state to read on startup. + FSTAssert(!self.isStarted, @"FSTMemoryPersistence double-started!"); + self.started = YES; + return YES; +} + +- (void)shutdown { + // No durable state to ensure is closed on shutdown. + FSTAssert(self.isStarted, @"FSTMemoryPersistence shutdown without start!"); + self.started = NO; +} + +- (id)mutationQueueForUser:(FSTUser *)user { + id queue = self.mutationQueues[user]; + if (!queue) { + queue = [FSTMemoryMutationQueue mutationQueue]; + self.mutationQueues[user] = queue; + } + return queue; +} + +- (id)queryCache { + return _queryCache; +} + +- (id)remoteDocumentCache { + return _remoteDocumentCache; +} + +- (FSTWriteGroup *)startGroupWithAction:(NSString *)action { + return [self.writeGroupTracker startGroupWithAction:action]; +} + +- (void)commitGroup:(FSTWriteGroup *)group { + [self.writeGroupTracker endGroup:group]; + + FSTAssert(group.isEmpty, @"Memory persistence shouldn't use write groups: %@", group.action); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryQueryCache.m b/Firestore/Source/Local/FSTMemoryQueryCache.m deleted file mode 100644 index bcab174..0000000 --- a/Firestore/Source/Local/FSTMemoryQueryCache.m +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTMemoryQueryCache.h" - -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Local/FSTReferenceSet.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTMemoryQueryCache () - -/** Maps a query to the data about that query. */ -@property(nonatomic, strong, readonly) NSMutableDictionary *queries; - -/** A ordered bidirectional mapping between documents and the remote target IDs. */ -@property(nonatomic, strong, readonly) FSTReferenceSet *references; - -/** The highest numbered target ID encountered. */ -@property(nonatomic, assign) FSTTargetID highestTargetID; - -@property(nonatomic, assign) FSTListenSequenceNumber highestListenSequenceNumber; - -@end - -@implementation FSTMemoryQueryCache { - /** The last received snapshot version. */ - FSTSnapshotVersion *_lastRemoteSnapshotVersion; -} - -- (instancetype)init { - if (self = [super init]) { - _queries = [NSMutableDictionary dictionary]; - _references = [[FSTReferenceSet alloc] init]; - _lastRemoteSnapshotVersion = [FSTSnapshotVersion noVersion]; - } - return self; -} - -#pragma mark - FSTQueryCache implementation -#pragma mark Query tracking - -- (void)start { - // Nothing to do. -} - -- (void)shutdown { - // No resources to release. -} - -- (FSTTargetID)highestTargetID { - return _highestTargetID; -} - -- (FSTListenSequenceNumber)highestListenSequenceNumber { - return _highestListenSequenceNumber; -} - -- (FSTSnapshotVersion *)lastRemoteSnapshotVersion { - return _lastRemoteSnapshotVersion; -} - -- (void)setLastRemoteSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion - group:(FSTWriteGroup *)group { - _lastRemoteSnapshotVersion = snapshotVersion; -} - -- (void)addQueryData:(FSTQueryData *)queryData group:(__unused FSTWriteGroup *)group { - self.queries[queryData.query] = queryData; - if (queryData.targetID > self.highestTargetID) { - self.highestTargetID = queryData.targetID; - } - if (queryData.sequenceNumber > self.highestListenSequenceNumber) { - self.highestListenSequenceNumber = queryData.sequenceNumber; - } -} - -- (void)removeQueryData:(FSTQueryData *)queryData group:(__unused FSTWriteGroup *)group { - [self.queries removeObjectForKey:queryData.query]; - [self.references removeReferencesForID:queryData.targetID]; -} - -- (nullable FSTQueryData *)queryDataForQuery:(FSTQuery *)query { - return self.queries[query]; -} - -#pragma mark Reference tracking - -- (void)addMatchingKeys:(FSTDocumentKeySet *)keys - forTargetID:(FSTTargetID)targetID - group:(__unused FSTWriteGroup *)group { - [self.references addReferencesToKeys:keys forID:targetID]; -} - -- (void)removeMatchingKeys:(FSTDocumentKeySet *)keys - forTargetID:(FSTTargetID)targetID - group:(__unused FSTWriteGroup *)group { - [self.references removeReferencesToKeys:keys forID:targetID]; -} - -- (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID group:(__unused FSTWriteGroup *)group { - [self.references removeReferencesForID:targetID]; -} - -- (FSTDocumentKeySet *)matchingKeysForTargetID:(FSTTargetID)targetID { - return [self.references referencedKeysForID:targetID]; -} - -#pragma mark - FSTGarbageSource implementation - -- (nullable id)garbageCollector { - return self.references.garbageCollector; -} - -- (void)setGarbageCollector:(nullable id)garbageCollector { - self.references.garbageCollector = garbageCollector; -} - -- (BOOL)containsKey:(FSTDocumentKey *)key { - return [self.references containsKey:key]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryQueryCache.mm b/Firestore/Source/Local/FSTMemoryQueryCache.mm new file mode 100644 index 0000000..bcab174 --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryQueryCache.mm @@ -0,0 +1,140 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTMemoryQueryCache.h" + +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Local/FSTQueryData.h" +#import "Firestore/Source/Local/FSTReferenceSet.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMemoryQueryCache () + +/** Maps a query to the data about that query. */ +@property(nonatomic, strong, readonly) NSMutableDictionary *queries; + +/** A ordered bidirectional mapping between documents and the remote target IDs. */ +@property(nonatomic, strong, readonly) FSTReferenceSet *references; + +/** The highest numbered target ID encountered. */ +@property(nonatomic, assign) FSTTargetID highestTargetID; + +@property(nonatomic, assign) FSTListenSequenceNumber highestListenSequenceNumber; + +@end + +@implementation FSTMemoryQueryCache { + /** The last received snapshot version. */ + FSTSnapshotVersion *_lastRemoteSnapshotVersion; +} + +- (instancetype)init { + if (self = [super init]) { + _queries = [NSMutableDictionary dictionary]; + _references = [[FSTReferenceSet alloc] init]; + _lastRemoteSnapshotVersion = [FSTSnapshotVersion noVersion]; + } + return self; +} + +#pragma mark - FSTQueryCache implementation +#pragma mark Query tracking + +- (void)start { + // Nothing to do. +} + +- (void)shutdown { + // No resources to release. +} + +- (FSTTargetID)highestTargetID { + return _highestTargetID; +} + +- (FSTListenSequenceNumber)highestListenSequenceNumber { + return _highestListenSequenceNumber; +} + +- (FSTSnapshotVersion *)lastRemoteSnapshotVersion { + return _lastRemoteSnapshotVersion; +} + +- (void)setLastRemoteSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + group:(FSTWriteGroup *)group { + _lastRemoteSnapshotVersion = snapshotVersion; +} + +- (void)addQueryData:(FSTQueryData *)queryData group:(__unused FSTWriteGroup *)group { + self.queries[queryData.query] = queryData; + if (queryData.targetID > self.highestTargetID) { + self.highestTargetID = queryData.targetID; + } + if (queryData.sequenceNumber > self.highestListenSequenceNumber) { + self.highestListenSequenceNumber = queryData.sequenceNumber; + } +} + +- (void)removeQueryData:(FSTQueryData *)queryData group:(__unused FSTWriteGroup *)group { + [self.queries removeObjectForKey:queryData.query]; + [self.references removeReferencesForID:queryData.targetID]; +} + +- (nullable FSTQueryData *)queryDataForQuery:(FSTQuery *)query { + return self.queries[query]; +} + +#pragma mark Reference tracking + +- (void)addMatchingKeys:(FSTDocumentKeySet *)keys + forTargetID:(FSTTargetID)targetID + group:(__unused FSTWriteGroup *)group { + [self.references addReferencesToKeys:keys forID:targetID]; +} + +- (void)removeMatchingKeys:(FSTDocumentKeySet *)keys + forTargetID:(FSTTargetID)targetID + group:(__unused FSTWriteGroup *)group { + [self.references removeReferencesToKeys:keys forID:targetID]; +} + +- (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID group:(__unused FSTWriteGroup *)group { + [self.references removeReferencesForID:targetID]; +} + +- (FSTDocumentKeySet *)matchingKeysForTargetID:(FSTTargetID)targetID { + return [self.references referencedKeysForID:targetID]; +} + +#pragma mark - FSTGarbageSource implementation + +- (nullable id)garbageCollector { + return self.references.garbageCollector; +} + +- (void)setGarbageCollector:(nullable id)garbageCollector { + self.references.garbageCollector = garbageCollector; +} + +- (BOOL)containsKey:(FSTDocumentKey *)key { + return [self.references containsKey:key]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.m b/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.m deleted file mode 100644 index 9bbc047..0000000 --- a/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.m +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTMemoryRemoteDocumentCache.h" - -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentDictionary.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTPath.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTMemoryRemoteDocumentCache () - -/** Underlying cache of documents. */ -@property(nonatomic, strong) FSTMaybeDocumentDictionary *docs; - -@end - -@implementation FSTMemoryRemoteDocumentCache - -- (instancetype)init { - if (self = [super init]) { - _docs = [FSTMaybeDocumentDictionary maybeDocumentDictionary]; - } - return self; -} - -- (void)shutdown { -} - -- (void)addEntry:(FSTMaybeDocument *)document group:(FSTWriteGroup *)group { - self.docs = [self.docs dictionaryBySettingObject:document forKey:document.key]; -} - -- (void)removeEntryForKey:(FSTDocumentKey *)key group:(FSTWriteGroup *)group { - self.docs = [self.docs dictionaryByRemovingObjectForKey:key]; -} - -- (nullable FSTMaybeDocument *)entryForKey:(FSTDocumentKey *)key { - return self.docs[key]; -} - -- (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query { - FSTDocumentDictionary *result = [FSTDocumentDictionary documentDictionary]; - - // Documents are ordered by key, so we can use a prefix scan to narrow down the documents - // we need to match the query against. - FSTDocumentKey *prefix = [FSTDocumentKey keyWithPath:[query.path pathByAppendingSegment:@""]]; - NSEnumerator *enumerator = [self.docs keyEnumeratorFrom:prefix]; - for (FSTDocumentKey *key in enumerator) { - if (![query.path isPrefixOfPath:key.path]) { - break; - } - FSTMaybeDocument *maybeDoc = self.docs[key]; - if (![maybeDoc isKindOfClass:[FSTDocument class]]) { - continue; - } - FSTDocument *doc = (FSTDocument *)maybeDoc; - if ([query matchesDocument:doc]) { - result = [result dictionaryBySettingObject:doc forKey:doc.key]; - } - } - - return result; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.mm b/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.mm new file mode 100644 index 0000000..9bbc047 --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.mm @@ -0,0 +1,84 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTMemoryRemoteDocumentCache.h" + +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentDictionary.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTPath.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMemoryRemoteDocumentCache () + +/** Underlying cache of documents. */ +@property(nonatomic, strong) FSTMaybeDocumentDictionary *docs; + +@end + +@implementation FSTMemoryRemoteDocumentCache + +- (instancetype)init { + if (self = [super init]) { + _docs = [FSTMaybeDocumentDictionary maybeDocumentDictionary]; + } + return self; +} + +- (void)shutdown { +} + +- (void)addEntry:(FSTMaybeDocument *)document group:(FSTWriteGroup *)group { + self.docs = [self.docs dictionaryBySettingObject:document forKey:document.key]; +} + +- (void)removeEntryForKey:(FSTDocumentKey *)key group:(FSTWriteGroup *)group { + self.docs = [self.docs dictionaryByRemovingObjectForKey:key]; +} + +- (nullable FSTMaybeDocument *)entryForKey:(FSTDocumentKey *)key { + return self.docs[key]; +} + +- (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query { + FSTDocumentDictionary *result = [FSTDocumentDictionary documentDictionary]; + + // Documents are ordered by key, so we can use a prefix scan to narrow down the documents + // we need to match the query against. + FSTDocumentKey *prefix = [FSTDocumentKey keyWithPath:[query.path pathByAppendingSegment:@""]]; + NSEnumerator *enumerator = [self.docs keyEnumeratorFrom:prefix]; + for (FSTDocumentKey *key in enumerator) { + if (![query.path isPrefixOfPath:key.path]) { + break; + } + FSTMaybeDocument *maybeDoc = self.docs[key]; + if (![maybeDoc isKindOfClass:[FSTDocument class]]) { + continue; + } + FSTDocument *doc = (FSTDocument *)maybeDoc; + if ([query matchesDocument:doc]) { + result = [result dictionaryBySettingObject:doc forKey:doc.key]; + } + } + + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTNoOpGarbageCollector.m b/Firestore/Source/Local/FSTNoOpGarbageCollector.m deleted file mode 100644 index e03b599..0000000 --- a/Firestore/Source/Local/FSTNoOpGarbageCollector.m +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTNoOpGarbageCollector.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTNoOpGarbageCollector - -- (BOOL)isEager { - return NO; -} - -- (void)addGarbageSource:(id)garbageSource { - // Not tracking garbage so don't track sources. -} - -- (void)removeGarbageSource:(id)garbageSource { - // Not tracking garbage so don't track sources. -} - -- (void)addPotentialGarbageKey:(FSTDocumentKey *)key { - // Not tracking garbage so ignore. -} - -- (NSSet *)collectGarbage { - return [NSSet set]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTNoOpGarbageCollector.mm b/Firestore/Source/Local/FSTNoOpGarbageCollector.mm new file mode 100644 index 0000000..e03b599 --- /dev/null +++ b/Firestore/Source/Local/FSTNoOpGarbageCollector.mm @@ -0,0 +1,45 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTNoOpGarbageCollector.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTNoOpGarbageCollector + +- (BOOL)isEager { + return NO; +} + +- (void)addGarbageSource:(id)garbageSource { + // Not tracking garbage so don't track sources. +} + +- (void)removeGarbageSource:(id)garbageSource { + // Not tracking garbage so don't track sources. +} + +- (void)addPotentialGarbageKey:(FSTDocumentKey *)key { + // Not tracking garbage so ignore. +} + +- (NSSet *)collectGarbage { + return [NSSet set]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTQueryData.m b/Firestore/Source/Local/FSTQueryData.m deleted file mode 100644 index 6bb716a..0000000 --- a/Firestore/Source/Local/FSTQueryData.m +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTQueryData.h" - -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTQueryData - -- (instancetype)initWithQuery:(FSTQuery *)query - targetID:(FSTTargetID)targetID - listenSequenceNumber:(FSTListenSequenceNumber)sequenceNumber - purpose:(FSTQueryPurpose)purpose - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion - resumeToken:(NSData *)resumeToken { - self = [super init]; - if (self) { - _query = query; - _targetID = targetID; - _sequenceNumber = sequenceNumber; - _purpose = purpose; - _snapshotVersion = snapshotVersion; - _resumeToken = [resumeToken copy]; - } - return self; -} - -- (instancetype)initWithQuery:(FSTQuery *)query - targetID:(FSTTargetID)targetID - listenSequenceNumber:(FSTListenSequenceNumber)sequenceNumber - purpose:(FSTQueryPurpose)purpose { - return [self initWithQuery:query - targetID:targetID - listenSequenceNumber:sequenceNumber - purpose:purpose - snapshotVersion:[FSTSnapshotVersion noVersion] - resumeToken:[NSData data]]; -} - -- (BOOL)isEqual:(id)object { - if (self == object) { - return YES; - } - if (![object isKindOfClass:[FSTQueryData class]]) { - return NO; - } - - FSTQueryData *other = (FSTQueryData *)object; - return [self.query isEqual:other.query] && self.targetID == other.targetID && - self.purpose == other.purpose && [self.snapshotVersion isEqual:other.snapshotVersion] && - [self.resumeToken isEqual:other.resumeToken]; -} - -- (NSUInteger)hash { - NSUInteger result = [self.query hash]; - result = result * 31 + self.targetID; - result = result * 31 + self.purpose; - result = result * 31 + [self.snapshotVersion hash]; - result = result * 31 + [self.resumeToken hash]; - return result; -} - -- (NSString *)description { - return [NSString - stringWithFormat:@"", - self.query, self.targetID, (unsigned long)self.purpose, self.snapshotVersion, - self.resumeToken]; -} - -- (instancetype)queryDataByReplacingSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion - resumeToken:(NSData *)resumeToken { - return [[FSTQueryData alloc] initWithQuery:self.query - targetID:self.targetID - listenSequenceNumber:self.sequenceNumber - purpose:self.purpose - snapshotVersion:snapshotVersion - resumeToken:resumeToken]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTQueryData.mm b/Firestore/Source/Local/FSTQueryData.mm new file mode 100644 index 0000000..6bb716a --- /dev/null +++ b/Firestore/Source/Local/FSTQueryData.mm @@ -0,0 +1,98 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTQueryData.h" + +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTSnapshotVersion.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTQueryData + +- (instancetype)initWithQuery:(FSTQuery *)query + targetID:(FSTTargetID)targetID + listenSequenceNumber:(FSTListenSequenceNumber)sequenceNumber + purpose:(FSTQueryPurpose)purpose + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + resumeToken:(NSData *)resumeToken { + self = [super init]; + if (self) { + _query = query; + _targetID = targetID; + _sequenceNumber = sequenceNumber; + _purpose = purpose; + _snapshotVersion = snapshotVersion; + _resumeToken = [resumeToken copy]; + } + return self; +} + +- (instancetype)initWithQuery:(FSTQuery *)query + targetID:(FSTTargetID)targetID + listenSequenceNumber:(FSTListenSequenceNumber)sequenceNumber + purpose:(FSTQueryPurpose)purpose { + return [self initWithQuery:query + targetID:targetID + listenSequenceNumber:sequenceNumber + purpose:purpose + snapshotVersion:[FSTSnapshotVersion noVersion] + resumeToken:[NSData data]]; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + if (![object isKindOfClass:[FSTQueryData class]]) { + return NO; + } + + FSTQueryData *other = (FSTQueryData *)object; + return [self.query isEqual:other.query] && self.targetID == other.targetID && + self.purpose == other.purpose && [self.snapshotVersion isEqual:other.snapshotVersion] && + [self.resumeToken isEqual:other.resumeToken]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.query hash]; + result = result * 31 + self.targetID; + result = result * 31 + self.purpose; + result = result * 31 + [self.snapshotVersion hash]; + result = result * 31 + [self.resumeToken hash]; + return result; +} + +- (NSString *)description { + return [NSString + stringWithFormat:@"", + self.query, self.targetID, (unsigned long)self.purpose, self.snapshotVersion, + self.resumeToken]; +} + +- (instancetype)queryDataByReplacingSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + resumeToken:(NSData *)resumeToken { + return [[FSTQueryData alloc] initWithQuery:self.query + targetID:self.targetID + listenSequenceNumber:self.sequenceNumber + purpose:self.purpose + snapshotVersion:snapshotVersion + resumeToken:resumeToken]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTReferenceSet.m b/Firestore/Source/Local/FSTReferenceSet.m deleted file mode 100644 index 2acd64b..0000000 --- a/Firestore/Source/Local/FSTReferenceSet.m +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTReferenceSet.h" - -#import "Firestore/Source/Local/FSTDocumentReference.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTReferenceSet - -@interface FSTReferenceSet () - -/** A set of outstanding references to a document sorted by key. */ -@property(nonatomic, strong) FSTImmutableSortedSet *referencesByKey; - -/** A set of outstanding references to a document sorted by target ID (or batch ID). */ -@property(nonatomic, strong) FSTImmutableSortedSet *referencesByID; - -@end - -@implementation FSTReferenceSet - -#pragma mark - Initializer - -- (instancetype)init { - self = [super init]; - if (self) { - _referencesByKey = - [FSTImmutableSortedSet setWithComparator:FSTDocumentReferenceComparatorByKey]; - _referencesByID = [FSTImmutableSortedSet setWithComparator:FSTDocumentReferenceComparatorByID]; - } - return self; -} - -#pragma mark - Testing helper methods - -- (BOOL)isEmpty { - return [self.referencesByKey isEmpty]; -} - -- (NSUInteger)count { - return self.referencesByKey.count; -} - -#pragma mark - Public methods - -- (void)addReferenceToKey:(FSTDocumentKey *)key forID:(int)ID { - FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:ID]; - self.referencesByKey = [self.referencesByKey setByAddingObject:reference]; - self.referencesByID = [self.referencesByID setByAddingObject:reference]; -} - -- (void)addReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID { - [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { - [self addReferenceToKey:key forID:ID]; - }]; -} - -- (void)removeReferenceToKey:(FSTDocumentKey *)key forID:(int)ID { - [self removeReference:[[FSTDocumentReference alloc] initWithKey:key ID:ID]]; -} - -- (void)removeReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID { - [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { - [self removeReferenceToKey:key forID:ID]; - }]; -} - -- (void)removeReferencesForID:(int)ID { - FSTDocumentKey *emptyKey = [FSTDocumentKey keyWithSegments:@[]]; - FSTDocumentReference *start = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:ID]; - FSTDocumentReference *end = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:(ID + 1)]; - - [self.referencesByID enumerateObjectsFrom:start - to:end - usingBlock:^(FSTDocumentReference *reference, BOOL *stop) { - [self removeReference:reference]; - }]; -} - -- (void)removeAllReferences { - for (FSTDocumentReference *reference in self.referencesByKey.objectEnumerator) { - [self removeReference:reference]; - } -} - -- (void)removeReference:(FSTDocumentReference *)reference { - self.referencesByKey = [self.referencesByKey setByRemovingObject:reference]; - self.referencesByID = [self.referencesByID setByRemovingObject:reference]; - [self.garbageCollector addPotentialGarbageKey:reference.key]; -} - -- (FSTDocumentKeySet *)referencedKeysForID:(int)ID { - FSTDocumentKey *emptyKey = [FSTDocumentKey keyWithSegments:@[]]; - FSTDocumentReference *start = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:ID]; - FSTDocumentReference *end = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:(ID + 1)]; - - __block FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet]; - [self.referencesByID enumerateObjectsFrom:start - to:end - usingBlock:^(FSTDocumentReference *reference, BOOL *stop) { - keys = [keys setByAddingObject:reference.key]; - }]; - return keys; -} - -- (BOOL)containsKey:(FSTDocumentKey *)key { - // Create a reference with a zero ID as the start position to find any document reference with - // this key. - FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:0]; - - NSEnumerator *enumerator = - [self.referencesByKey objectEnumeratorFrom:reference]; - FSTDocumentKey *_Nullable firstKey = [enumerator nextObject].key; - return [firstKey isEqual:reference.key]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTReferenceSet.mm b/Firestore/Source/Local/FSTReferenceSet.mm new file mode 100644 index 0000000..2acd64b --- /dev/null +++ b/Firestore/Source/Local/FSTReferenceSet.mm @@ -0,0 +1,135 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTReferenceSet.h" + +#import "Firestore/Source/Local/FSTDocumentReference.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTReferenceSet + +@interface FSTReferenceSet () + +/** A set of outstanding references to a document sorted by key. */ +@property(nonatomic, strong) FSTImmutableSortedSet *referencesByKey; + +/** A set of outstanding references to a document sorted by target ID (or batch ID). */ +@property(nonatomic, strong) FSTImmutableSortedSet *referencesByID; + +@end + +@implementation FSTReferenceSet + +#pragma mark - Initializer + +- (instancetype)init { + self = [super init]; + if (self) { + _referencesByKey = + [FSTImmutableSortedSet setWithComparator:FSTDocumentReferenceComparatorByKey]; + _referencesByID = [FSTImmutableSortedSet setWithComparator:FSTDocumentReferenceComparatorByID]; + } + return self; +} + +#pragma mark - Testing helper methods + +- (BOOL)isEmpty { + return [self.referencesByKey isEmpty]; +} + +- (NSUInteger)count { + return self.referencesByKey.count; +} + +#pragma mark - Public methods + +- (void)addReferenceToKey:(FSTDocumentKey *)key forID:(int)ID { + FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:ID]; + self.referencesByKey = [self.referencesByKey setByAddingObject:reference]; + self.referencesByID = [self.referencesByID setByAddingObject:reference]; +} + +- (void)addReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID { + [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + [self addReferenceToKey:key forID:ID]; + }]; +} + +- (void)removeReferenceToKey:(FSTDocumentKey *)key forID:(int)ID { + [self removeReference:[[FSTDocumentReference alloc] initWithKey:key ID:ID]]; +} + +- (void)removeReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID { + [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + [self removeReferenceToKey:key forID:ID]; + }]; +} + +- (void)removeReferencesForID:(int)ID { + FSTDocumentKey *emptyKey = [FSTDocumentKey keyWithSegments:@[]]; + FSTDocumentReference *start = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:ID]; + FSTDocumentReference *end = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:(ID + 1)]; + + [self.referencesByID enumerateObjectsFrom:start + to:end + usingBlock:^(FSTDocumentReference *reference, BOOL *stop) { + [self removeReference:reference]; + }]; +} + +- (void)removeAllReferences { + for (FSTDocumentReference *reference in self.referencesByKey.objectEnumerator) { + [self removeReference:reference]; + } +} + +- (void)removeReference:(FSTDocumentReference *)reference { + self.referencesByKey = [self.referencesByKey setByRemovingObject:reference]; + self.referencesByID = [self.referencesByID setByRemovingObject:reference]; + [self.garbageCollector addPotentialGarbageKey:reference.key]; +} + +- (FSTDocumentKeySet *)referencedKeysForID:(int)ID { + FSTDocumentKey *emptyKey = [FSTDocumentKey keyWithSegments:@[]]; + FSTDocumentReference *start = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:ID]; + FSTDocumentReference *end = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:(ID + 1)]; + + __block FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet]; + [self.referencesByID enumerateObjectsFrom:start + to:end + usingBlock:^(FSTDocumentReference *reference, BOOL *stop) { + keys = [keys setByAddingObject:reference.key]; + }]; + return keys; +} + +- (BOOL)containsKey:(FSTDocumentKey *)key { + // Create a reference with a zero ID as the start position to find any document reference with + // this key. + FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:0]; + + NSEnumerator *enumerator = + [self.referencesByKey objectEnumeratorFrom:reference]; + FSTDocumentKey *_Nullable firstKey = [enumerator nextObject].key; + return [firstKey isEqual:reference.key]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.m b/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.m deleted file mode 100644 index bca587a..0000000 --- a/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.m +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.h" - -#import "Firestore/Source/Local/FSTRemoteDocumentCache.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTRemoteDocumentChangeBuffer () - -- (instancetype)initWithCache:(id)cache; - -/** The underlying cache we're buffering changes for. */ -@property(nonatomic, strong, nonnull) id remoteDocumentCache; - -/** The buffered changes, stored as a dictionary for easy lookups. */ -@property(nonatomic, strong, nullable) - NSMutableDictionary *changes; - -@end - -@implementation FSTRemoteDocumentChangeBuffer - -+ (instancetype)changeBufferWithCache:(id)cache { - return [[FSTRemoteDocumentChangeBuffer alloc] initWithCache:cache]; -} - -- (instancetype)initWithCache:(id)cache { - if (self = [super init]) { - _remoteDocumentCache = cache; - _changes = [NSMutableDictionary dictionary]; - } - return self; -} - -- (void)addEntry:(FSTMaybeDocument *)maybeDocument { - [self assertValid]; - - self.changes[maybeDocument.key] = maybeDocument; -} - -- (nullable FSTMaybeDocument *)entryForKey:(FSTDocumentKey *)documentKey { - [self assertValid]; - - FSTMaybeDocument *bufferedEntry = self.changes[documentKey]; - if (bufferedEntry) { - return bufferedEntry; - } else { - return [self.remoteDocumentCache entryForKey:documentKey]; - } -} - -- (void)applyToWriteGroup:(FSTWriteGroup *)group { - [self assertValid]; - - [self.changes enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, FSTMaybeDocument *value, - BOOL *stop) { - [self.remoteDocumentCache addEntry:value group:group]; - }]; - - // We should not be used to buffer any more changes. - self.changes = nil; -} - -- (void)assertValid { - FSTAssert(self.changes, @"Changes have already been applied."); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.mm b/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.mm new file mode 100644 index 0000000..bca587a --- /dev/null +++ b/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.mm @@ -0,0 +1,88 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.h" + +#import "Firestore/Source/Local/FSTRemoteDocumentCache.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTRemoteDocumentChangeBuffer () + +- (instancetype)initWithCache:(id)cache; + +/** The underlying cache we're buffering changes for. */ +@property(nonatomic, strong, nonnull) id remoteDocumentCache; + +/** The buffered changes, stored as a dictionary for easy lookups. */ +@property(nonatomic, strong, nullable) + NSMutableDictionary *changes; + +@end + +@implementation FSTRemoteDocumentChangeBuffer + ++ (instancetype)changeBufferWithCache:(id)cache { + return [[FSTRemoteDocumentChangeBuffer alloc] initWithCache:cache]; +} + +- (instancetype)initWithCache:(id)cache { + if (self = [super init]) { + _remoteDocumentCache = cache; + _changes = [NSMutableDictionary dictionary]; + } + return self; +} + +- (void)addEntry:(FSTMaybeDocument *)maybeDocument { + [self assertValid]; + + self.changes[maybeDocument.key] = maybeDocument; +} + +- (nullable FSTMaybeDocument *)entryForKey:(FSTDocumentKey *)documentKey { + [self assertValid]; + + FSTMaybeDocument *bufferedEntry = self.changes[documentKey]; + if (bufferedEntry) { + return bufferedEntry; + } else { + return [self.remoteDocumentCache entryForKey:documentKey]; + } +} + +- (void)applyToWriteGroup:(FSTWriteGroup *)group { + [self assertValid]; + + [self.changes enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, FSTMaybeDocument *value, + BOOL *stop) { + [self.remoteDocumentCache addEntry:value group:group]; + }]; + + // We should not be used to buffer any more changes. + self.changes = nil; +} + +- (void)assertValid { + FSTAssert(self.changes, @"Changes have already been applied."); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTWriteGroup.h b/Firestore/Source/Local/FSTWriteGroup.h index 5ea0387..c21ff72 100644 --- a/Firestore/Source/Local/FSTWriteGroup.h +++ b/Firestore/Source/Local/FSTWriteGroup.h @@ -16,17 +16,10 @@ #import -#ifdef __cplusplus #include #include "Firestore/Source/Local/StringView.h" - -namespace leveldb { -class DB; -class Status; -} - -#endif +#include "leveldb/db.h" NS_ASSUME_NONNULL_BEGIN @@ -61,8 +54,6 @@ NS_ASSUME_NONNULL_BEGIN /** Returns YES if the write group has no messages in it. */ - (BOOL)isEmpty; -#ifdef __cplusplus - /** * Marks the given key for deletion. * @@ -90,8 +81,6 @@ NS_ASSUME_NONNULL_BEGIN /** Writes the contents to the given LevelDB. */ - (leveldb::Status)writeToDB:(std::shared_ptr)db; -#endif - @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTWriteGroupTracker.m b/Firestore/Source/Local/FSTWriteGroupTracker.m deleted file mode 100644 index 7e3bf60..0000000 --- a/Firestore/Source/Local/FSTWriteGroupTracker.m +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTWriteGroupTracker.h" - -#import "Firestore/Source/Local/FSTWriteGroup.h" -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTWriteGroupTracker () -@property(nonatomic, strong, nullable) FSTWriteGroup *activeGroup; -@end - -@implementation FSTWriteGroupTracker - -+ (instancetype)tracker { - return [[FSTWriteGroupTracker alloc] init]; -} - -- (FSTWriteGroup *)startGroupWithAction:(NSString *)action { - // NOTE: We can relax this to allow nesting if/when we find we need it. - FSTAssert(!self.activeGroup, - @"Attempt to create write group (%@) while existing write group (%@) still active.", - action, self.activeGroup.action); - self.activeGroup = [FSTWriteGroup groupWithAction:action]; - return self.activeGroup; -} - -- (void)endGroup:(FSTWriteGroup *)group { - FSTAssert(self.activeGroup == group, - @"Attempted to end write group (%@) which is different from active group (%@)", - group.action, self.activeGroup.action); - self.activeGroup = nil; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTWriteGroupTracker.mm b/Firestore/Source/Local/FSTWriteGroupTracker.mm new file mode 100644 index 0000000..7e3bf60 --- /dev/null +++ b/Firestore/Source/Local/FSTWriteGroupTracker.mm @@ -0,0 +1,52 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Local/FSTWriteGroupTracker.h" + +#import "Firestore/Source/Local/FSTWriteGroup.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTWriteGroupTracker () +@property(nonatomic, strong, nullable) FSTWriteGroup *activeGroup; +@end + +@implementation FSTWriteGroupTracker + ++ (instancetype)tracker { + return [[FSTWriteGroupTracker alloc] init]; +} + +- (FSTWriteGroup *)startGroupWithAction:(NSString *)action { + // NOTE: We can relax this to allow nesting if/when we find we need it. + FSTAssert(!self.activeGroup, + @"Attempt to create write group (%@) while existing write group (%@) still active.", + action, self.activeGroup.action); + self.activeGroup = [FSTWriteGroup groupWithAction:action]; + return self.activeGroup; +} + +- (void)endGroup:(FSTWriteGroup *)group { + FSTAssert(self.activeGroup == group, + @"Attempted to end write group (%@) which is different from active group (%@)", + group.action, self.activeGroup.action); + self.activeGroup = nil; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/StringView.h b/Firestore/Source/Local/StringView.h index 8156193..4e36cff 100644 --- a/Firestore/Source/Local/StringView.h +++ b/Firestore/Source/Local/StringView.h @@ -17,10 +17,6 @@ #ifndef IPHONE_FIRESTORE_SOURCE_LOCAL_STRING_VIEW_H_ #define IPHONE_FIRESTORE_SOURCE_LOCAL_STRING_VIEW_H_ -#ifndef __cplusplus -#error "StringView is Objective-C++ and can only be included from .mm files" -#endif - #import #include diff --git a/Firestore/Source/Model/FSTDatabaseID.m b/Firestore/Source/Model/FSTDatabaseID.m deleted file mode 100644 index bff5855..0000000 --- a/Firestore/Source/Model/FSTDatabaseID.m +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Model/FSTDatabaseID.h" - -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -/** The default name for "unset" database ID in resource names. */ -NSString *const kDefaultDatabaseID = @"(default)"; - -#pragma mark - FSTDatabaseID - -@implementation FSTDatabaseID - -+ (instancetype)databaseIDWithProject:(NSString *)projectID database:(NSString *)databaseID { - return [[FSTDatabaseID alloc] initWithProject:projectID database:databaseID]; -} - -/** - * Designated initializer. - * - * @param projectID The project for the database. - * @param databaseID The database in the datastore. - */ -- (instancetype)initWithProject:(NSString *)projectID database:(NSString *)databaseID { - if (self = [super init]) { - FSTAssert(databaseID, @"databaseID cannot be nil"); - _projectID = [projectID copy]; - _databaseID = [databaseID copy]; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (other == self) return YES; - if (![[other class] isEqual:[self class]]) return NO; - - return [self isEqualToDatabaseId:other]; -} - -- (NSUInteger)hash { - NSUInteger hash = [self.projectID hash]; - hash = hash * 31u + [self.databaseID hash]; - return hash; -} - -- (NSString *)description { - return [NSString - stringWithFormat:@"", self.projectID, self.databaseID]; -} - -- (NSComparisonResult)compare:(FSTDatabaseID *)other { - NSComparisonResult cmp = [self.projectID compare:other.projectID]; - return cmp == NSOrderedSame ? [self.databaseID compare:other.databaseID] : cmp; -} - -- (BOOL)isDefaultDatabase { - return [self.databaseID isEqualToString:kDefaultDatabaseID]; -} - -- (BOOL)isEqualToDatabaseId:(FSTDatabaseID *)databaseID { - if (self == databaseID) return YES; - if (databaseID == nil) return NO; - if (self.projectID != databaseID.projectID && - ![self.projectID isEqualToString:databaseID.projectID]) - return NO; - if (self.databaseID != databaseID.databaseID && - ![self.databaseID isEqualToString:databaseID.databaseID]) - return NO; - return YES; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDatabaseID.mm b/Firestore/Source/Model/FSTDatabaseID.mm new file mode 100644 index 0000000..bff5855 --- /dev/null +++ b/Firestore/Source/Model/FSTDatabaseID.mm @@ -0,0 +1,90 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Model/FSTDatabaseID.h" + +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +/** The default name for "unset" database ID in resource names. */ +NSString *const kDefaultDatabaseID = @"(default)"; + +#pragma mark - FSTDatabaseID + +@implementation FSTDatabaseID + ++ (instancetype)databaseIDWithProject:(NSString *)projectID database:(NSString *)databaseID { + return [[FSTDatabaseID alloc] initWithProject:projectID database:databaseID]; +} + +/** + * Designated initializer. + * + * @param projectID The project for the database. + * @param databaseID The database in the datastore. + */ +- (instancetype)initWithProject:(NSString *)projectID database:(NSString *)databaseID { + if (self = [super init]) { + FSTAssert(databaseID, @"databaseID cannot be nil"); + _projectID = [projectID copy]; + _databaseID = [databaseID copy]; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + return [self isEqualToDatabaseId:other]; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.projectID hash]; + hash = hash * 31u + [self.databaseID hash]; + return hash; +} + +- (NSString *)description { + return [NSString + stringWithFormat:@"", self.projectID, self.databaseID]; +} + +- (NSComparisonResult)compare:(FSTDatabaseID *)other { + NSComparisonResult cmp = [self.projectID compare:other.projectID]; + return cmp == NSOrderedSame ? [self.databaseID compare:other.databaseID] : cmp; +} + +- (BOOL)isDefaultDatabase { + return [self.databaseID isEqualToString:kDefaultDatabaseID]; +} + +- (BOOL)isEqualToDatabaseId:(FSTDatabaseID *)databaseID { + if (self == databaseID) return YES; + if (databaseID == nil) return NO; + if (self.projectID != databaseID.projectID && + ![self.projectID isEqualToString:databaseID.projectID]) + return NO; + if (self.databaseID != databaseID.databaseID && + ![self.databaseID isEqualToString:databaseID.databaseID]) + return NO; + return YES; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocument.m b/Firestore/Source/Model/FSTDocument.m deleted file mode 100644 index bf416e7..0000000 --- a/Firestore/Source/Model/FSTDocument.m +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Model/FSTDocument.h" - -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTMaybeDocument () - -- (instancetype)initWithKey:(FSTDocumentKey *)key - version:(FSTSnapshotVersion *)version NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FSTMaybeDocument - -- (instancetype)initWithKey:(FSTDocumentKey *)key version:(FSTSnapshotVersion *)version { - FSTAssert(!!version, @"Version must not be nil."); - self = [super init]; - if (self) { - _key = key; - _version = version; - } - return self; -} - -- (id)copyWithZone:(NSZone *_Nullable)zone { - // All document types are immutable - return self; -} - -@end - -@implementation FSTDocument - -+ (instancetype)documentWithData:(FSTObjectValue *)data - key:(FSTDocumentKey *)key - version:(FSTSnapshotVersion *)version - hasLocalMutations:(BOOL)mutations { - return - [[FSTDocument alloc] initWithData:data key:key version:version hasLocalMutations:mutations]; -} - -- (instancetype)initWithData:(FSTObjectValue *)data - key:(FSTDocumentKey *)key - version:(FSTSnapshotVersion *)version - hasLocalMutations:(BOOL)mutations { - self = [super initWithKey:key version:version]; - if (self) { - _data = data; - _localMutations = mutations; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isKindOfClass:[FSTDocument class]]) { - return NO; - } - - FSTDocument *otherDoc = other; - return [self.key isEqual:otherDoc.key] && [self.version isEqual:otherDoc.version] && - [self.data isEqual:otherDoc.data] && self.hasLocalMutations == otherDoc.hasLocalMutations; -} - -- (NSUInteger)hash { - NSUInteger result = [self.key hash]; - result = result * 31 + [self.version hash]; - result = result * 31 + [self.data hash]; - result = result * 31 + (self.hasLocalMutations ? 1 : 0); - return result; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"", - self.key.path, self.version, - self.localMutations ? @"YES" : @"NO", self.data]; -} - -- (nullable FSTFieldValue *)fieldForPath:(FSTFieldPath *)path { - return [_data valueForPath:path]; -} - -@end - -@implementation FSTDeletedDocument - -+ (instancetype)documentWithKey:(FSTDocumentKey *)key version:(FSTSnapshotVersion *)version { - return [[FSTDeletedDocument alloc] initWithKey:key version:version]; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isKindOfClass:[FSTDeletedDocument class]]) { - return NO; - } - - FSTDocument *otherDoc = other; - return [self.key isEqual:otherDoc.key] && [self.version isEqual:otherDoc.version]; -} - -- (NSUInteger)hash { - NSUInteger result = [self.key hash]; - result = result * 31 + [self.version hash]; - return result; -} - -@end - -const NSComparator FSTDocumentComparatorByKey = - ^NSComparisonResult(FSTMaybeDocument *doc1, FSTMaybeDocument *doc2) { - return [doc1.key compare:doc2.key]; - }; - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocument.mm b/Firestore/Source/Model/FSTDocument.mm new file mode 100644 index 0000000..bf416e7 --- /dev/null +++ b/Firestore/Source/Model/FSTDocument.mm @@ -0,0 +1,139 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Model/FSTDocument.h" + +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMaybeDocument () + +- (instancetype)initWithKey:(FSTDocumentKey *)key + version:(FSTSnapshotVersion *)version NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTMaybeDocument + +- (instancetype)initWithKey:(FSTDocumentKey *)key version:(FSTSnapshotVersion *)version { + FSTAssert(!!version, @"Version must not be nil."); + self = [super init]; + if (self) { + _key = key; + _version = version; + } + return self; +} + +- (id)copyWithZone:(NSZone *_Nullable)zone { + // All document types are immutable + return self; +} + +@end + +@implementation FSTDocument + ++ (instancetype)documentWithData:(FSTObjectValue *)data + key:(FSTDocumentKey *)key + version:(FSTSnapshotVersion *)version + hasLocalMutations:(BOOL)mutations { + return + [[FSTDocument alloc] initWithData:data key:key version:version hasLocalMutations:mutations]; +} + +- (instancetype)initWithData:(FSTObjectValue *)data + key:(FSTDocumentKey *)key + version:(FSTSnapshotVersion *)version + hasLocalMutations:(BOOL)mutations { + self = [super initWithKey:key version:version]; + if (self) { + _data = data; + _localMutations = mutations; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTDocument class]]) { + return NO; + } + + FSTDocument *otherDoc = other; + return [self.key isEqual:otherDoc.key] && [self.version isEqual:otherDoc.version] && + [self.data isEqual:otherDoc.data] && self.hasLocalMutations == otherDoc.hasLocalMutations; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = result * 31 + [self.version hash]; + result = result * 31 + [self.data hash]; + result = result * 31 + (self.hasLocalMutations ? 1 : 0); + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", + self.key.path, self.version, + self.localMutations ? @"YES" : @"NO", self.data]; +} + +- (nullable FSTFieldValue *)fieldForPath:(FSTFieldPath *)path { + return [_data valueForPath:path]; +} + +@end + +@implementation FSTDeletedDocument + ++ (instancetype)documentWithKey:(FSTDocumentKey *)key version:(FSTSnapshotVersion *)version { + return [[FSTDeletedDocument alloc] initWithKey:key version:version]; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTDeletedDocument class]]) { + return NO; + } + + FSTDocument *otherDoc = other; + return [self.key isEqual:otherDoc.key] && [self.version isEqual:otherDoc.version]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = result * 31 + [self.version hash]; + return result; +} + +@end + +const NSComparator FSTDocumentComparatorByKey = + ^NSComparisonResult(FSTMaybeDocument *doc1, FSTMaybeDocument *doc2) { + return [doc1.key compare:doc2.key]; + }; + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentDictionary.m b/Firestore/Source/Model/FSTDocumentDictionary.m deleted file mode 100644 index 362af54..0000000 --- a/Firestore/Source/Model/FSTDocumentDictionary.m +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Model/FSTDocumentDictionary.h" - -#import "Firestore/Source/Model/FSTDocumentKey.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTImmutableSortedDictionary (FSTMaybeDocumentDictionary) - -+ (instancetype)maybeDocumentDictionary { - // Immutable dictionaries are contravariant in their value type, so just return a - // FSTDocumentDictionary here. - return [FSTDocumentDictionary documentDictionary]; -} - -+ (instancetype)documentDictionary { - static FSTDocumentDictionary *singleton; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - singleton = [FSTDocumentDictionary dictionaryWithComparator:FSTDocumentKeyComparator]; - }); - return singleton; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentDictionary.mm b/Firestore/Source/Model/FSTDocumentDictionary.mm new file mode 100644 index 0000000..362af54 --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentDictionary.mm @@ -0,0 +1,42 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Model/FSTDocumentDictionary.h" + +#import "Firestore/Source/Model/FSTDocumentKey.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTImmutableSortedDictionary (FSTMaybeDocumentDictionary) + ++ (instancetype)maybeDocumentDictionary { + // Immutable dictionaries are contravariant in their value type, so just return a + // FSTDocumentDictionary here. + return [FSTDocumentDictionary documentDictionary]; +} + ++ (instancetype)documentDictionary { + static FSTDocumentDictionary *singleton; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + singleton = [FSTDocumentDictionary dictionaryWithComparator:FSTDocumentKeyComparator]; + }); + return singleton; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentKey.m b/Firestore/Source/Model/FSTDocumentKey.m deleted file mode 100644 index a382a55..0000000 --- a/Firestore/Source/Model/FSTDocumentKey.m +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Model/FSTDocumentKey.h" - -#import "Firestore/Source/Core/FSTFirestoreClient.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTDocumentKey () -/** The path to the document. */ -@property(strong, nonatomic, readwrite) FSTResourcePath *path; -@end - -@implementation FSTDocumentKey - -+ (instancetype)keyWithPath:(FSTResourcePath *)path { - return [[FSTDocumentKey alloc] initWithPath:path]; -} - -+ (instancetype)keyWithSegments:(NSArray *)segments { - return [FSTDocumentKey keyWithPath:[FSTResourcePath pathWithSegments:segments]]; -} - -+ (instancetype)keyWithPathString:(NSString *)resourcePath { - NSArray *segments = [resourcePath componentsSeparatedByString:@"/"]; - return [FSTDocumentKey keyWithSegments:segments]; -} - -/** Designated initializer. */ -- (instancetype)initWithPath:(FSTResourcePath *)path { - FSTAssert([FSTDocumentKey isDocumentKey:path], @"invalid document key path: %@", path); - - if (self = [super init]) { - _path = path; - } - return self; -} - -- (BOOL)isEqual:(id)object { - if (self == object) { - return YES; - } - if (![object isKindOfClass:[FSTDocumentKey class]]) { - return NO; - } - return [self isEqualToKey:(FSTDocumentKey *)object]; -} - -- (NSUInteger)hash { - return self.path.hash; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"", self.path]; -} - -/** Implements NSCopying without actually copying because FSTDocumentKeys are immutable. */ -- (id)copyWithZone:(NSZone *_Nullable)zone { - return self; -} - -- (BOOL)isEqualToKey:(FSTDocumentKey *)other { - return FSTDocumentKeyComparator(self, other) == NSOrderedSame; -} - -- (NSComparisonResult)compare:(FSTDocumentKey *)other { - return FSTDocumentKeyComparator(self, other); -} - -+ (NSComparator)comparator { - return ^NSComparisonResult(id obj1, id obj2) { - return [obj1 compare:obj2]; - }; -} - -+ (BOOL)isDocumentKey:(FSTResourcePath *)path { - return path.length % 2 == 0; -} - -@end - -const NSComparator FSTDocumentKeyComparator = - ^NSComparisonResult(FSTDocumentKey *key1, FSTDocumentKey *key2) { - return [key1.path compare:key2.path]; - }; - -NSString *const kDocumentKeyPath = @"__name__"; - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentKey.mm b/Firestore/Source/Model/FSTDocumentKey.mm new file mode 100644 index 0000000..a382a55 --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentKey.mm @@ -0,0 +1,105 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Model/FSTDocumentKey.h" + +#import "Firestore/Source/Core/FSTFirestoreClient.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDocumentKey () +/** The path to the document. */ +@property(strong, nonatomic, readwrite) FSTResourcePath *path; +@end + +@implementation FSTDocumentKey + ++ (instancetype)keyWithPath:(FSTResourcePath *)path { + return [[FSTDocumentKey alloc] initWithPath:path]; +} + ++ (instancetype)keyWithSegments:(NSArray *)segments { + return [FSTDocumentKey keyWithPath:[FSTResourcePath pathWithSegments:segments]]; +} + ++ (instancetype)keyWithPathString:(NSString *)resourcePath { + NSArray *segments = [resourcePath componentsSeparatedByString:@"/"]; + return [FSTDocumentKey keyWithSegments:segments]; +} + +/** Designated initializer. */ +- (instancetype)initWithPath:(FSTResourcePath *)path { + FSTAssert([FSTDocumentKey isDocumentKey:path], @"invalid document key path: %@", path); + + if (self = [super init]) { + _path = path; + } + return self; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + if (![object isKindOfClass:[FSTDocumentKey class]]) { + return NO; + } + return [self isEqualToKey:(FSTDocumentKey *)object]; +} + +- (NSUInteger)hash { + return self.path.hash; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.path]; +} + +/** Implements NSCopying without actually copying because FSTDocumentKeys are immutable. */ +- (id)copyWithZone:(NSZone *_Nullable)zone { + return self; +} + +- (BOOL)isEqualToKey:(FSTDocumentKey *)other { + return FSTDocumentKeyComparator(self, other) == NSOrderedSame; +} + +- (NSComparisonResult)compare:(FSTDocumentKey *)other { + return FSTDocumentKeyComparator(self, other); +} + ++ (NSComparator)comparator { + return ^NSComparisonResult(id obj1, id obj2) { + return [obj1 compare:obj2]; + }; +} + ++ (BOOL)isDocumentKey:(FSTResourcePath *)path { + return path.length % 2 == 0; +} + +@end + +const NSComparator FSTDocumentKeyComparator = + ^NSComparisonResult(FSTDocumentKey *key1, FSTDocumentKey *key2) { + return [key1.path compare:key2.path]; + }; + +NSString *const kDocumentKeyPath = @"__name__"; + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentKeySet.m b/Firestore/Source/Model/FSTDocumentKeySet.m deleted file mode 100644 index f07b785..0000000 --- a/Firestore/Source/Model/FSTDocumentKeySet.m +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Model/FSTDocumentKeySet.h" - -#import "Firestore/Source/Model/FSTDocumentKey.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTImmutableSortedSet (FSTDocumentKey) - -+ (instancetype)keySet { - return [FSTDocumentKeySet setWithComparator:FSTDocumentKeyComparator]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentKeySet.mm b/Firestore/Source/Model/FSTDocumentKeySet.mm new file mode 100644 index 0000000..f07b785 --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentKeySet.mm @@ -0,0 +1,31 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Model/FSTDocumentKeySet.h" + +#import "Firestore/Source/Model/FSTDocumentKey.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTImmutableSortedSet (FSTDocumentKey) + ++ (instancetype)keySet { + return [FSTDocumentKeySet setWithComparator:FSTDocumentKeyComparator]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentSet.m b/Firestore/Source/Model/FSTDocumentSet.m deleted file mode 100644 index c4c0f49..0000000 --- a/Firestore/Source/Model/FSTDocumentSet.m +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Model/FSTDocumentSet.h" - -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/third_party/Immutable/FSTImmutableSortedSet.h" - -NS_ASSUME_NONNULL_BEGIN - -/** - * The type of the index of the documents in an FSTDocumentSet. - * @see FSTDocumentSet#index - */ -typedef FSTImmutableSortedDictionary IndexType; - -/** - * The type of the main collection of documents in an FSTDocumentSet. - * @see FSTDocumentSet#sortedSet - */ -typedef FSTImmutableSortedSet SetType; - -@interface FSTDocumentSet () - -- (instancetype)initWithIndex:(IndexType *)index set:(SetType *)sortedSet NS_DESIGNATED_INITIALIZER; - -/** - * An index of the documents in the FSTDocumentSet, indexed by document key. The index - * exists to guarantee the uniqueness of document keys in the set and to allow lookup and removal - * of documents by key. - */ -@property(nonatomic, strong, readonly) IndexType *index; - -/** - * The main collection of documents in the FSTDocumentSet. The documents are ordered by a - * comparator supplied from a query. The SetType collection exists in addition to the index to - * allow ordered traversal of the FSTDocumentSet. - */ -@property(nonatomic, strong, readonly) SetType *sortedSet; -@end - -@implementation FSTDocumentSet - -+ (instancetype)documentSetWithComparator:(NSComparator)comparator { - IndexType *index = - [FSTImmutableSortedDictionary dictionaryWithComparator:FSTDocumentKeyComparator]; - SetType *set = [FSTImmutableSortedSet setWithComparator:comparator]; - return [[FSTDocumentSet alloc] initWithIndex:index set:set]; -} - -- (instancetype)initWithIndex:(IndexType *)index set:(SetType *)sortedSet { - self = [super init]; - if (self) { - _index = index; - _sortedSet = sortedSet; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isMemberOfClass:[FSTDocumentSet class]]) { - return NO; - } - - FSTDocumentSet *otherSet = (FSTDocumentSet *)other; - if ([self count] != [otherSet count]) { - return NO; - } - - NSEnumerator *selfIter = [self.sortedSet objectEnumerator]; - NSEnumerator *otherIter = [otherSet.sortedSet objectEnumerator]; - - FSTDocument *selfDoc = [selfIter nextObject]; - FSTDocument *otherDoc = [otherIter nextObject]; - while (selfDoc) { - if (![selfDoc isEqual:otherDoc]) { - return NO; - } - selfDoc = [selfIter nextObject]; - otherDoc = [otherIter nextObject]; - } - return YES; -} - -- (NSUInteger)hash { - NSUInteger hash = 0; - for (FSTDocument *doc in self.sortedSet.objectEnumerator) { - hash = 31 * hash + [doc hash]; - } - return hash; -} - -- (NSString *)description { - return [self.sortedSet description]; -} - -- (NSUInteger)count { - return [self.index count]; -} - -- (BOOL)isEmpty { - return [self.index isEmpty]; -} - -- (BOOL)containsKey:(FSTDocumentKey *)key { - return [self.index objectForKey:key] != nil; -} - -- (FSTDocument *_Nullable)documentForKey:(FSTDocumentKey *)key { - return [self.index objectForKey:key]; -} - -- (FSTDocument *_Nullable)firstDocument { - return [self.sortedSet firstObject]; -} - -- (FSTDocument *_Nullable)lastDocument { - return [self.sortedSet lastObject]; -} - -- (FSTDocument *_Nullable)predecessorDocumentForKey:(FSTDocumentKey *)key { - FSTDocument *doc = [self.index objectForKey:key]; - if (!doc) { - @throw [NSException exceptionWithName:NSInvalidArgumentException - reason:[NSString stringWithFormat:@"Key %@ does not exist", key] - userInfo:nil]; - } - return [self.sortedSet predecessorObject:doc]; -} - -- (NSUInteger)indexOfKey:(FSTDocumentKey *)key { - FSTDocument *doc = [self.index objectForKey:key]; - return doc ? [self.sortedSet indexOfObject:doc] : NSNotFound; -} - -- (NSEnumerator *)documentEnumerator { - return [self.sortedSet objectEnumerator]; -} - -- (NSArray *)arrayValue { - NSMutableArray *result = [NSMutableArray arrayWithCapacity:self.count]; - for (FSTDocument *doc in self.documentEnumerator) { - [result addObject:doc]; - } - return result; -} - -- (FSTMaybeDocumentDictionary *)dictionaryValue { - return self.index; -} - -- (instancetype)documentSetByAddingDocument:(FSTDocument *_Nullable)document { - // TODO(mcg): look into making document nonnull. - if (!document) { - return self; - } - - // Remove any prior mapping of the document's key before adding, preventing sortedSet from - // accumulating values that aren't in the index. - FSTDocumentSet *removed = [self documentSetByRemovingKey:document.key]; - - IndexType *index = [removed.index dictionaryBySettingObject:document forKey:document.key]; - SetType *set = [removed.sortedSet setByAddingObject:document]; - return [[FSTDocumentSet alloc] initWithIndex:index set:set]; -} - -- (instancetype)documentSetByRemovingKey:(FSTDocumentKey *)key { - FSTDocument *doc = [self.index objectForKey:key]; - if (!doc) { - return self; - } - - IndexType *index = [self.index dictionaryByRemovingObjectForKey:key]; - SetType *set = [self.sortedSet setByRemovingObject:doc]; - return [[FSTDocumentSet alloc] initWithIndex:index set:set]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentSet.mm b/Firestore/Source/Model/FSTDocumentSet.mm new file mode 100644 index 0000000..c4c0f49 --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentSet.mm @@ -0,0 +1,197 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Model/FSTDocumentSet.h" + +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/third_party/Immutable/FSTImmutableSortedSet.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The type of the index of the documents in an FSTDocumentSet. + * @see FSTDocumentSet#index + */ +typedef FSTImmutableSortedDictionary IndexType; + +/** + * The type of the main collection of documents in an FSTDocumentSet. + * @see FSTDocumentSet#sortedSet + */ +typedef FSTImmutableSortedSet SetType; + +@interface FSTDocumentSet () + +- (instancetype)initWithIndex:(IndexType *)index set:(SetType *)sortedSet NS_DESIGNATED_INITIALIZER; + +/** + * An index of the documents in the FSTDocumentSet, indexed by document key. The index + * exists to guarantee the uniqueness of document keys in the set and to allow lookup and removal + * of documents by key. + */ +@property(nonatomic, strong, readonly) IndexType *index; + +/** + * The main collection of documents in the FSTDocumentSet. The documents are ordered by a + * comparator supplied from a query. The SetType collection exists in addition to the index to + * allow ordered traversal of the FSTDocumentSet. + */ +@property(nonatomic, strong, readonly) SetType *sortedSet; +@end + +@implementation FSTDocumentSet + ++ (instancetype)documentSetWithComparator:(NSComparator)comparator { + IndexType *index = + [FSTImmutableSortedDictionary dictionaryWithComparator:FSTDocumentKeyComparator]; + SetType *set = [FSTImmutableSortedSet setWithComparator:comparator]; + return [[FSTDocumentSet alloc] initWithIndex:index set:set]; +} + +- (instancetype)initWithIndex:(IndexType *)index set:(SetType *)sortedSet { + self = [super init]; + if (self) { + _index = index; + _sortedSet = sortedSet; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTDocumentSet class]]) { + return NO; + } + + FSTDocumentSet *otherSet = (FSTDocumentSet *)other; + if ([self count] != [otherSet count]) { + return NO; + } + + NSEnumerator *selfIter = [self.sortedSet objectEnumerator]; + NSEnumerator *otherIter = [otherSet.sortedSet objectEnumerator]; + + FSTDocument *selfDoc = [selfIter nextObject]; + FSTDocument *otherDoc = [otherIter nextObject]; + while (selfDoc) { + if (![selfDoc isEqual:otherDoc]) { + return NO; + } + selfDoc = [selfIter nextObject]; + otherDoc = [otherIter nextObject]; + } + return YES; +} + +- (NSUInteger)hash { + NSUInteger hash = 0; + for (FSTDocument *doc in self.sortedSet.objectEnumerator) { + hash = 31 * hash + [doc hash]; + } + return hash; +} + +- (NSString *)description { + return [self.sortedSet description]; +} + +- (NSUInteger)count { + return [self.index count]; +} + +- (BOOL)isEmpty { + return [self.index isEmpty]; +} + +- (BOOL)containsKey:(FSTDocumentKey *)key { + return [self.index objectForKey:key] != nil; +} + +- (FSTDocument *_Nullable)documentForKey:(FSTDocumentKey *)key { + return [self.index objectForKey:key]; +} + +- (FSTDocument *_Nullable)firstDocument { + return [self.sortedSet firstObject]; +} + +- (FSTDocument *_Nullable)lastDocument { + return [self.sortedSet lastObject]; +} + +- (FSTDocument *_Nullable)predecessorDocumentForKey:(FSTDocumentKey *)key { + FSTDocument *doc = [self.index objectForKey:key]; + if (!doc) { + @throw [NSException exceptionWithName:NSInvalidArgumentException + reason:[NSString stringWithFormat:@"Key %@ does not exist", key] + userInfo:nil]; + } + return [self.sortedSet predecessorObject:doc]; +} + +- (NSUInteger)indexOfKey:(FSTDocumentKey *)key { + FSTDocument *doc = [self.index objectForKey:key]; + return doc ? [self.sortedSet indexOfObject:doc] : NSNotFound; +} + +- (NSEnumerator *)documentEnumerator { + return [self.sortedSet objectEnumerator]; +} + +- (NSArray *)arrayValue { + NSMutableArray *result = [NSMutableArray arrayWithCapacity:self.count]; + for (FSTDocument *doc in self.documentEnumerator) { + [result addObject:doc]; + } + return result; +} + +- (FSTMaybeDocumentDictionary *)dictionaryValue { + return self.index; +} + +- (instancetype)documentSetByAddingDocument:(FSTDocument *_Nullable)document { + // TODO(mcg): look into making document nonnull. + if (!document) { + return self; + } + + // Remove any prior mapping of the document's key before adding, preventing sortedSet from + // accumulating values that aren't in the index. + FSTDocumentSet *removed = [self documentSetByRemovingKey:document.key]; + + IndexType *index = [removed.index dictionaryBySettingObject:document forKey:document.key]; + SetType *set = [removed.sortedSet setByAddingObject:document]; + return [[FSTDocumentSet alloc] initWithIndex:index set:set]; +} + +- (instancetype)documentSetByRemovingKey:(FSTDocumentKey *)key { + FSTDocument *doc = [self.index objectForKey:key]; + if (!doc) { + return self; + } + + IndexType *index = [self.index dictionaryByRemovingObjectForKey:key]; + SetType *set = [self.sortedSet setByRemovingObject:doc]; + return [[FSTDocumentSet alloc] initWithIndex:index set:set]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentVersionDictionary.m b/Firestore/Source/Model/FSTDocumentVersionDictionary.m deleted file mode 100644 index 870e082..0000000 --- a/Firestore/Source/Model/FSTDocumentVersionDictionary.m +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Model/FSTDocumentVersionDictionary.h" - -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTImmutableSortedDictionary (FSTDocumentVersionDictionary) - -+ (instancetype)documentVersionDictionary { - static FSTDocumentVersionDictionary *singleton; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - singleton = [FSTDocumentVersionDictionary dictionaryWithComparator:FSTDocumentKeyComparator]; - }); - return singleton; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentVersionDictionary.mm b/Firestore/Source/Model/FSTDocumentVersionDictionary.mm new file mode 100644 index 0000000..870e082 --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentVersionDictionary.mm @@ -0,0 +1,37 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Model/FSTDocumentVersionDictionary.h" + +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTImmutableSortedDictionary (FSTDocumentVersionDictionary) + ++ (instancetype)documentVersionDictionary { + static FSTDocumentVersionDictionary *singleton; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + singleton = [FSTDocumentVersionDictionary dictionaryWithComparator:FSTDocumentKeyComparator]; + }); + return singleton; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTMutation.m b/Firestore/Source/Model/FSTMutation.m deleted file mode 100644 index c249138..0000000 --- a/Firestore/Source/Model/FSTMutation.m +++ /dev/null @@ -1,593 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Model/FSTMutation.h" - -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Core/FSTTimestamp.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTClasses.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTFieldMask - -@implementation FSTFieldMask - -- (instancetype)initWithFields:(NSArray *)fields { - if (self = [super init]) { - _fields = fields; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isKindOfClass:[FSTFieldMask class]]) { - return NO; - } - - FSTFieldMask *otherMask = (FSTFieldMask *)other; - return [self.fields isEqual:otherMask.fields]; -} - -- (NSUInteger)hash { - return self.fields.hash; -} -@end - -#pragma mark - FSTServerTimestampTransform - -@implementation FSTServerTimestampTransform - -+ (instancetype)serverTimestampTransform { - static FSTServerTimestampTransform *sharedInstance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sharedInstance = [[FSTServerTimestampTransform alloc] init]; - }); - return sharedInstance; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - return [other isKindOfClass:[FSTServerTimestampTransform class]]; -} - -- (NSUInteger)hash { - // arbitrary number since all instances are equal. - return 37; -} - -@end - -#pragma mark - FSTFieldTransform - -@implementation FSTFieldTransform - -- (instancetype)initWithPath:(FSTFieldPath *)path transform:(id)transform { - self = [super init]; - if (self) { - _path = path; - _transform = transform; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (other == self) return YES; - if (![[other class] isEqual:[self class]]) return NO; - FSTFieldTransform *otherFieldTransform = other; - return [self.path isEqual:otherFieldTransform.path] && - [self.transform isEqual:otherFieldTransform.transform]; -} - -- (NSUInteger)hash { - NSUInteger hash = [self.path hash]; - hash = hash * 31 + [self.transform hash]; - return hash; -} - -@end - -#pragma mark - FSTPrecondition - -@implementation FSTPrecondition - -+ (FSTPrecondition *)preconditionWithExists:(BOOL)exists { - FSTPreconditionExists existsEnum = exists ? FSTPreconditionExistsYes : FSTPreconditionExistsNo; - return [[FSTPrecondition alloc] initWithUpdateTime:nil exists:existsEnum]; -} - -+ (FSTPrecondition *)preconditionWithUpdateTime:(FSTSnapshotVersion *)updateTime { - return [[FSTPrecondition alloc] initWithUpdateTime:updateTime exists:FSTPreconditionExistsNotSet]; -} - -+ (FSTPrecondition *)none { - static dispatch_once_t onceToken; - static FSTPrecondition *noPrecondition; - dispatch_once(&onceToken, ^{ - noPrecondition = - [[FSTPrecondition alloc] initWithUpdateTime:nil exists:FSTPreconditionExistsNotSet]; - }); - return noPrecondition; -} - -- (instancetype)initWithUpdateTime:(FSTSnapshotVersion *_Nullable)updateTime - exists:(FSTPreconditionExists)exists { - if (self = [super init]) { - _updateTime = updateTime; - _exists = exists; - } - return self; -} - -- (BOOL)isValidForDocument:(FSTMaybeDocument *_Nullable)maybeDoc { - if (self.updateTime) { - return - [maybeDoc isKindOfClass:[FSTDocument class]] && [maybeDoc.version isEqual:self.updateTime]; - } else if (self.exists != FSTPreconditionExistsNotSet) { - if (self.exists == FSTPreconditionExistsYes) { - return [maybeDoc isKindOfClass:[FSTDocument class]]; - } else { - FSTAssert(self.exists == FSTPreconditionExistsNo, @"Invalid precondition"); - return maybeDoc == nil || [maybeDoc isKindOfClass:[FSTDeletedDocument class]]; - } - } else { - FSTAssert(self.isNone, @"Precondition should be empty"); - return YES; - } -} - -- (BOOL)isNone { - return self.updateTime == nil && self.exists == FSTPreconditionExistsNotSet; -} - -- (BOOL)isEqual:(id)other { - if (self == other) { - return YES; - } - - if (![other isKindOfClass:[FSTPrecondition class]]) { - return NO; - } - - FSTPrecondition *otherPrecondition = (FSTPrecondition *)other; - // Compare references to cover nil equality - return (self.updateTime == otherPrecondition.updateTime || - [self.updateTime isEqual:otherPrecondition.updateTime]) && - self.exists == otherPrecondition.exists; -} - -- (NSUInteger)hash { - NSUInteger hash = [self.updateTime hash]; - hash = hash * 31 + self.exists; - return hash; -} - -- (NSString *)description { - if (self.isNone) { - return @">"; - } else { - NSString *existsString; - switch (self.exists) { - case FSTPreconditionExistsYes: - existsString = @"yes"; - break; - case FSTPreconditionExistsNo: - existsString = @"no"; - break; - default: - existsString = @""; - break; - } - return [NSString stringWithFormat:@"", self.updateTime, - existsString]; - } -} - -@end - -#pragma mark - FSTMutationResult - -@implementation FSTMutationResult - -- (instancetype)initWithVersion:(FSTSnapshotVersion *_Nullable)version - transformResults:(NSArray *_Nullable)transformResults { - if (self = [super init]) { - _version = version; - _transformResults = transformResults; - } - return self; -} - -@end - -#pragma mark - FSTMutation - -@implementation FSTMutation - -- (instancetype)initWithKey:(FSTDocumentKey *)key precondition:(FSTPrecondition *)precondition { - if (self = [super init]) { - _key = key; - _precondition = precondition; - } - return self; -} - -- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc - baseDocument:(nullable FSTMaybeDocument *)baseDoc - localWriteTime:(FSTTimestamp *)localWriteTime - mutationResult:(nullable FSTMutationResult *)mutationResult { - @throw FSTAbstractMethodException(); // NOLINT -} - -- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc - baseDocument:(nullable FSTMaybeDocument *)baseDoc - localWriteTime:(nullable FSTTimestamp *)localWriteTime { - return - [self applyTo:maybeDoc baseDocument:baseDoc localWriteTime:localWriteTime mutationResult:nil]; -} - -@end - -#pragma mark - FSTSetMutation - -@implementation FSTSetMutation - -- (instancetype)initWithKey:(FSTDocumentKey *)key - value:(FSTObjectValue *)value - precondition:(FSTPrecondition *)precondition { - if (self = [super initWithKey:key precondition:precondition]) { - _value = value; - } - return self; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"", self.key, - self.value, self.precondition]; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isKindOfClass:[FSTSetMutation class]]) { - return NO; - } - - FSTSetMutation *otherMutation = (FSTSetMutation *)other; - return [self.key isEqual:otherMutation.key] && [self.value isEqual:otherMutation.value] && - [self.precondition isEqual:otherMutation.precondition]; -} - -- (NSUInteger)hash { - NSUInteger result = [self.key hash]; - result = 31 * result + [self.precondition hash]; - result = 31 * result + [self.value hash]; - return result; -} - -- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc - baseDocument:(nullable FSTMaybeDocument *)baseDoc - localWriteTime:(FSTTimestamp *)localWriteTime - mutationResult:(nullable FSTMutationResult *)mutationResult { - if (mutationResult) { - FSTAssert(!mutationResult.transformResults, @"Transform results received by FSTSetMutation."); - } - - if (![self.precondition isValidForDocument:maybeDoc]) { - return maybeDoc; - } - - BOOL hasLocalMutations = (mutationResult == nil); - if (!maybeDoc || [maybeDoc isMemberOfClass:[FSTDeletedDocument class]]) { - // If the document didn't exist before, create it. - return [FSTDocument documentWithData:self.value - key:self.key - version:[FSTSnapshotVersion noVersion] - hasLocalMutations:hasLocalMutations]; - } - - FSTAssert([maybeDoc isMemberOfClass:[FSTDocument class]], @"Unknown MaybeDocument type %@", - [maybeDoc class]); - FSTDocument *doc = (FSTDocument *)maybeDoc; - - FSTAssert([doc.key isEqual:self.key], @"Can only set a document with the same key"); - return [FSTDocument documentWithData:self.value - key:doc.key - version:doc.version - hasLocalMutations:hasLocalMutations]; -} -@end - -#pragma mark - FSTPatchMutation - -@implementation FSTPatchMutation - -- (instancetype)initWithKey:(FSTDocumentKey *)key - fieldMask:(FSTFieldMask *)fieldMask - value:(FSTObjectValue *)value - precondition:(FSTPrecondition *)precondition { - self = [super initWithKey:key precondition:precondition]; - if (self) { - _fieldMask = fieldMask; - _value = value; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isKindOfClass:[FSTPatchMutation class]]) { - return NO; - } - - FSTPatchMutation *otherMutation = (FSTPatchMutation *)other; - return [self.key isEqual:otherMutation.key] && [self.fieldMask isEqual:otherMutation.fieldMask] && - [self.value isEqual:otherMutation.value] && - [self.precondition isEqual:otherMutation.precondition]; -} - -- (NSUInteger)hash { - NSUInteger result = [self.key hash]; - result = 31 * result + [self.precondition hash]; - result = 31 * result + [self.fieldMask hash]; - result = 31 * result + [self.value hash]; - return result; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"", - self.key, self.fieldMask, self.value, self.precondition]; -} - -- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc - baseDocument:(nullable FSTMaybeDocument *)baseDoc - localWriteTime:(FSTTimestamp *)localWriteTime - mutationResult:(nullable FSTMutationResult *)mutationResult { - if (mutationResult) { - FSTAssert(!mutationResult.transformResults, @"Transform results received by FSTPatchMutation."); - } - - if (![self.precondition isValidForDocument:maybeDoc]) { - return maybeDoc; - } - - BOOL hasLocalMutations = (mutationResult == nil); - if (!maybeDoc || [maybeDoc isMemberOfClass:[FSTDeletedDocument class]]) { - // Precondition applied, so create the document if necessary - FSTDocumentKey *key = maybeDoc ? maybeDoc.key : self.key; - FSTSnapshotVersion *version = maybeDoc ? maybeDoc.version : [FSTSnapshotVersion noVersion]; - maybeDoc = [FSTDocument documentWithData:[FSTObjectValue objectValue] - key:key - version:version - hasLocalMutations:hasLocalMutations]; - } - - FSTAssert([maybeDoc isMemberOfClass:[FSTDocument class]], @"Unknown MaybeDocument type %@", - [maybeDoc class]); - FSTDocument *doc = (FSTDocument *)maybeDoc; - - FSTAssert([doc.key isEqual:self.key], @"Can only patch a document with the same key"); - - FSTObjectValue *newData = [self patchObjectValue:doc.data]; - return [FSTDocument documentWithData:newData - key:doc.key - version:doc.version - hasLocalMutations:hasLocalMutations]; -} - -- (FSTObjectValue *)patchObjectValue:(FSTObjectValue *)objectValue { - FSTObjectValue *result = objectValue; - for (FSTFieldPath *fieldPath in self.fieldMask.fields) { - FSTFieldValue *newValue = [self.value valueForPath:fieldPath]; - if (newValue) { - result = [result objectBySettingValue:newValue forPath:fieldPath]; - } else { - result = [result objectByDeletingPath:fieldPath]; - } - } - return result; -} - -@end - -@implementation FSTTransformMutation - -- (instancetype)initWithKey:(FSTDocumentKey *)key - fieldTransforms:(NSArray *)fieldTransforms { - // NOTE: We set a precondition of exists: true as a safety-check, since we always combine - // FSTTransformMutations with a FSTSetMutation or FSTPatchMutation which (if successful) should - // end up with an existing document. - if (self = [super initWithKey:key precondition:[FSTPrecondition preconditionWithExists:YES]]) { - _fieldTransforms = fieldTransforms; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isKindOfClass:[FSTTransformMutation class]]) { - return NO; - } - - FSTTransformMutation *otherMutation = (FSTTransformMutation *)other; - return [self.key isEqual:otherMutation.key] && - [self.fieldTransforms isEqual:otherMutation.fieldTransforms] && - [self.precondition isEqual:otherMutation.precondition]; -} - -- (NSUInteger)hash { - NSUInteger result = [self.key hash]; - result = 31 * result + [self.precondition hash]; - result = 31 * result + [self.fieldTransforms hash]; - return result; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"", - self.key, self.fieldTransforms, self.precondition]; -} - -- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc - baseDocument:(nullable FSTMaybeDocument *)baseDoc - localWriteTime:(FSTTimestamp *)localWriteTime - mutationResult:(nullable FSTMutationResult *)mutationResult { - if (mutationResult) { - FSTAssert(mutationResult.transformResults, - @"Transform results missing for FSTTransformMutation."); - } - - if (![self.precondition isValidForDocument:maybeDoc]) { - return maybeDoc; - } - - // We only support transforms with precondition exists, so we can only apply it to an existing - // document - FSTAssert([maybeDoc isMemberOfClass:[FSTDocument class]], @"Unknown MaybeDocument type %@", - [maybeDoc class]); - FSTDocument *doc = (FSTDocument *)maybeDoc; - - FSTAssert([doc.key isEqual:self.key], @"Can only patch a document with the same key"); - - BOOL hasLocalMutations = (mutationResult == nil); - NSArray *transformResults = - mutationResult - ? mutationResult.transformResults - : [self localTransformResultsWithBaseDocument:baseDoc writeTime:localWriteTime]; - FSTObjectValue *newData = [self transformObject:doc.data transformResults:transformResults]; - return [FSTDocument documentWithData:newData - key:doc.key - version:doc.version - hasLocalMutations:hasLocalMutations]; -} - -/** - * Creates an array of "transform results" (a transform result is a field value representing the - * result of applying a transform) for use when applying an FSTTransformMutation locally. - * - * @param baseDocument The document prior to applying this mutation batch. - * @param localWriteTime The local time of the transform mutation (used to generate - * FSTServerTimestampValues). - * @return The transform results array. - */ -- (NSArray *)localTransformResultsWithBaseDocument: - (FSTMaybeDocument *_Nullable)baseDocument - writeTime:(FSTTimestamp *)localWriteTime { - NSMutableArray *transformResults = [NSMutableArray array]; - for (FSTFieldTransform *fieldTransform in self.fieldTransforms) { - if ([fieldTransform.transform isKindOfClass:[FSTServerTimestampTransform class]]) { - FSTFieldValue *previousValue = nil; - - if ([baseDocument isMemberOfClass:[FSTDocument class]]) { - previousValue = [((FSTDocument *)baseDocument) fieldForPath:fieldTransform.path]; - } - - [transformResults - addObject:[FSTServerTimestampValue serverTimestampValueWithLocalWriteTime:localWriteTime - previousValue:previousValue]]; - } else { - FSTFail(@"Encountered unknown transform: %@", fieldTransform); - } - } - return transformResults; -} - -- (FSTObjectValue *)transformObject:(FSTObjectValue *)objectValue - transformResults:(NSArray *)transformResults { - FSTAssert(transformResults.count == self.fieldTransforms.count, - @"Transform results length mismatch."); - - for (NSUInteger i = 0; i < self.fieldTransforms.count; i++) { - FSTFieldTransform *fieldTransform = self.fieldTransforms[i]; - id transform = fieldTransform.transform; - FSTFieldPath *fieldPath = fieldTransform.path; - if ([transform isKindOfClass:[FSTServerTimestampTransform class]]) { - objectValue = [objectValue objectBySettingValue:transformResults[i] forPath:fieldPath]; - } else { - FSTFail(@"Encountered unknown transform: %@", transform); - } - } - return objectValue; -} - -@end - -#pragma mark - FSTDeleteMutation - -@implementation FSTDeleteMutation - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isKindOfClass:[FSTDeleteMutation class]]) { - return NO; - } - - FSTDeleteMutation *otherMutation = (FSTDeleteMutation *)other; - return [self.key isEqual:otherMutation.key] && - [self.precondition isEqual:otherMutation.precondition]; -} - -- (NSUInteger)hash { - NSUInteger result = [self.key hash]; - result = 31 * result + [self.precondition hash]; - return result; -} - -- (NSString *)description { - return [NSString - stringWithFormat:@"", self.key, self.precondition]; -} - -- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc - baseDocument:(nullable FSTMaybeDocument *)baseDoc - localWriteTime:(FSTTimestamp *)localWriteTime - mutationResult:(nullable FSTMutationResult *)mutationResult { - if (mutationResult) { - FSTAssert(!mutationResult.transformResults, - @"Transform results received by FSTDeleteMutation."); - } - - if (![self.precondition isValidForDocument:maybeDoc]) { - return maybeDoc; - } - - if (maybeDoc) { - FSTAssert([maybeDoc.key isEqual:self.key], @"Can only delete a document with the same key"); - } - - return [FSTDeletedDocument documentWithKey:self.key version:[FSTSnapshotVersion noVersion]]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTMutation.mm b/Firestore/Source/Model/FSTMutation.mm new file mode 100644 index 0000000..c249138 --- /dev/null +++ b/Firestore/Source/Model/FSTMutation.mm @@ -0,0 +1,593 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Model/FSTMutation.h" + +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Core/FSTTimestamp.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTClasses.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTFieldMask + +@implementation FSTFieldMask + +- (instancetype)initWithFields:(NSArray *)fields { + if (self = [super init]) { + _fields = fields; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTFieldMask class]]) { + return NO; + } + + FSTFieldMask *otherMask = (FSTFieldMask *)other; + return [self.fields isEqual:otherMask.fields]; +} + +- (NSUInteger)hash { + return self.fields.hash; +} +@end + +#pragma mark - FSTServerTimestampTransform + +@implementation FSTServerTimestampTransform + ++ (instancetype)serverTimestampTransform { + static FSTServerTimestampTransform *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTServerTimestampTransform alloc] init]; + }); + return sharedInstance; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + return [other isKindOfClass:[FSTServerTimestampTransform class]]; +} + +- (NSUInteger)hash { + // arbitrary number since all instances are equal. + return 37; +} + +@end + +#pragma mark - FSTFieldTransform + +@implementation FSTFieldTransform + +- (instancetype)initWithPath:(FSTFieldPath *)path transform:(id)transform { + self = [super init]; + if (self) { + _path = path; + _transform = transform; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + FSTFieldTransform *otherFieldTransform = other; + return [self.path isEqual:otherFieldTransform.path] && + [self.transform isEqual:otherFieldTransform.transform]; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.path hash]; + hash = hash * 31 + [self.transform hash]; + return hash; +} + +@end + +#pragma mark - FSTPrecondition + +@implementation FSTPrecondition + ++ (FSTPrecondition *)preconditionWithExists:(BOOL)exists { + FSTPreconditionExists existsEnum = exists ? FSTPreconditionExistsYes : FSTPreconditionExistsNo; + return [[FSTPrecondition alloc] initWithUpdateTime:nil exists:existsEnum]; +} + ++ (FSTPrecondition *)preconditionWithUpdateTime:(FSTSnapshotVersion *)updateTime { + return [[FSTPrecondition alloc] initWithUpdateTime:updateTime exists:FSTPreconditionExistsNotSet]; +} + ++ (FSTPrecondition *)none { + static dispatch_once_t onceToken; + static FSTPrecondition *noPrecondition; + dispatch_once(&onceToken, ^{ + noPrecondition = + [[FSTPrecondition alloc] initWithUpdateTime:nil exists:FSTPreconditionExistsNotSet]; + }); + return noPrecondition; +} + +- (instancetype)initWithUpdateTime:(FSTSnapshotVersion *_Nullable)updateTime + exists:(FSTPreconditionExists)exists { + if (self = [super init]) { + _updateTime = updateTime; + _exists = exists; + } + return self; +} + +- (BOOL)isValidForDocument:(FSTMaybeDocument *_Nullable)maybeDoc { + if (self.updateTime) { + return + [maybeDoc isKindOfClass:[FSTDocument class]] && [maybeDoc.version isEqual:self.updateTime]; + } else if (self.exists != FSTPreconditionExistsNotSet) { + if (self.exists == FSTPreconditionExistsYes) { + return [maybeDoc isKindOfClass:[FSTDocument class]]; + } else { + FSTAssert(self.exists == FSTPreconditionExistsNo, @"Invalid precondition"); + return maybeDoc == nil || [maybeDoc isKindOfClass:[FSTDeletedDocument class]]; + } + } else { + FSTAssert(self.isNone, @"Precondition should be empty"); + return YES; + } +} + +- (BOOL)isNone { + return self.updateTime == nil && self.exists == FSTPreconditionExistsNotSet; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } + + if (![other isKindOfClass:[FSTPrecondition class]]) { + return NO; + } + + FSTPrecondition *otherPrecondition = (FSTPrecondition *)other; + // Compare references to cover nil equality + return (self.updateTime == otherPrecondition.updateTime || + [self.updateTime isEqual:otherPrecondition.updateTime]) && + self.exists == otherPrecondition.exists; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.updateTime hash]; + hash = hash * 31 + self.exists; + return hash; +} + +- (NSString *)description { + if (self.isNone) { + return @">"; + } else { + NSString *existsString; + switch (self.exists) { + case FSTPreconditionExistsYes: + existsString = @"yes"; + break; + case FSTPreconditionExistsNo: + existsString = @"no"; + break; + default: + existsString = @""; + break; + } + return [NSString stringWithFormat:@"", self.updateTime, + existsString]; + } +} + +@end + +#pragma mark - FSTMutationResult + +@implementation FSTMutationResult + +- (instancetype)initWithVersion:(FSTSnapshotVersion *_Nullable)version + transformResults:(NSArray *_Nullable)transformResults { + if (self = [super init]) { + _version = version; + _transformResults = transformResults; + } + return self; +} + +@end + +#pragma mark - FSTMutation + +@implementation FSTMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key precondition:(FSTPrecondition *)precondition { + if (self = [super init]) { + _key = key; + _precondition = precondition; + } + return self; +} + +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(nullable FSTMutationResult *)mutationResult { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc + localWriteTime:(nullable FSTTimestamp *)localWriteTime { + return + [self applyTo:maybeDoc baseDocument:baseDoc localWriteTime:localWriteTime mutationResult:nil]; +} + +@end + +#pragma mark - FSTSetMutation + +@implementation FSTSetMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key + value:(FSTObjectValue *)value + precondition:(FSTPrecondition *)precondition { + if (self = [super initWithKey:key precondition:precondition]) { + _value = value; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.key, + self.value, self.precondition]; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTSetMutation class]]) { + return NO; + } + + FSTSetMutation *otherMutation = (FSTSetMutation *)other; + return [self.key isEqual:otherMutation.key] && [self.value isEqual:otherMutation.value] && + [self.precondition isEqual:otherMutation.precondition]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = 31 * result + [self.precondition hash]; + result = 31 * result + [self.value hash]; + return result; +} + +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(nullable FSTMutationResult *)mutationResult { + if (mutationResult) { + FSTAssert(!mutationResult.transformResults, @"Transform results received by FSTSetMutation."); + } + + if (![self.precondition isValidForDocument:maybeDoc]) { + return maybeDoc; + } + + BOOL hasLocalMutations = (mutationResult == nil); + if (!maybeDoc || [maybeDoc isMemberOfClass:[FSTDeletedDocument class]]) { + // If the document didn't exist before, create it. + return [FSTDocument documentWithData:self.value + key:self.key + version:[FSTSnapshotVersion noVersion] + hasLocalMutations:hasLocalMutations]; + } + + FSTAssert([maybeDoc isMemberOfClass:[FSTDocument class]], @"Unknown MaybeDocument type %@", + [maybeDoc class]); + FSTDocument *doc = (FSTDocument *)maybeDoc; + + FSTAssert([doc.key isEqual:self.key], @"Can only set a document with the same key"); + return [FSTDocument documentWithData:self.value + key:doc.key + version:doc.version + hasLocalMutations:hasLocalMutations]; +} +@end + +#pragma mark - FSTPatchMutation + +@implementation FSTPatchMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key + fieldMask:(FSTFieldMask *)fieldMask + value:(FSTObjectValue *)value + precondition:(FSTPrecondition *)precondition { + self = [super initWithKey:key precondition:precondition]; + if (self) { + _fieldMask = fieldMask; + _value = value; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTPatchMutation class]]) { + return NO; + } + + FSTPatchMutation *otherMutation = (FSTPatchMutation *)other; + return [self.key isEqual:otherMutation.key] && [self.fieldMask isEqual:otherMutation.fieldMask] && + [self.value isEqual:otherMutation.value] && + [self.precondition isEqual:otherMutation.precondition]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = 31 * result + [self.precondition hash]; + result = 31 * result + [self.fieldMask hash]; + result = 31 * result + [self.value hash]; + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", + self.key, self.fieldMask, self.value, self.precondition]; +} + +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(nullable FSTMutationResult *)mutationResult { + if (mutationResult) { + FSTAssert(!mutationResult.transformResults, @"Transform results received by FSTPatchMutation."); + } + + if (![self.precondition isValidForDocument:maybeDoc]) { + return maybeDoc; + } + + BOOL hasLocalMutations = (mutationResult == nil); + if (!maybeDoc || [maybeDoc isMemberOfClass:[FSTDeletedDocument class]]) { + // Precondition applied, so create the document if necessary + FSTDocumentKey *key = maybeDoc ? maybeDoc.key : self.key; + FSTSnapshotVersion *version = maybeDoc ? maybeDoc.version : [FSTSnapshotVersion noVersion]; + maybeDoc = [FSTDocument documentWithData:[FSTObjectValue objectValue] + key:key + version:version + hasLocalMutations:hasLocalMutations]; + } + + FSTAssert([maybeDoc isMemberOfClass:[FSTDocument class]], @"Unknown MaybeDocument type %@", + [maybeDoc class]); + FSTDocument *doc = (FSTDocument *)maybeDoc; + + FSTAssert([doc.key isEqual:self.key], @"Can only patch a document with the same key"); + + FSTObjectValue *newData = [self patchObjectValue:doc.data]; + return [FSTDocument documentWithData:newData + key:doc.key + version:doc.version + hasLocalMutations:hasLocalMutations]; +} + +- (FSTObjectValue *)patchObjectValue:(FSTObjectValue *)objectValue { + FSTObjectValue *result = objectValue; + for (FSTFieldPath *fieldPath in self.fieldMask.fields) { + FSTFieldValue *newValue = [self.value valueForPath:fieldPath]; + if (newValue) { + result = [result objectBySettingValue:newValue forPath:fieldPath]; + } else { + result = [result objectByDeletingPath:fieldPath]; + } + } + return result; +} + +@end + +@implementation FSTTransformMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key + fieldTransforms:(NSArray *)fieldTransforms { + // NOTE: We set a precondition of exists: true as a safety-check, since we always combine + // FSTTransformMutations with a FSTSetMutation or FSTPatchMutation which (if successful) should + // end up with an existing document. + if (self = [super initWithKey:key precondition:[FSTPrecondition preconditionWithExists:YES]]) { + _fieldTransforms = fieldTransforms; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTTransformMutation class]]) { + return NO; + } + + FSTTransformMutation *otherMutation = (FSTTransformMutation *)other; + return [self.key isEqual:otherMutation.key] && + [self.fieldTransforms isEqual:otherMutation.fieldTransforms] && + [self.precondition isEqual:otherMutation.precondition]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = 31 * result + [self.precondition hash]; + result = 31 * result + [self.fieldTransforms hash]; + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", + self.key, self.fieldTransforms, self.precondition]; +} + +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(nullable FSTMutationResult *)mutationResult { + if (mutationResult) { + FSTAssert(mutationResult.transformResults, + @"Transform results missing for FSTTransformMutation."); + } + + if (![self.precondition isValidForDocument:maybeDoc]) { + return maybeDoc; + } + + // We only support transforms with precondition exists, so we can only apply it to an existing + // document + FSTAssert([maybeDoc isMemberOfClass:[FSTDocument class]], @"Unknown MaybeDocument type %@", + [maybeDoc class]); + FSTDocument *doc = (FSTDocument *)maybeDoc; + + FSTAssert([doc.key isEqual:self.key], @"Can only patch a document with the same key"); + + BOOL hasLocalMutations = (mutationResult == nil); + NSArray *transformResults = + mutationResult + ? mutationResult.transformResults + : [self localTransformResultsWithBaseDocument:baseDoc writeTime:localWriteTime]; + FSTObjectValue *newData = [self transformObject:doc.data transformResults:transformResults]; + return [FSTDocument documentWithData:newData + key:doc.key + version:doc.version + hasLocalMutations:hasLocalMutations]; +} + +/** + * Creates an array of "transform results" (a transform result is a field value representing the + * result of applying a transform) for use when applying an FSTTransformMutation locally. + * + * @param baseDocument The document prior to applying this mutation batch. + * @param localWriteTime The local time of the transform mutation (used to generate + * FSTServerTimestampValues). + * @return The transform results array. + */ +- (NSArray *)localTransformResultsWithBaseDocument: + (FSTMaybeDocument *_Nullable)baseDocument + writeTime:(FSTTimestamp *)localWriteTime { + NSMutableArray *transformResults = [NSMutableArray array]; + for (FSTFieldTransform *fieldTransform in self.fieldTransforms) { + if ([fieldTransform.transform isKindOfClass:[FSTServerTimestampTransform class]]) { + FSTFieldValue *previousValue = nil; + + if ([baseDocument isMemberOfClass:[FSTDocument class]]) { + previousValue = [((FSTDocument *)baseDocument) fieldForPath:fieldTransform.path]; + } + + [transformResults + addObject:[FSTServerTimestampValue serverTimestampValueWithLocalWriteTime:localWriteTime + previousValue:previousValue]]; + } else { + FSTFail(@"Encountered unknown transform: %@", fieldTransform); + } + } + return transformResults; +} + +- (FSTObjectValue *)transformObject:(FSTObjectValue *)objectValue + transformResults:(NSArray *)transformResults { + FSTAssert(transformResults.count == self.fieldTransforms.count, + @"Transform results length mismatch."); + + for (NSUInteger i = 0; i < self.fieldTransforms.count; i++) { + FSTFieldTransform *fieldTransform = self.fieldTransforms[i]; + id transform = fieldTransform.transform; + FSTFieldPath *fieldPath = fieldTransform.path; + if ([transform isKindOfClass:[FSTServerTimestampTransform class]]) { + objectValue = [objectValue objectBySettingValue:transformResults[i] forPath:fieldPath]; + } else { + FSTFail(@"Encountered unknown transform: %@", transform); + } + } + return objectValue; +} + +@end + +#pragma mark - FSTDeleteMutation + +@implementation FSTDeleteMutation + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTDeleteMutation class]]) { + return NO; + } + + FSTDeleteMutation *otherMutation = (FSTDeleteMutation *)other; + return [self.key isEqual:otherMutation.key] && + [self.precondition isEqual:otherMutation.precondition]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = 31 * result + [self.precondition hash]; + return result; +} + +- (NSString *)description { + return [NSString + stringWithFormat:@"", self.key, self.precondition]; +} + +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(nullable FSTMutationResult *)mutationResult { + if (mutationResult) { + FSTAssert(!mutationResult.transformResults, + @"Transform results received by FSTDeleteMutation."); + } + + if (![self.precondition isValidForDocument:maybeDoc]) { + return maybeDoc; + } + + if (maybeDoc) { + FSTAssert([maybeDoc.key isEqual:self.key], @"Can only delete a document with the same key"); + } + + return [FSTDeletedDocument documentWithKey:self.key version:[FSTSnapshotVersion noVersion]]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTMutationBatch.m b/Firestore/Source/Model/FSTMutationBatch.m deleted file mode 100644 index 01adca7..0000000 --- a/Firestore/Source/Model/FSTMutationBatch.m +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Model/FSTMutationBatch.h" - -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Core/FSTTimestamp.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -const FSTBatchID kFSTBatchIDUnknown = -1; - -@implementation FSTMutationBatch - -- (instancetype)initWithBatchID:(FSTBatchID)batchID - localWriteTime:(FSTTimestamp *)localWriteTime - mutations:(NSArray *)mutations { - self = [super init]; - if (self) { - _batchID = batchID; - _localWriteTime = localWriteTime; - _mutations = mutations; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (self == other) { - return YES; - } else if (![other isKindOfClass:[FSTMutationBatch class]]) { - return NO; - } - - FSTMutationBatch *otherBatch = (FSTMutationBatch *)other; - return self.batchID == otherBatch.batchID && - [self.localWriteTime isEqual:otherBatch.localWriteTime] && - [self.mutations isEqual:otherBatch.mutations]; -} - -- (NSUInteger)hash { - NSUInteger result = (NSUInteger)self.batchID; - result = result * 31 + self.localWriteTime.hash; - result = result * 31 + self.mutations.hash; - return result; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"", - self.batchID, self.localWriteTime, self.mutations]; -} - -- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc - documentKey:(FSTDocumentKey *)documentKey - mutationBatchResult:(FSTMutationBatchResult *_Nullable)mutationBatchResult { - FSTAssert(!maybeDoc || [maybeDoc.key isEqualToKey:documentKey], - @"applyTo: key %@ doesn't match maybeDoc key %@", documentKey, maybeDoc.key); - FSTMaybeDocument *baseDoc = maybeDoc; - if (mutationBatchResult) { - FSTAssert(mutationBatchResult.mutationResults.count == self.mutations.count, - @"Mismatch between mutations length (%lu) and results length (%lu)", - (unsigned long)self.mutations.count, - (unsigned long)mutationBatchResult.mutationResults.count); - } - - for (NSUInteger i = 0; i < self.mutations.count; i++) { - FSTMutation *mutation = self.mutations[i]; - FSTMutationResult *_Nullable mutationResult = mutationBatchResult.mutationResults[i]; - if ([mutation.key isEqualToKey:documentKey]) { - maybeDoc = [mutation applyTo:maybeDoc - baseDocument:baseDoc - localWriteTime:self.localWriteTime - mutationResult:mutationResult]; - } - } - return maybeDoc; -} - -- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc - documentKey:(FSTDocumentKey *)documentKey { - return [self applyTo:maybeDoc documentKey:documentKey mutationBatchResult:nil]; -} - -- (BOOL)isTombstone { - return self.mutations.count == 0; -} - -- (FSTMutationBatch *)toTombstone { - return [[FSTMutationBatch alloc] initWithBatchID:self.batchID - localWriteTime:self.localWriteTime - mutations:@[]]; -} - -// TODO(klimt): This could use NSMutableDictionary instead. -- (FSTDocumentKeySet *)keys { - FSTDocumentKeySet *set = [FSTDocumentKeySet keySet]; - for (FSTMutation *mutation in self.mutations) { - set = [set setByAddingObject:mutation.key]; - } - return set; -} - -@end - -#pragma mark - FSTMutationBatchResult - -@interface FSTMutationBatchResult () -- (instancetype)initWithBatch:(FSTMutationBatch *)batch - commitVersion:(FSTSnapshotVersion *)commitVersion - mutationResults:(NSArray *)mutationResults - streamToken:(nullable NSData *)streamToken - docVersions:(FSTDocumentVersionDictionary *)docVersions NS_DESIGNATED_INITIALIZER; -@end - -@implementation FSTMutationBatchResult - -- (instancetype)initWithBatch:(FSTMutationBatch *)batch - commitVersion:(FSTSnapshotVersion *)commitVersion - mutationResults:(NSArray *)mutationResults - streamToken:(nullable NSData *)streamToken - docVersions:(FSTDocumentVersionDictionary *)docVersions { - if (self = [super init]) { - _batch = batch; - _commitVersion = commitVersion; - _mutationResults = mutationResults; - _streamToken = streamToken; - _docVersions = docVersions; - } - return self; -} - -+ (instancetype)resultWithBatch:(FSTMutationBatch *)batch - commitVersion:(FSTSnapshotVersion *)commitVersion - mutationResults:(NSArray *)mutationResults - streamToken:(nullable NSData *)streamToken { - FSTAssert(batch.mutations.count == mutationResults.count, - @"Mutations sent %lu must equal results received %lu", - (unsigned long)batch.mutations.count, (unsigned long)mutationResults.count); - - FSTDocumentVersionDictionary *docVersions = - [FSTDocumentVersionDictionary documentVersionDictionary]; - NSArray *mutations = batch.mutations; - for (NSUInteger i = 0; i < mutations.count; i++) { - FSTSnapshotVersion *_Nullable version = mutationResults[i].version; - if (!version) { - // deletes don't have a version, so we substitute the commitVersion - // of the entire batch. - version = commitVersion; - } - - docVersions = [docVersions dictionaryBySettingObject:version forKey:mutations[i].key]; - } - - return [[FSTMutationBatchResult alloc] initWithBatch:batch - commitVersion:commitVersion - mutationResults:mutationResults - streamToken:streamToken - docVersions:docVersions]; -} - -@end -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTMutationBatch.mm b/Firestore/Source/Model/FSTMutationBatch.mm new file mode 100644 index 0000000..01adca7 --- /dev/null +++ b/Firestore/Source/Model/FSTMutationBatch.mm @@ -0,0 +1,178 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Model/FSTMutationBatch.h" + +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Core/FSTTimestamp.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +const FSTBatchID kFSTBatchIDUnknown = -1; + +@implementation FSTMutationBatch + +- (instancetype)initWithBatchID:(FSTBatchID)batchID + localWriteTime:(FSTTimestamp *)localWriteTime + mutations:(NSArray *)mutations { + self = [super init]; + if (self) { + _batchID = batchID; + _localWriteTime = localWriteTime; + _mutations = mutations; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } else if (![other isKindOfClass:[FSTMutationBatch class]]) { + return NO; + } + + FSTMutationBatch *otherBatch = (FSTMutationBatch *)other; + return self.batchID == otherBatch.batchID && + [self.localWriteTime isEqual:otherBatch.localWriteTime] && + [self.mutations isEqual:otherBatch.mutations]; +} + +- (NSUInteger)hash { + NSUInteger result = (NSUInteger)self.batchID; + result = result * 31 + self.localWriteTime.hash; + result = result * 31 + self.mutations.hash; + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", + self.batchID, self.localWriteTime, self.mutations]; +} + +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + documentKey:(FSTDocumentKey *)documentKey + mutationBatchResult:(FSTMutationBatchResult *_Nullable)mutationBatchResult { + FSTAssert(!maybeDoc || [maybeDoc.key isEqualToKey:documentKey], + @"applyTo: key %@ doesn't match maybeDoc key %@", documentKey, maybeDoc.key); + FSTMaybeDocument *baseDoc = maybeDoc; + if (mutationBatchResult) { + FSTAssert(mutationBatchResult.mutationResults.count == self.mutations.count, + @"Mismatch between mutations length (%lu) and results length (%lu)", + (unsigned long)self.mutations.count, + (unsigned long)mutationBatchResult.mutationResults.count); + } + + for (NSUInteger i = 0; i < self.mutations.count; i++) { + FSTMutation *mutation = self.mutations[i]; + FSTMutationResult *_Nullable mutationResult = mutationBatchResult.mutationResults[i]; + if ([mutation.key isEqualToKey:documentKey]) { + maybeDoc = [mutation applyTo:maybeDoc + baseDocument:baseDoc + localWriteTime:self.localWriteTime + mutationResult:mutationResult]; + } + } + return maybeDoc; +} + +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + documentKey:(FSTDocumentKey *)documentKey { + return [self applyTo:maybeDoc documentKey:documentKey mutationBatchResult:nil]; +} + +- (BOOL)isTombstone { + return self.mutations.count == 0; +} + +- (FSTMutationBatch *)toTombstone { + return [[FSTMutationBatch alloc] initWithBatchID:self.batchID + localWriteTime:self.localWriteTime + mutations:@[]]; +} + +// TODO(klimt): This could use NSMutableDictionary instead. +- (FSTDocumentKeySet *)keys { + FSTDocumentKeySet *set = [FSTDocumentKeySet keySet]; + for (FSTMutation *mutation in self.mutations) { + set = [set setByAddingObject:mutation.key]; + } + return set; +} + +@end + +#pragma mark - FSTMutationBatchResult + +@interface FSTMutationBatchResult () +- (instancetype)initWithBatch:(FSTMutationBatch *)batch + commitVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray *)mutationResults + streamToken:(nullable NSData *)streamToken + docVersions:(FSTDocumentVersionDictionary *)docVersions NS_DESIGNATED_INITIALIZER; +@end + +@implementation FSTMutationBatchResult + +- (instancetype)initWithBatch:(FSTMutationBatch *)batch + commitVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray *)mutationResults + streamToken:(nullable NSData *)streamToken + docVersions:(FSTDocumentVersionDictionary *)docVersions { + if (self = [super init]) { + _batch = batch; + _commitVersion = commitVersion; + _mutationResults = mutationResults; + _streamToken = streamToken; + _docVersions = docVersions; + } + return self; +} + ++ (instancetype)resultWithBatch:(FSTMutationBatch *)batch + commitVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray *)mutationResults + streamToken:(nullable NSData *)streamToken { + FSTAssert(batch.mutations.count == mutationResults.count, + @"Mutations sent %lu must equal results received %lu", + (unsigned long)batch.mutations.count, (unsigned long)mutationResults.count); + + FSTDocumentVersionDictionary *docVersions = + [FSTDocumentVersionDictionary documentVersionDictionary]; + NSArray *mutations = batch.mutations; + for (NSUInteger i = 0; i < mutations.count; i++) { + FSTSnapshotVersion *_Nullable version = mutationResults[i].version; + if (!version) { + // deletes don't have a version, so we substitute the commitVersion + // of the entire batch. + version = commitVersion; + } + + docVersions = [docVersions dictionaryBySettingObject:version forKey:mutations[i].key]; + } + + return [[FSTMutationBatchResult alloc] initWithBatch:batch + commitVersion:commitVersion + mutationResults:mutationResults + streamToken:streamToken + docVersions:docVersions]; +} + +@end +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTPath.m b/Firestore/Source/Model/FSTPath.m deleted file mode 100644 index 636c322..0000000 --- a/Firestore/Source/Model/FSTPath.m +++ /dev/null @@ -1,356 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Model/FSTPath.h" - -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTClasses.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTPath () -/** An underlying array of which a subset of elements are the segments of the path. */ -@property(strong, nonatomic) NSArray *segments; -/** The index into the segments array of the first segment in this path. */ -@property int offset; -@end - -@implementation FSTPath - -/** - * Designated initializer. - * - * @param segments The underlying array of segments for the path. - * @param offset The starting index in the underlying array for the subarray to use. - * @param length The length of the subarray to use. - */ -- (instancetype)initWithSegments:(NSArray *)segments - offset:(int)offset - length:(int)length { - FSTAssert(offset <= segments.count, @"offset %d out of range %d", offset, (int)segments.count); - FSTAssert(length <= segments.count - offset, @"offset %d out of range %d", offset, - (int)segments.count - offset); - - if (self = [super init]) { - _segments = segments; - _offset = offset; - _length = length; - } - return self; -} - -- (BOOL)isEqual:(id)object { - if (self == object) { - return YES; - } - if (![object isKindOfClass:[FSTPath class]]) { - return NO; - } - FSTPath *path = object; - return [self compare:path] == NSOrderedSame; -} - -- (NSUInteger)hash { - NSUInteger hash = 0; - for (int i = 0; i < self.length; ++i) { - hash += [self segmentAtIndex:i].hash; - } - return hash; -} - -- (NSString *)description { - return [self canonicalString]; -} - -- (id)objectAtIndexedSubscript:(int)index { - return [self segmentAtIndex:index]; -} - -- (NSString *)segmentAtIndex:(int)index { - FSTAssert(index < self.length, @"index %d out of range", index); - return self.segments[self.offset + index]; -} - -- (NSString *)firstSegment { - FSTAssert(!self.isEmpty, @"Cannot call firstSegment on empty path"); - return [self segmentAtIndex:0]; -} - -- (NSString *)lastSegment { - FSTAssert(!self.isEmpty, @"Cannot call lastSegment on empty path"); - return [self segmentAtIndex:self.length - 1]; -} - -- (NSComparisonResult)compare:(FSTPath *)other { - int length = MIN(self.length, other.length); - for (int i = 0; i < length; ++i) { - NSString *left = [self segmentAtIndex:i]; - NSString *right = [other segmentAtIndex:i]; - NSComparisonResult result = [left compare:right]; - if (result != NSOrderedSame) { - return result; - } - } - if (self.length < other.length) { - return NSOrderedAscending; - } - if (self.length > other.length) { - return NSOrderedDescending; - } - return NSOrderedSame; -} - -- (instancetype)pathWithSegments:(NSArray *)segments - offset:(int)offset - length:(int)length { - return [[[self class] alloc] initWithSegments:segments offset:offset length:length]; -} - -- (instancetype)pathByAppendingSegment:(NSString *)segment { - int newLength = self.length + 1; - NSMutableArray *segments = [NSMutableArray arrayWithCapacity:newLength]; - for (int i = 0; i < self.length; ++i) { - [segments addObject:self[i]]; - } - [segments addObject:segment]; - return [self pathWithSegments:segments offset:0 length:newLength]; -} - -- (instancetype)pathByAppendingPath:(FSTPath *)path { - int newLength = self.length + path.length; - NSMutableArray *segments = [NSMutableArray arrayWithCapacity:newLength]; - for (int i = 0; i < self.length; ++i) { - [segments addObject:self[i]]; - } - for (int i = 0; i < path.length; ++i) { - [segments addObject:path[i]]; - } - return [self pathWithSegments:segments offset:0 length:newLength]; -} - -- (BOOL)isEmpty { - return self.length == 0; -} - -- (instancetype)pathByRemovingFirstSegment { - FSTAssert(!self.isEmpty, @"Cannot call pathByRemovingFirstSegment on empty path"); - return [self pathWithSegments:self.segments offset:self.offset + 1 length:self.length - 1]; -} - -- (instancetype)pathByRemovingFirstSegments:(int)count { - FSTAssert(self.length >= count, @"pathByRemovingFirstSegments:%d on path of length %d", count, - self.length); - return - [self pathWithSegments:self.segments offset:self.offset + count length:self.length - count]; -} - -- (instancetype)pathByRemovingLastSegment { - FSTAssert(!self.isEmpty, @"Cannot call pathByRemovingLastSegment on empty path"); - return [self pathWithSegments:self.segments offset:self.offset length:self.length - 1]; -} - -- (BOOL)isPrefixOfPath:(FSTPath *)other { - if (other.length < self.length) { - return NO; - } - for (int i = 0; i < self.length; ++i) { - if (![self[i] isEqual:other[i]]) { - return NO; - } - } - return YES; -} - -/** Returns a standardized string representation of this path. */ -- (NSString *)canonicalString { - @throw FSTAbstractMethodException(); // NOLINT -} -@end - -@implementation FSTFieldPath -+ (instancetype)pathWithSegments:(NSArray *)segments { - return [[FSTFieldPath alloc] initWithSegments:segments offset:0 length:(int)segments.count]; -} - -+ (instancetype)pathWithServerFormat:(NSString *)fieldPath { - NSMutableArray *segments = [NSMutableArray array]; - - // TODO(b/37244157): Once we move to v1beta1, we should make this more strict. Right now, it - // allows non-identifier path components, even if they aren't escaped. Technically, this will - // mangle paths with backticks in them used in v1alpha1, but that's fine. - - const char *source = [fieldPath UTF8String]; - char *segment = (char *)malloc(strlen(source) + 1); - char *segmentEnd = segment; - - // If we're inside '`' backticks, then we should ignore '.' dots. - BOOL inBackticks = NO; - - char c; - do { - // Examine current character. This is legit even on zero-length strings because there's always - // a null terminator. - c = *source++; - switch (c) { - case '\0': // Falls through - case '.': - if (!inBackticks) { - // Segment is complete - *segmentEnd = '\0'; - if (segment == segmentEnd) { - FSTThrowInvalidArgument( - @"Invalid field path (%@). Paths must not be empty, begin with " - @"'.', end with '.', or contain '..'", - fieldPath); - } - - [segments addObject:[NSString stringWithUTF8String:segment]]; - segmentEnd = segment; - } else { - // copy into the current segment - *segmentEnd++ = c; - } - break; - - case '`': - if (inBackticks) { - inBackticks = NO; - } else { - inBackticks = YES; - } - break; - - case '\\': - // advance to escaped character - c = *source++; - // TODO(b/37244157): Make this a user-facing exception once we finalize field escaping. - FSTAssert(c != '\0', @"Trailing escape characters not allowed in %@", fieldPath); - // Fall through - - default: - // copy into the current segment - *segmentEnd++ = c; - break; - } - } while (c); - - FSTAssert(!inBackticks, @"Unterminated ` in path %@", fieldPath); - - free(segment); - return [FSTFieldPath pathWithSegments:segments]; -} - -+ (instancetype)keyFieldPath { - static FSTFieldPath *keyFieldPath; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - keyFieldPath = [FSTFieldPath pathWithSegments:@[ kDocumentKeyPath ]]; - }); - return keyFieldPath; -} - -+ (instancetype)emptyPath { - static FSTFieldPath *emptyPath; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - emptyPath = [FSTFieldPath pathWithSegments:@[]]; - }); - return emptyPath; -} - -/** Return YES if the string could be used as a segment in a field path without escaping. */ -+ (BOOL)isValidIdentifier:(NSString *)segment { - if (segment.length == 0) { - return NO; - } - unichar first = [segment characterAtIndex:0]; - if (first != '_' && (first < 'a' || first > 'z') && (first < 'A' || first > 'Z')) { - return NO; - } - for (int i = 1; i < segment.length; i++) { - unichar c = [segment characterAtIndex:i]; - if (c != '_' && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9')) { - return NO; - } - } - return YES; -} - -- (BOOL)isKeyFieldPath { - return [self isEqual:FSTFieldPath.keyFieldPath]; -} - -- (NSString *)canonicalString { - NSMutableString *result = [NSMutableString string]; - for (int i = 0; i < self.length; i++) { - if (i > 0) { - [result appendString:@"."]; - } - - NSString *escaped = [self[i] stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; - escaped = [escaped stringByReplacingOccurrencesOfString:@"`" withString:@"\\`"]; - if (![FSTFieldPath isValidIdentifier:escaped]) { - escaped = [NSString stringWithFormat:@"`%@`", escaped]; - } - - [result appendString:escaped]; - } - return result; -} - -@end - -@implementation FSTResourcePath -+ (instancetype)pathWithSegments:(NSArray *)segments { - return [[FSTResourcePath alloc] initWithSegments:segments offset:0 length:(int)segments.count]; -} - -+ (instancetype)pathWithString:(NSString *)resourcePath { - // NOTE: The client is ignorant of any path segments containing escape sequences (e.g. __id123__) - // and just passes them through raw (they exist for legacy reasons and should not be used - // frequently). - - if ([resourcePath rangeOfString:@"//"].location != NSNotFound) { - FSTThrowInvalidArgument(@"Invalid path (%@). Paths must not contain // in them.", resourcePath); - } - - NSMutableArray *segments = [[resourcePath componentsSeparatedByString:@"/"] mutableCopy]; - // We may still have an empty segment at the beginning or end if they had a leading or trailing - // slash (which we allow). - [segments removeObject:@""]; - - return [self pathWithSegments:segments]; -} - -- (NSString *)canonicalString { - // NOTE: The client is ignorant of any path segments containing escape sequences (e.g. __id123__) - // and just passes them through raw (they exist for legacy reasons and should not be used - // frequently). - - NSMutableString *result = [NSMutableString string]; - for (int i = 0; i < self.length; i++) { - if (i > 0) { - [result appendString:@"/"]; - } - [result appendString:self[i]]; - } - return result; -} -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTPath.mm b/Firestore/Source/Model/FSTPath.mm new file mode 100644 index 0000000..636c322 --- /dev/null +++ b/Firestore/Source/Model/FSTPath.mm @@ -0,0 +1,356 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Model/FSTPath.h" + +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTClasses.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTPath () +/** An underlying array of which a subset of elements are the segments of the path. */ +@property(strong, nonatomic) NSArray *segments; +/** The index into the segments array of the first segment in this path. */ +@property int offset; +@end + +@implementation FSTPath + +/** + * Designated initializer. + * + * @param segments The underlying array of segments for the path. + * @param offset The starting index in the underlying array for the subarray to use. + * @param length The length of the subarray to use. + */ +- (instancetype)initWithSegments:(NSArray *)segments + offset:(int)offset + length:(int)length { + FSTAssert(offset <= segments.count, @"offset %d out of range %d", offset, (int)segments.count); + FSTAssert(length <= segments.count - offset, @"offset %d out of range %d", offset, + (int)segments.count - offset); + + if (self = [super init]) { + _segments = segments; + _offset = offset; + _length = length; + } + return self; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + if (![object isKindOfClass:[FSTPath class]]) { + return NO; + } + FSTPath *path = object; + return [self compare:path] == NSOrderedSame; +} + +- (NSUInteger)hash { + NSUInteger hash = 0; + for (int i = 0; i < self.length; ++i) { + hash += [self segmentAtIndex:i].hash; + } + return hash; +} + +- (NSString *)description { + return [self canonicalString]; +} + +- (id)objectAtIndexedSubscript:(int)index { + return [self segmentAtIndex:index]; +} + +- (NSString *)segmentAtIndex:(int)index { + FSTAssert(index < self.length, @"index %d out of range", index); + return self.segments[self.offset + index]; +} + +- (NSString *)firstSegment { + FSTAssert(!self.isEmpty, @"Cannot call firstSegment on empty path"); + return [self segmentAtIndex:0]; +} + +- (NSString *)lastSegment { + FSTAssert(!self.isEmpty, @"Cannot call lastSegment on empty path"); + return [self segmentAtIndex:self.length - 1]; +} + +- (NSComparisonResult)compare:(FSTPath *)other { + int length = MIN(self.length, other.length); + for (int i = 0; i < length; ++i) { + NSString *left = [self segmentAtIndex:i]; + NSString *right = [other segmentAtIndex:i]; + NSComparisonResult result = [left compare:right]; + if (result != NSOrderedSame) { + return result; + } + } + if (self.length < other.length) { + return NSOrderedAscending; + } + if (self.length > other.length) { + return NSOrderedDescending; + } + return NSOrderedSame; +} + +- (instancetype)pathWithSegments:(NSArray *)segments + offset:(int)offset + length:(int)length { + return [[[self class] alloc] initWithSegments:segments offset:offset length:length]; +} + +- (instancetype)pathByAppendingSegment:(NSString *)segment { + int newLength = self.length + 1; + NSMutableArray *segments = [NSMutableArray arrayWithCapacity:newLength]; + for (int i = 0; i < self.length; ++i) { + [segments addObject:self[i]]; + } + [segments addObject:segment]; + return [self pathWithSegments:segments offset:0 length:newLength]; +} + +- (instancetype)pathByAppendingPath:(FSTPath *)path { + int newLength = self.length + path.length; + NSMutableArray *segments = [NSMutableArray arrayWithCapacity:newLength]; + for (int i = 0; i < self.length; ++i) { + [segments addObject:self[i]]; + } + for (int i = 0; i < path.length; ++i) { + [segments addObject:path[i]]; + } + return [self pathWithSegments:segments offset:0 length:newLength]; +} + +- (BOOL)isEmpty { + return self.length == 0; +} + +- (instancetype)pathByRemovingFirstSegment { + FSTAssert(!self.isEmpty, @"Cannot call pathByRemovingFirstSegment on empty path"); + return [self pathWithSegments:self.segments offset:self.offset + 1 length:self.length - 1]; +} + +- (instancetype)pathByRemovingFirstSegments:(int)count { + FSTAssert(self.length >= count, @"pathByRemovingFirstSegments:%d on path of length %d", count, + self.length); + return + [self pathWithSegments:self.segments offset:self.offset + count length:self.length - count]; +} + +- (instancetype)pathByRemovingLastSegment { + FSTAssert(!self.isEmpty, @"Cannot call pathByRemovingLastSegment on empty path"); + return [self pathWithSegments:self.segments offset:self.offset length:self.length - 1]; +} + +- (BOOL)isPrefixOfPath:(FSTPath *)other { + if (other.length < self.length) { + return NO; + } + for (int i = 0; i < self.length; ++i) { + if (![self[i] isEqual:other[i]]) { + return NO; + } + } + return YES; +} + +/** Returns a standardized string representation of this path. */ +- (NSString *)canonicalString { + @throw FSTAbstractMethodException(); // NOLINT +} +@end + +@implementation FSTFieldPath ++ (instancetype)pathWithSegments:(NSArray *)segments { + return [[FSTFieldPath alloc] initWithSegments:segments offset:0 length:(int)segments.count]; +} + ++ (instancetype)pathWithServerFormat:(NSString *)fieldPath { + NSMutableArray *segments = [NSMutableArray array]; + + // TODO(b/37244157): Once we move to v1beta1, we should make this more strict. Right now, it + // allows non-identifier path components, even if they aren't escaped. Technically, this will + // mangle paths with backticks in them used in v1alpha1, but that's fine. + + const char *source = [fieldPath UTF8String]; + char *segment = (char *)malloc(strlen(source) + 1); + char *segmentEnd = segment; + + // If we're inside '`' backticks, then we should ignore '.' dots. + BOOL inBackticks = NO; + + char c; + do { + // Examine current character. This is legit even on zero-length strings because there's always + // a null terminator. + c = *source++; + switch (c) { + case '\0': // Falls through + case '.': + if (!inBackticks) { + // Segment is complete + *segmentEnd = '\0'; + if (segment == segmentEnd) { + FSTThrowInvalidArgument( + @"Invalid field path (%@). Paths must not be empty, begin with " + @"'.', end with '.', or contain '..'", + fieldPath); + } + + [segments addObject:[NSString stringWithUTF8String:segment]]; + segmentEnd = segment; + } else { + // copy into the current segment + *segmentEnd++ = c; + } + break; + + case '`': + if (inBackticks) { + inBackticks = NO; + } else { + inBackticks = YES; + } + break; + + case '\\': + // advance to escaped character + c = *source++; + // TODO(b/37244157): Make this a user-facing exception once we finalize field escaping. + FSTAssert(c != '\0', @"Trailing escape characters not allowed in %@", fieldPath); + // Fall through + + default: + // copy into the current segment + *segmentEnd++ = c; + break; + } + } while (c); + + FSTAssert(!inBackticks, @"Unterminated ` in path %@", fieldPath); + + free(segment); + return [FSTFieldPath pathWithSegments:segments]; +} + ++ (instancetype)keyFieldPath { + static FSTFieldPath *keyFieldPath; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + keyFieldPath = [FSTFieldPath pathWithSegments:@[ kDocumentKeyPath ]]; + }); + return keyFieldPath; +} + ++ (instancetype)emptyPath { + static FSTFieldPath *emptyPath; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + emptyPath = [FSTFieldPath pathWithSegments:@[]]; + }); + return emptyPath; +} + +/** Return YES if the string could be used as a segment in a field path without escaping. */ ++ (BOOL)isValidIdentifier:(NSString *)segment { + if (segment.length == 0) { + return NO; + } + unichar first = [segment characterAtIndex:0]; + if (first != '_' && (first < 'a' || first > 'z') && (first < 'A' || first > 'Z')) { + return NO; + } + for (int i = 1; i < segment.length; i++) { + unichar c = [segment characterAtIndex:i]; + if (c != '_' && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9')) { + return NO; + } + } + return YES; +} + +- (BOOL)isKeyFieldPath { + return [self isEqual:FSTFieldPath.keyFieldPath]; +} + +- (NSString *)canonicalString { + NSMutableString *result = [NSMutableString string]; + for (int i = 0; i < self.length; i++) { + if (i > 0) { + [result appendString:@"."]; + } + + NSString *escaped = [self[i] stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; + escaped = [escaped stringByReplacingOccurrencesOfString:@"`" withString:@"\\`"]; + if (![FSTFieldPath isValidIdentifier:escaped]) { + escaped = [NSString stringWithFormat:@"`%@`", escaped]; + } + + [result appendString:escaped]; + } + return result; +} + +@end + +@implementation FSTResourcePath ++ (instancetype)pathWithSegments:(NSArray *)segments { + return [[FSTResourcePath alloc] initWithSegments:segments offset:0 length:(int)segments.count]; +} + ++ (instancetype)pathWithString:(NSString *)resourcePath { + // NOTE: The client is ignorant of any path segments containing escape sequences (e.g. __id123__) + // and just passes them through raw (they exist for legacy reasons and should not be used + // frequently). + + if ([resourcePath rangeOfString:@"//"].location != NSNotFound) { + FSTThrowInvalidArgument(@"Invalid path (%@). Paths must not contain // in them.", resourcePath); + } + + NSMutableArray *segments = [[resourcePath componentsSeparatedByString:@"/"] mutableCopy]; + // We may still have an empty segment at the beginning or end if they had a leading or trailing + // slash (which we allow). + [segments removeObject:@""]; + + return [self pathWithSegments:segments]; +} + +- (NSString *)canonicalString { + // NOTE: The client is ignorant of any path segments containing escape sequences (e.g. __id123__) + // and just passes them through raw (they exist for legacy reasons and should not be used + // frequently). + + NSMutableString *result = [NSMutableString string]; + for (int i = 0; i < self.length; i++) { + if (i > 0) { + [result appendString:@"/"]; + } + [result appendString:self[i]]; + } + return result; +} +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTBufferedWriter.m b/Firestore/Source/Remote/FSTBufferedWriter.m deleted file mode 100644 index 47dbb21..0000000 --- a/Firestore/Source/Remote/FSTBufferedWriter.m +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#import "Firestore/Source/Remote/FSTBufferedWriter.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTBufferedWriter { - GRXWriterState _state; - NSMutableArray *_queue; - - id _writeable; -} - -- (instancetype)init { - if (self = [super init]) { - _state = GRXWriterStateNotStarted; - _queue = [[NSMutableArray alloc] init]; - } - return self; -} - -#pragma mark - GRXWriteable implementation - -/** Push the next value of the sequence to the receiving object. */ -- (void)writeValue:(id)value { - if (_state == GRXWriterStateStarted && _queue.count == 0) { - // Skip the queue. - [_writeable writeValue:value]; - } else { - // Buffer the new value. Note that the value is assumed to be transient and doesn't need to - // be copied. - [_queue addObject:value]; - } -} - -/** - * Signal that the sequence is completed, or that an error ocurred. After this message is sent to - * the receiver, neither it nor writeValue: may be called again. - */ -- (void)writesFinishedWithError:(nullable NSError *)error { - // Unimplemented. If we ever wanted to implement sender-side initiated half close we could do so - // by buffering (or sending) and error. - [self doesNotRecognizeSelector:_cmd]; -} - -#pragma mark GRXWriter implementation -// The GRXWriter implementation defines the send side of the RPC stream. Once the RPC is ready it -// will call startWithWriteable passing a GRXWriteable into which requests can be written but only -// when the GRXWriter is in the started state. - -/** - * Called by GRPCCall when it is ready to accept for the first request. Requests should be written - * to the passed writeable. - * - * GRPCCall will synchronize on the receiver around this call. - */ -- (void)startWithWriteable:(id)writeable { - _state = GRXWriterStateStarted; - _writeable = writeable; -} - -/** - * Called by GRPCCall to implement flow control on the sending side of the stream. After each - * writeValue: on the requestsWriteable, GRPCCall will call setState:GRXWriterStatePaused to apply - * backpressure. Once the stream is ready to accept another message, GRPCCall will call - * setState:GRXWriterStateStarted. - * - * GRPCCall will synchronize on the receiver around this call. - */ -- (void)setState:(GRXWriterState)newState { - // Manual transitions are only allowed from the started or paused states. - if (_state == GRXWriterStateNotStarted || _state == GRXWriterStateFinished) { - return; - } - - switch (newState) { - case GRXWriterStateFinished: - _state = newState; - // Per GRXWriter's contract, setting the state to Finished manually means one doesn't wish the - // writeable to be messaged anymore. - _queue = nil; - _writeable = nil; - return; - case GRXWriterStatePaused: - _state = newState; - return; - case GRXWriterStateStarted: - if (_state == GRXWriterStatePaused) { - _state = newState; - [self writeBufferedMessages]; - } - return; - case GRXWriterStateNotStarted: - return; - } -} - -- (void)finishWithError:(nullable NSError *)error { - [_writeable writesFinishedWithError:error]; - self.state = GRXWriterStateFinished; -} - -- (void)writeBufferedMessages { - while (_state == GRXWriterStateStarted && _queue.count > 0) { - id value = _queue[0]; - [_queue removeObjectAtIndex:0]; - - // In addition to writing the value here GRPC will apply backpressure by pausing the GRXWriter - // wrapping this buffer. That writer must call -pauseMessages which will cause this loop to - // exit. Synchronization is not required since the callback happens within the body of the - // writeValue implementation. - [_writeable writeValue:value]; - } -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTBufferedWriter.mm b/Firestore/Source/Remote/FSTBufferedWriter.mm new file mode 100644 index 0000000..47dbb21 --- /dev/null +++ b/Firestore/Source/Remote/FSTBufferedWriter.mm @@ -0,0 +1,134 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import "Firestore/Source/Remote/FSTBufferedWriter.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTBufferedWriter { + GRXWriterState _state; + NSMutableArray *_queue; + + id _writeable; +} + +- (instancetype)init { + if (self = [super init]) { + _state = GRXWriterStateNotStarted; + _queue = [[NSMutableArray alloc] init]; + } + return self; +} + +#pragma mark - GRXWriteable implementation + +/** Push the next value of the sequence to the receiving object. */ +- (void)writeValue:(id)value { + if (_state == GRXWriterStateStarted && _queue.count == 0) { + // Skip the queue. + [_writeable writeValue:value]; + } else { + // Buffer the new value. Note that the value is assumed to be transient and doesn't need to + // be copied. + [_queue addObject:value]; + } +} + +/** + * Signal that the sequence is completed, or that an error ocurred. After this message is sent to + * the receiver, neither it nor writeValue: may be called again. + */ +- (void)writesFinishedWithError:(nullable NSError *)error { + // Unimplemented. If we ever wanted to implement sender-side initiated half close we could do so + // by buffering (or sending) and error. + [self doesNotRecognizeSelector:_cmd]; +} + +#pragma mark GRXWriter implementation +// The GRXWriter implementation defines the send side of the RPC stream. Once the RPC is ready it +// will call startWithWriteable passing a GRXWriteable into which requests can be written but only +// when the GRXWriter is in the started state. + +/** + * Called by GRPCCall when it is ready to accept for the first request. Requests should be written + * to the passed writeable. + * + * GRPCCall will synchronize on the receiver around this call. + */ +- (void)startWithWriteable:(id)writeable { + _state = GRXWriterStateStarted; + _writeable = writeable; +} + +/** + * Called by GRPCCall to implement flow control on the sending side of the stream. After each + * writeValue: on the requestsWriteable, GRPCCall will call setState:GRXWriterStatePaused to apply + * backpressure. Once the stream is ready to accept another message, GRPCCall will call + * setState:GRXWriterStateStarted. + * + * GRPCCall will synchronize on the receiver around this call. + */ +- (void)setState:(GRXWriterState)newState { + // Manual transitions are only allowed from the started or paused states. + if (_state == GRXWriterStateNotStarted || _state == GRXWriterStateFinished) { + return; + } + + switch (newState) { + case GRXWriterStateFinished: + _state = newState; + // Per GRXWriter's contract, setting the state to Finished manually means one doesn't wish the + // writeable to be messaged anymore. + _queue = nil; + _writeable = nil; + return; + case GRXWriterStatePaused: + _state = newState; + return; + case GRXWriterStateStarted: + if (_state == GRXWriterStatePaused) { + _state = newState; + [self writeBufferedMessages]; + } + return; + case GRXWriterStateNotStarted: + return; + } +} + +- (void)finishWithError:(nullable NSError *)error { + [_writeable writesFinishedWithError:error]; + self.state = GRXWriterStateFinished; +} + +- (void)writeBufferedMessages { + while (_state == GRXWriterStateStarted && _queue.count > 0) { + id value = _queue[0]; + [_queue removeObjectAtIndex:0]; + + // In addition to writing the value here GRPC will apply backpressure by pausing the GRXWriter + // wrapping this buffer. That writer must call -pauseMessages which will cause this loop to + // exit. Synchronization is not required since the callback happens within the body of the + // writeValue implementation. + [_writeable writeValue:value]; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTDatastore.m b/Firestore/Source/Remote/FSTDatastore.m deleted file mode 100644 index 02d868c..0000000 --- a/Firestore/Source/Remote/FSTDatastore.m +++ /dev/null @@ -1,336 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Remote/FSTDatastore.h" - -#import -#import - -#import "FIRFirestoreErrors.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRFirestoreVersion.h" -#import "Firestore/Source/Auth/FSTCredentialsProvider.h" -#import "Firestore/Source/Core/FSTDatabaseInfo.h" -#import "Firestore/Source/Local/FSTLocalStore.h" -#import "Firestore/Source/Model/FSTDatabaseID.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Remote/FSTSerializerBeta.h" -#import "Firestore/Source/Remote/FSTStream.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTDispatchQueue.h" -#import "Firestore/Source/Util/FSTLogger.h" - -#import "Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h" - -NS_ASSUME_NONNULL_BEGIN - -// GRPC does not publicly declare a means of disabling SSL, which we need for testing. Firestore -// directly exposes an sslEnabled setting so this is required to plumb that through. Note that our -// own tests depend on this working so we'll know if this changes upstream. -@interface GRPCHost -+ (nullable instancetype)hostWithAddress:(NSString *)address; -@property(nonatomic, getter=isSecure) BOOL secure; -@end - -static NSString *const kXGoogAPIClientHeader = @"x-goog-api-client"; -static NSString *const kGoogleCloudResourcePrefix = @"google-cloud-resource-prefix"; - -/** Function typedef used to create RPCs. */ -typedef GRPCProtoCall * (^RPCFactory)(void); - -#pragma mark - FSTDatastore - -@interface FSTDatastore () - -/** The GRPC service for Firestore. */ -@property(nonatomic, strong, readonly) GCFSFirestore *service; - -@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue; - -/** An object for getting an auth token before each request. */ -@property(nonatomic, strong, readonly) id credentials; - -@property(nonatomic, strong, readonly) FSTSerializerBeta *serializer; - -@end - -@implementation FSTDatastore - -+ (instancetype)datastoreWithDatabase:(FSTDatabaseInfo *)databaseInfo - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue - credentials:(id)credentials { - return [[FSTDatastore alloc] initWithDatabaseInfo:databaseInfo - workerDispatchQueue:workerDispatchQueue - credentials:credentials]; -} - -- (instancetype)initWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue - credentials:(id)credentials { - if (self = [super init]) { - _databaseInfo = databaseInfo; - if (!databaseInfo.isSSLEnabled) { - GRPCHost *hostConfig = [GRPCHost hostWithAddress:databaseInfo.host]; - hostConfig.secure = NO; - } - _service = [GCFSFirestore serviceWithHost:databaseInfo.host]; - _workerDispatchQueue = workerDispatchQueue; - _credentials = credentials; - _serializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseInfo.databaseID]; - } - return self; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"", self.databaseInfo]; -} - -/** - * Converts the error to an error within the domain FIRFirestoreErrorDomain. - */ -+ (NSError *)firestoreErrorForError:(NSError *)error { - if (!error) { - return error; - } else if ([error.domain isEqualToString:FIRFirestoreErrorDomain]) { - return error; - } else if ([error.domain isEqualToString:kGRPCErrorDomain]) { - FSTAssert(error.code >= GRPCErrorCodeCancelled && error.code <= GRPCErrorCodeUnauthenticated, - @"Unknown GRPC error code: %ld", (long)error.code); - return - [NSError errorWithDomain:FIRFirestoreErrorDomain code:error.code userInfo:error.userInfo]; - } else { - return [NSError errorWithDomain:FIRFirestoreErrorDomain - code:FIRFirestoreErrorCodeUnknown - userInfo:@{NSUnderlyingErrorKey : error}]; - } -} - -+ (BOOL)isAbortedError:(NSError *)error { - FSTAssert([error.domain isEqualToString:FIRFirestoreErrorDomain], - @"isAbortedError: only works with errors emitted by FSTDatastore."); - return error.code == FIRFirestoreErrorCodeAborted; -} - -+ (BOOL)isPermanentWriteError:(NSError *)error { - FSTAssert([error.domain isEqualToString:FIRFirestoreErrorDomain], - @"isPerminanteWriteError: only works with errors emitted by FSTDatastore."); - switch (error.code) { - case FIRFirestoreErrorCodeCancelled: - case FIRFirestoreErrorCodeUnknown: - case FIRFirestoreErrorCodeDeadlineExceeded: - case FIRFirestoreErrorCodeResourceExhausted: - case FIRFirestoreErrorCodeInternal: - case FIRFirestoreErrorCodeUnavailable: - case FIRFirestoreErrorCodeUnauthenticated: - // Unauthenticated means something went wrong with our token and we need - // to retry with new credentials which will happen automatically. - // TODO(b/37325376): Give up after second unauthenticated error. - return NO; - case FIRFirestoreErrorCodeInvalidArgument: - case FIRFirestoreErrorCodeNotFound: - case FIRFirestoreErrorCodeAlreadyExists: - case FIRFirestoreErrorCodePermissionDenied: - case FIRFirestoreErrorCodeFailedPrecondition: - case FIRFirestoreErrorCodeAborted: - // Aborted might be retried in some scenarios, but that is dependant on - // the context and should handled individually by the calling code. - // See https://cloud.google.com/apis/design/errors - case FIRFirestoreErrorCodeOutOfRange: - case FIRFirestoreErrorCodeUnimplemented: - case FIRFirestoreErrorCodeDataLoss: - default: - return YES; - } -} - -/** Returns the string to be used as x-goog-api-client header value. */ -+ (NSString *)googAPIClientHeaderValue { - // TODO(dimond): This should ideally also include the grpc version, however, gRPC defines the - // version as a macro, so it would be hardcoded based on version we have at compile time of - // the Firestore library, rather than the version available at runtime/at compile time by the - // user of the library. - return [NSString stringWithFormat:@"gl-objc/ fire/%s grpc/", FirebaseFirestoreVersionString]; -} - -/** Returns the string to be used as google-cloud-resource-prefix header value. */ -+ (NSString *)googleCloudResourcePrefixForDatabaseID:(FSTDatabaseID *)databaseID { - return [NSString - stringWithFormat:@"projects/%@/databases/%@", databaseID.projectID, databaseID.databaseID]; -} -/** - * Takes a dictionary of (HTTP) response headers and returns the set of whitelisted headers - * (for logging purposes). - */ -+ (NSDictionary *)extractWhiteListedHeaders: - (NSDictionary *)headers { - NSMutableDictionary *whiteListedHeaders = - [NSMutableDictionary dictionary]; - NSArray *whiteList = @[ - @"date", @"x-google-backends", @"x-google-netmon-label", @"x-google-service", - @"x-google-gfe-request-trace" - ]; - [headers - enumerateKeysAndObjectsUsingBlock:^(NSString *headerName, NSString *headerValue, BOOL *stop) { - if ([whiteList containsObject:[headerName lowercaseString]]) { - whiteListedHeaders[headerName] = headerValue; - } - }]; - return whiteListedHeaders; -} - -/** Logs the (whitelisted) headers returned for an GRPCProtoCall RPC. */ -+ (void)logHeadersForRPC:(GRPCProtoCall *)rpc RPCName:(NSString *)rpcName { - if ([FIRFirestore isLoggingEnabled]) { - FSTLog(@"RPC %@ returned headers (whitelisted): %@", rpcName, - [FSTDatastore extractWhiteListedHeaders:rpc.responseHeaders]); - } -} - -- (void)commitMutations:(NSArray *)mutations - completion:(FSTVoidErrorBlock)completion { - GCFSCommitRequest *request = [GCFSCommitRequest message]; - request.database = [self.serializer encodedDatabaseID]; - - NSMutableArray *mutationProtos = [NSMutableArray array]; - for (FSTMutation *mutation in mutations) { - [mutationProtos addObject:[self.serializer encodedMutation:mutation]]; - } - request.writesArray = mutationProtos; - - RPCFactory rpcFactory = ^GRPCProtoCall * { - __block GRPCProtoCall *rpc = [self.service - RPCToCommitWithRequest:request - handler:^(GCFSCommitResponse *response, NSError *_Nullable error) { - error = [FSTDatastore firestoreErrorForError:error]; - [self.workerDispatchQueue dispatchAsync:^{ - FSTLog(@"RPC CommitRequest completed. Error: %@", error); - [FSTDatastore logHeadersForRPC:rpc RPCName:@"CommitRequest"]; - completion(error); - }]; - }]; - return rpc; - }; - - [self invokeRPCWithFactory:rpcFactory errorHandler:completion]; -} - -- (void)lookupDocuments:(NSArray *)keys - completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion { - GCFSBatchGetDocumentsRequest *request = [GCFSBatchGetDocumentsRequest message]; - request.database = [self.serializer encodedDatabaseID]; - for (FSTDocumentKey *key in keys) { - [request.documentsArray addObject:[self.serializer encodedDocumentKey:key]]; - } - - __block FSTMaybeDocumentDictionary *results = - [FSTMaybeDocumentDictionary maybeDocumentDictionary]; - - RPCFactory rpcFactory = ^GRPCProtoCall * { - __block GRPCProtoCall *rpc = [self.service - RPCToBatchGetDocumentsWithRequest:request - eventHandler:^(BOOL done, - GCFSBatchGetDocumentsResponse *_Nullable response, - NSError *_Nullable error) { - error = [FSTDatastore firestoreErrorForError:error]; - [self.workerDispatchQueue dispatchAsync:^{ - if (error) { - FSTLog(@"RPC BatchGetDocuments completed. Error: %@", error); - [FSTDatastore logHeadersForRPC:rpc RPCName:@"BatchGetDocuments"]; - completion(nil, error); - return; - } - - if (!done) { - // Streaming response, accumulate result - FSTMaybeDocument *doc = - [self.serializer decodedMaybeDocumentFromBatch:response]; - results = [results dictionaryBySettingObject:doc forKey:doc.key]; - } else { - // Streaming response is done, call completion - FSTLog(@"RPC BatchGetDocuments completed successfully."); - [FSTDatastore logHeadersForRPC:rpc RPCName:@"BatchGetDocuments"]; - FSTAssert(!response, @"Got response after done."); - NSMutableArray *docs = - [NSMutableArray arrayWithCapacity:keys.count]; - for (FSTDocumentKey *key in keys) { - [docs addObject:results[key]]; - } - completion(docs, nil); - } - }]; - }]; - return rpc; - }; - - [self invokeRPCWithFactory:rpcFactory - errorHandler:^(NSError *_Nonnull error) { - error = [FSTDatastore firestoreErrorForError:error]; - completion(nil, error); - }]; -} - -- (void)invokeRPCWithFactory:(GRPCProtoCall * (^)(void))rpcFactory - errorHandler:(FSTVoidErrorBlock)errorHandler { - // TODO(mikelehen): We should force a refresh if the previous RPC failed due to an expired token, - // but I'm not sure how to detect that right now. http://b/32762461 - [self.credentials - getTokenForcingRefresh:NO - completion:^(FSTGetTokenResult *_Nullable result, NSError *_Nullable error) { - error = [FSTDatastore firestoreErrorForError:error]; - [self.workerDispatchQueue dispatchAsyncAllowingSameQueue:^{ - if (error) { - errorHandler(error); - } else { - GRPCProtoCall *rpc = rpcFactory(); - [FSTDatastore prepareHeadersForRPC:rpc - databaseID:self.databaseInfo.databaseID - token:result.token]; - [rpc start]; - } - }]; - }]; -} - -- (FSTWatchStream *)createWatchStream { - return [[FSTWatchStream alloc] initWithDatabase:_databaseInfo - workerDispatchQueue:_workerDispatchQueue - credentials:_credentials - serializer:_serializer]; -} - -- (FSTWriteStream *)createWriteStream { - return [[FSTWriteStream alloc] initWithDatabase:_databaseInfo - workerDispatchQueue:_workerDispatchQueue - credentials:_credentials - serializer:_serializer]; -} - -/** Adds headers to the RPC including any OAuth access token if provided .*/ -+ (void)prepareHeadersForRPC:(GRPCCall *)rpc - databaseID:(FSTDatabaseID *)databaseID - token:(nullable NSString *)token { - rpc.oauth2AccessToken = token; - rpc.requestHeaders[kXGoogAPIClientHeader] = [FSTDatastore googAPIClientHeaderValue]; - // This header is used to improve routing and project isolation by the backend. - rpc.requestHeaders[kGoogleCloudResourcePrefix] = - [FSTDatastore googleCloudResourcePrefixForDatabaseID:databaseID]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTDatastore.mm b/Firestore/Source/Remote/FSTDatastore.mm new file mode 100644 index 0000000..02d868c --- /dev/null +++ b/Firestore/Source/Remote/FSTDatastore.mm @@ -0,0 +1,336 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Remote/FSTDatastore.h" + +#import +#import + +#import "FIRFirestoreErrors.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRFirestoreVersion.h" +#import "Firestore/Source/Auth/FSTCredentialsProvider.h" +#import "Firestore/Source/Core/FSTDatabaseInfo.h" +#import "Firestore/Source/Local/FSTLocalStore.h" +#import "Firestore/Source/Model/FSTDatabaseID.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Remote/FSTSerializerBeta.h" +#import "Firestore/Source/Remote/FSTStream.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTDispatchQueue.h" +#import "Firestore/Source/Util/FSTLogger.h" + +#import "Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h" + +NS_ASSUME_NONNULL_BEGIN + +// GRPC does not publicly declare a means of disabling SSL, which we need for testing. Firestore +// directly exposes an sslEnabled setting so this is required to plumb that through. Note that our +// own tests depend on this working so we'll know if this changes upstream. +@interface GRPCHost ++ (nullable instancetype)hostWithAddress:(NSString *)address; +@property(nonatomic, getter=isSecure) BOOL secure; +@end + +static NSString *const kXGoogAPIClientHeader = @"x-goog-api-client"; +static NSString *const kGoogleCloudResourcePrefix = @"google-cloud-resource-prefix"; + +/** Function typedef used to create RPCs. */ +typedef GRPCProtoCall * (^RPCFactory)(void); + +#pragma mark - FSTDatastore + +@interface FSTDatastore () + +/** The GRPC service for Firestore. */ +@property(nonatomic, strong, readonly) GCFSFirestore *service; + +@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue; + +/** An object for getting an auth token before each request. */ +@property(nonatomic, strong, readonly) id credentials; + +@property(nonatomic, strong, readonly) FSTSerializerBeta *serializer; + +@end + +@implementation FSTDatastore + ++ (instancetype)datastoreWithDatabase:(FSTDatabaseInfo *)databaseInfo + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials { + return [[FSTDatastore alloc] initWithDatabaseInfo:databaseInfo + workerDispatchQueue:workerDispatchQueue + credentials:credentials]; +} + +- (instancetype)initWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials { + if (self = [super init]) { + _databaseInfo = databaseInfo; + if (!databaseInfo.isSSLEnabled) { + GRPCHost *hostConfig = [GRPCHost hostWithAddress:databaseInfo.host]; + hostConfig.secure = NO; + } + _service = [GCFSFirestore serviceWithHost:databaseInfo.host]; + _workerDispatchQueue = workerDispatchQueue; + _credentials = credentials; + _serializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseInfo.databaseID]; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.databaseInfo]; +} + +/** + * Converts the error to an error within the domain FIRFirestoreErrorDomain. + */ ++ (NSError *)firestoreErrorForError:(NSError *)error { + if (!error) { + return error; + } else if ([error.domain isEqualToString:FIRFirestoreErrorDomain]) { + return error; + } else if ([error.domain isEqualToString:kGRPCErrorDomain]) { + FSTAssert(error.code >= GRPCErrorCodeCancelled && error.code <= GRPCErrorCodeUnauthenticated, + @"Unknown GRPC error code: %ld", (long)error.code); + return + [NSError errorWithDomain:FIRFirestoreErrorDomain code:error.code userInfo:error.userInfo]; + } else { + return [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnknown + userInfo:@{NSUnderlyingErrorKey : error}]; + } +} + ++ (BOOL)isAbortedError:(NSError *)error { + FSTAssert([error.domain isEqualToString:FIRFirestoreErrorDomain], + @"isAbortedError: only works with errors emitted by FSTDatastore."); + return error.code == FIRFirestoreErrorCodeAborted; +} + ++ (BOOL)isPermanentWriteError:(NSError *)error { + FSTAssert([error.domain isEqualToString:FIRFirestoreErrorDomain], + @"isPerminanteWriteError: only works with errors emitted by FSTDatastore."); + switch (error.code) { + case FIRFirestoreErrorCodeCancelled: + case FIRFirestoreErrorCodeUnknown: + case FIRFirestoreErrorCodeDeadlineExceeded: + case FIRFirestoreErrorCodeResourceExhausted: + case FIRFirestoreErrorCodeInternal: + case FIRFirestoreErrorCodeUnavailable: + case FIRFirestoreErrorCodeUnauthenticated: + // Unauthenticated means something went wrong with our token and we need + // to retry with new credentials which will happen automatically. + // TODO(b/37325376): Give up after second unauthenticated error. + return NO; + case FIRFirestoreErrorCodeInvalidArgument: + case FIRFirestoreErrorCodeNotFound: + case FIRFirestoreErrorCodeAlreadyExists: + case FIRFirestoreErrorCodePermissionDenied: + case FIRFirestoreErrorCodeFailedPrecondition: + case FIRFirestoreErrorCodeAborted: + // Aborted might be retried in some scenarios, but that is dependant on + // the context and should handled individually by the calling code. + // See https://cloud.google.com/apis/design/errors + case FIRFirestoreErrorCodeOutOfRange: + case FIRFirestoreErrorCodeUnimplemented: + case FIRFirestoreErrorCodeDataLoss: + default: + return YES; + } +} + +/** Returns the string to be used as x-goog-api-client header value. */ ++ (NSString *)googAPIClientHeaderValue { + // TODO(dimond): This should ideally also include the grpc version, however, gRPC defines the + // version as a macro, so it would be hardcoded based on version we have at compile time of + // the Firestore library, rather than the version available at runtime/at compile time by the + // user of the library. + return [NSString stringWithFormat:@"gl-objc/ fire/%s grpc/", FirebaseFirestoreVersionString]; +} + +/** Returns the string to be used as google-cloud-resource-prefix header value. */ ++ (NSString *)googleCloudResourcePrefixForDatabaseID:(FSTDatabaseID *)databaseID { + return [NSString + stringWithFormat:@"projects/%@/databases/%@", databaseID.projectID, databaseID.databaseID]; +} +/** + * Takes a dictionary of (HTTP) response headers and returns the set of whitelisted headers + * (for logging purposes). + */ ++ (NSDictionary *)extractWhiteListedHeaders: + (NSDictionary *)headers { + NSMutableDictionary *whiteListedHeaders = + [NSMutableDictionary dictionary]; + NSArray *whiteList = @[ + @"date", @"x-google-backends", @"x-google-netmon-label", @"x-google-service", + @"x-google-gfe-request-trace" + ]; + [headers + enumerateKeysAndObjectsUsingBlock:^(NSString *headerName, NSString *headerValue, BOOL *stop) { + if ([whiteList containsObject:[headerName lowercaseString]]) { + whiteListedHeaders[headerName] = headerValue; + } + }]; + return whiteListedHeaders; +} + +/** Logs the (whitelisted) headers returned for an GRPCProtoCall RPC. */ ++ (void)logHeadersForRPC:(GRPCProtoCall *)rpc RPCName:(NSString *)rpcName { + if ([FIRFirestore isLoggingEnabled]) { + FSTLog(@"RPC %@ returned headers (whitelisted): %@", rpcName, + [FSTDatastore extractWhiteListedHeaders:rpc.responseHeaders]); + } +} + +- (void)commitMutations:(NSArray *)mutations + completion:(FSTVoidErrorBlock)completion { + GCFSCommitRequest *request = [GCFSCommitRequest message]; + request.database = [self.serializer encodedDatabaseID]; + + NSMutableArray *mutationProtos = [NSMutableArray array]; + for (FSTMutation *mutation in mutations) { + [mutationProtos addObject:[self.serializer encodedMutation:mutation]]; + } + request.writesArray = mutationProtos; + + RPCFactory rpcFactory = ^GRPCProtoCall * { + __block GRPCProtoCall *rpc = [self.service + RPCToCommitWithRequest:request + handler:^(GCFSCommitResponse *response, NSError *_Nullable error) { + error = [FSTDatastore firestoreErrorForError:error]; + [self.workerDispatchQueue dispatchAsync:^{ + FSTLog(@"RPC CommitRequest completed. Error: %@", error); + [FSTDatastore logHeadersForRPC:rpc RPCName:@"CommitRequest"]; + completion(error); + }]; + }]; + return rpc; + }; + + [self invokeRPCWithFactory:rpcFactory errorHandler:completion]; +} + +- (void)lookupDocuments:(NSArray *)keys + completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion { + GCFSBatchGetDocumentsRequest *request = [GCFSBatchGetDocumentsRequest message]; + request.database = [self.serializer encodedDatabaseID]; + for (FSTDocumentKey *key in keys) { + [request.documentsArray addObject:[self.serializer encodedDocumentKey:key]]; + } + + __block FSTMaybeDocumentDictionary *results = + [FSTMaybeDocumentDictionary maybeDocumentDictionary]; + + RPCFactory rpcFactory = ^GRPCProtoCall * { + __block GRPCProtoCall *rpc = [self.service + RPCToBatchGetDocumentsWithRequest:request + eventHandler:^(BOOL done, + GCFSBatchGetDocumentsResponse *_Nullable response, + NSError *_Nullable error) { + error = [FSTDatastore firestoreErrorForError:error]; + [self.workerDispatchQueue dispatchAsync:^{ + if (error) { + FSTLog(@"RPC BatchGetDocuments completed. Error: %@", error); + [FSTDatastore logHeadersForRPC:rpc RPCName:@"BatchGetDocuments"]; + completion(nil, error); + return; + } + + if (!done) { + // Streaming response, accumulate result + FSTMaybeDocument *doc = + [self.serializer decodedMaybeDocumentFromBatch:response]; + results = [results dictionaryBySettingObject:doc forKey:doc.key]; + } else { + // Streaming response is done, call completion + FSTLog(@"RPC BatchGetDocuments completed successfully."); + [FSTDatastore logHeadersForRPC:rpc RPCName:@"BatchGetDocuments"]; + FSTAssert(!response, @"Got response after done."); + NSMutableArray *docs = + [NSMutableArray arrayWithCapacity:keys.count]; + for (FSTDocumentKey *key in keys) { + [docs addObject:results[key]]; + } + completion(docs, nil); + } + }]; + }]; + return rpc; + }; + + [self invokeRPCWithFactory:rpcFactory + errorHandler:^(NSError *_Nonnull error) { + error = [FSTDatastore firestoreErrorForError:error]; + completion(nil, error); + }]; +} + +- (void)invokeRPCWithFactory:(GRPCProtoCall * (^)(void))rpcFactory + errorHandler:(FSTVoidErrorBlock)errorHandler { + // TODO(mikelehen): We should force a refresh if the previous RPC failed due to an expired token, + // but I'm not sure how to detect that right now. http://b/32762461 + [self.credentials + getTokenForcingRefresh:NO + completion:^(FSTGetTokenResult *_Nullable result, NSError *_Nullable error) { + error = [FSTDatastore firestoreErrorForError:error]; + [self.workerDispatchQueue dispatchAsyncAllowingSameQueue:^{ + if (error) { + errorHandler(error); + } else { + GRPCProtoCall *rpc = rpcFactory(); + [FSTDatastore prepareHeadersForRPC:rpc + databaseID:self.databaseInfo.databaseID + token:result.token]; + [rpc start]; + } + }]; + }]; +} + +- (FSTWatchStream *)createWatchStream { + return [[FSTWatchStream alloc] initWithDatabase:_databaseInfo + workerDispatchQueue:_workerDispatchQueue + credentials:_credentials + serializer:_serializer]; +} + +- (FSTWriteStream *)createWriteStream { + return [[FSTWriteStream alloc] initWithDatabase:_databaseInfo + workerDispatchQueue:_workerDispatchQueue + credentials:_credentials + serializer:_serializer]; +} + +/** Adds headers to the RPC including any OAuth access token if provided .*/ ++ (void)prepareHeadersForRPC:(GRPCCall *)rpc + databaseID:(FSTDatabaseID *)databaseID + token:(nullable NSString *)token { + rpc.oauth2AccessToken = token; + rpc.requestHeaders[kXGoogAPIClientHeader] = [FSTDatastore googAPIClientHeaderValue]; + // This header is used to improve routing and project isolation by the backend. + rpc.requestHeaders[kGoogleCloudResourcePrefix] = + [FSTDatastore googleCloudResourcePrefixForDatabaseID:databaseID]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTExistenceFilter.m b/Firestore/Source/Remote/FSTExistenceFilter.m deleted file mode 100644 index d5ec7b3..0000000 --- a/Firestore/Source/Remote/FSTExistenceFilter.m +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Remote/FSTExistenceFilter.h" - -@interface FSTExistenceFilter () - -- (instancetype)initWithCount:(int32_t)count NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FSTExistenceFilter - -+ (instancetype)filterWithCount:(int32_t)count { - return [[FSTExistenceFilter alloc] initWithCount:count]; -} - -- (instancetype)initWithCount:(int32_t)count { - if (self = [super init]) { - _count = count; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isMemberOfClass:[FSTExistenceFilter class]]) { - return NO; - } - - return _count == ((FSTExistenceFilter *)other).count; -} - -- (NSUInteger)hash { - return _count; -} - -@end diff --git a/Firestore/Source/Remote/FSTExistenceFilter.mm b/Firestore/Source/Remote/FSTExistenceFilter.mm new file mode 100644 index 0000000..d5ec7b3 --- /dev/null +++ b/Firestore/Source/Remote/FSTExistenceFilter.mm @@ -0,0 +1,53 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Remote/FSTExistenceFilter.h" + +@interface FSTExistenceFilter () + +- (instancetype)initWithCount:(int32_t)count NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTExistenceFilter + ++ (instancetype)filterWithCount:(int32_t)count { + return [[FSTExistenceFilter alloc] initWithCount:count]; +} + +- (instancetype)initWithCount:(int32_t)count { + if (self = [super init]) { + _count = count; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTExistenceFilter class]]) { + return NO; + } + + return _count == ((FSTExistenceFilter *)other).count; +} + +- (NSUInteger)hash { + return _count; +} + +@end diff --git a/Firestore/Source/Remote/FSTRemoteEvent.m b/Firestore/Source/Remote/FSTRemoteEvent.m deleted file mode 100644 index a97eb86..0000000 --- a/Firestore/Source/Remote/FSTRemoteEvent.m +++ /dev/null @@ -1,516 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Remote/FSTRemoteEvent.h" - -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Remote/FSTWatchChange.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTClasses.h" -#import "Firestore/Source/Util/FSTLogger.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTTargetMapping - -@interface FSTTargetMapping () - -/** Private mutator method to add a document key to the mapping */ -- (void)addDocumentKey:(FSTDocumentKey *)documentKey; - -/** Private mutator method to remove a document key from the mapping */ -- (void)removeDocumentKey:(FSTDocumentKey *)documentKey; - -@end - -@implementation FSTTargetMapping - -- (void)addDocumentKey:(FSTDocumentKey *)documentKey { - @throw FSTAbstractMethodException(); // NOLINT -} - -- (void)removeDocumentKey:(FSTDocumentKey *)documentKey { - @throw FSTAbstractMethodException(); // NOLINT -} - -@end - -#pragma mark - FSTResetMapping - -@interface FSTResetMapping () -@property(nonatomic, strong) FSTDocumentKeySet *documents; -@end - -@implementation FSTResetMapping - -+ (instancetype)mappingWithDocuments:(NSArray *)documents { - FSTResetMapping *mapping = [[FSTResetMapping alloc] init]; - for (FSTDocument *doc in documents) { - mapping.documents = [mapping.documents setByAddingObject:doc.key]; - } - return mapping; -} - -- (instancetype)init { - self = [super init]; - if (self) { - _documents = [FSTDocumentKeySet keySet]; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isMemberOfClass:[FSTResetMapping class]]) { - return NO; - } - - FSTResetMapping *otherMapping = (FSTResetMapping *)other; - return [self.documents isEqual:otherMapping.documents]; -} - -- (NSUInteger)hash { - return self.documents.hash; -} - -- (void)addDocumentKey:(FSTDocumentKey *)documentKey { - self.documents = [self.documents setByAddingObject:documentKey]; -} - -- (void)removeDocumentKey:(FSTDocumentKey *)documentKey { - self.documents = [self.documents setByRemovingObject:documentKey]; -} - -@end - -#pragma mark - FSTUpdateMapping - -@interface FSTUpdateMapping () -@property(nonatomic, strong) FSTDocumentKeySet *addedDocuments; -@property(nonatomic, strong) FSTDocumentKeySet *removedDocuments; -@end - -@implementation FSTUpdateMapping - -+ (FSTUpdateMapping *)mappingWithAddedDocuments:(NSArray *)added - removedDocuments:(NSArray *)removed { - FSTUpdateMapping *mapping = [[FSTUpdateMapping alloc] init]; - for (FSTDocument *doc in added) { - mapping.addedDocuments = [mapping.addedDocuments setByAddingObject:doc.key]; - } - for (FSTDocument *doc in removed) { - mapping.removedDocuments = [mapping.removedDocuments setByAddingObject:doc.key]; - } - return mapping; -} - -- (instancetype)init { - self = [super init]; - if (self) { - _addedDocuments = [FSTDocumentKeySet keySet]; - _removedDocuments = [FSTDocumentKeySet keySet]; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isMemberOfClass:[FSTUpdateMapping class]]) { - return NO; - } - - FSTUpdateMapping *otherMapping = (FSTUpdateMapping *)other; - return [self.addedDocuments isEqual:otherMapping.addedDocuments] && - [self.removedDocuments isEqual:otherMapping.removedDocuments]; -} - -- (NSUInteger)hash { - return self.addedDocuments.hash * 31 + self.removedDocuments.hash; -} - -- (FSTDocumentKeySet *)applyTo:(FSTDocumentKeySet *)keys { - __block FSTDocumentKeySet *result = keys; - [self.addedDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { - result = [result setByAddingObject:key]; - }]; - [self.removedDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { - result = [result setByRemovingObject:key]; - }]; - return result; -} - -- (void)addDocumentKey:(FSTDocumentKey *)documentKey { - self.addedDocuments = [self.addedDocuments setByAddingObject:documentKey]; - self.removedDocuments = [self.removedDocuments setByRemovingObject:documentKey]; -} - -- (void)removeDocumentKey:(FSTDocumentKey *)documentKey { - self.addedDocuments = [self.addedDocuments setByRemovingObject:documentKey]; - self.removedDocuments = [self.removedDocuments setByAddingObject:documentKey]; -} - -@end - -#pragma mark - FSTTargetChange - -@interface FSTTargetChange () -@property(nonatomic, assign) FSTCurrentStatusUpdate currentStatusUpdate; -@property(nonatomic, strong, nullable) FSTTargetMapping *mapping; -@property(nonatomic, strong) FSTSnapshotVersion *snapshotVersion; -@property(nonatomic, strong) NSData *resumeToken; -@end - -@implementation FSTTargetChange - -- (instancetype)init { - if (self = [super init]) { - _currentStatusUpdate = FSTCurrentStatusUpdateNone; - _resumeToken = [NSData data]; - } - return self; -} - -+ (instancetype)changeWithDocuments:(NSArray *)docs - currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate { - FSTUpdateMapping *mapping = [[FSTUpdateMapping alloc] init]; - for (FSTMaybeDocument *doc in docs) { - if ([doc isKindOfClass:[FSTDeletedDocument class]]) { - mapping.removedDocuments = [mapping.removedDocuments setByAddingObject:doc.key]; - } else { - mapping.addedDocuments = [mapping.addedDocuments setByAddingObject:doc.key]; - } - } - FSTTargetChange *change = [[FSTTargetChange alloc] init]; - change.mapping = mapping; - change.currentStatusUpdate = currentStatusUpdate; - return change; -} - -+ (instancetype)changeWithMapping:(FSTTargetMapping *)mapping - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion - currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate { - FSTTargetChange *change = [[FSTTargetChange alloc] init]; - change.mapping = mapping; - change.snapshotVersion = snapshotVersion; - change.currentStatusUpdate = currentStatusUpdate; - return change; -} - -- (FSTTargetMapping *)mapping { - if (!_mapping) { - // Create an FSTUpdateMapping by default, since resets are always explicit - _mapping = [[FSTUpdateMapping alloc] init]; - } - return _mapping; -} - -/** - * Sets the resume token but only when it has a new value. Empty resumeTokens are - * discarded. - */ -- (void)setResumeToken:(NSData *)resumeToken { - if (resumeToken.length > 0) { - _resumeToken = resumeToken; - } -} - -@end - -#pragma mark - FSTRemoteEvent - -@interface FSTRemoteEvent () { - NSMutableDictionary *_documentUpdates; - NSMutableDictionary *_targetChanges; -} - -- (instancetype) -initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion - targetChanges:(NSMutableDictionary *)targetChanges - documentUpdates: - (NSMutableDictionary *)documentUpdates; - -@property(nonatomic, strong) FSTSnapshotVersion *snapshotVersion; - -@end - -@implementation FSTRemoteEvent - -+ (instancetype) -eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion - targetChanges:(NSMutableDictionary *)targetChanges - documentUpdates: - (NSMutableDictionary *)documentUpdates { - return [[FSTRemoteEvent alloc] initWithSnapshotVersion:snapshotVersion - targetChanges:targetChanges - documentUpdates:documentUpdates]; -} - -- (instancetype) -initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion - targetChanges:(NSMutableDictionary *)targetChanges - documentUpdates: - (NSMutableDictionary *)documentUpdates { - self = [super init]; - if (self) { - _snapshotVersion = snapshotVersion; - _targetChanges = targetChanges; - _documentUpdates = documentUpdates; - } - return self; -} - -/** Adds a document update to this remote event */ -- (void)addDocumentUpdate:(FSTMaybeDocument *)document { - _documentUpdates[document.key] = document; -} - -/** Handles an existence filter mismatch */ -- (void)handleExistenceFilterMismatchForTargetID:(FSTBoxedTargetID *)targetID { - // An existence filter mismatch will reset the query and we need to reset the mapping to contain - // no documents and an empty resume token. - // - // Note: - // * The reset mapping is empty, specifically forcing the consumer of the change to - // forget all keys for this targetID; - // * The resume snapshot for this target must be reset - // * The target must be unacked because unwatching and rewatching introduces a race for - // changes. - // - // TODO(dimond): keep track of reset targets not to raise. - FSTTargetChange *targetChange = - [FSTTargetChange changeWithMapping:[[FSTResetMapping alloc] init] - snapshotVersion:[FSTSnapshotVersion noVersion] - currentStatusUpdate:FSTCurrentStatusUpdateMarkNotCurrent]; - _targetChanges[targetID] = targetChange; -} - -@end - -#pragma mark - FSTWatchChangeAggregator - -@interface FSTWatchChangeAggregator () - -/** The snapshot version for every target change this creates. */ -@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion; - -/** Keeps track of the current target mappings */ -@property(nonatomic, strong, readonly) - NSMutableDictionary *targetChanges; - -/** Keeps track of document to update */ -@property(nonatomic, strong, readonly) - NSMutableDictionary *documentUpdates; - -/** The set of open listens on the client */ -@property(nonatomic, strong, readonly) - NSDictionary *listenTargets; - -/** Whether this aggregator was frozen and can no longer be modified */ -@property(nonatomic, assign) BOOL frozen; - -@end - -@implementation FSTWatchChangeAggregator { - NSMutableDictionary *_existenceFilters; -} - -- (instancetype) -initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion - listenTargets:(NSDictionary *)listenTargets - pendingTargetResponses:(NSDictionary *)pendingTargetResponses { - self = [super init]; - if (self) { - _snapshotVersion = snapshotVersion; - - _frozen = NO; - _targetChanges = [NSMutableDictionary dictionary]; - _listenTargets = listenTargets; - _pendingTargetResponses = [NSMutableDictionary dictionaryWithDictionary:pendingTargetResponses]; - - _existenceFilters = [NSMutableDictionary dictionary]; - _documentUpdates = [NSMutableDictionary dictionary]; - } - return self; -} - -- (FSTTargetChange *)targetChangeForTargetID:(FSTBoxedTargetID *)targetID { - FSTTargetChange *change = self.targetChanges[targetID]; - if (!change) { - change = [[FSTTargetChange alloc] init]; - change.snapshotVersion = self.snapshotVersion; - self.targetChanges[targetID] = change; - } - return change; -} - -- (void)addWatchChanges:(NSArray *)watchChanges { - FSTAssert(!self.frozen, @"Trying to modify frozen FSTWatchChangeAggregator"); - for (FSTWatchChange *watchChange in watchChanges) { - [self addWatchChange:watchChange]; - } -} - -- (void)addWatchChange:(FSTWatchChange *)watchChange { - FSTAssert(!self.frozen, @"Trying to modify frozen FSTWatchChangeAggregator"); - if ([watchChange isKindOfClass:[FSTDocumentWatchChange class]]) { - [self addDocumentChange:(FSTDocumentWatchChange *)watchChange]; - } else if ([watchChange isKindOfClass:[FSTWatchTargetChange class]]) { - [self addTargetChange:(FSTWatchTargetChange *)watchChange]; - } else if ([watchChange isKindOfClass:[FSTExistenceFilterWatchChange class]]) { - [self addExistenceFilterChange:(FSTExistenceFilterWatchChange *)watchChange]; - } else { - FSTFail(@"Unknown watch change: %@", watchChange); - } -} - -- (void)addDocumentChange:(FSTDocumentWatchChange *)docChange { - BOOL relevant = NO; - - for (FSTBoxedTargetID *targetID in docChange.updatedTargetIDs) { - if ([self isActiveTarget:targetID]) { - FSTTargetChange *change = [self targetChangeForTargetID:targetID]; - [change.mapping addDocumentKey:docChange.documentKey]; - relevant = YES; - } - } - - for (FSTBoxedTargetID *targetID in docChange.removedTargetIDs) { - if ([self isActiveTarget:targetID]) { - FSTTargetChange *change = [self targetChangeForTargetID:targetID]; - [change.mapping removeDocumentKey:docChange.documentKey]; - relevant = YES; - } - } - - // Only update the document if there is a new document to replace, this might be just a target - // update instead. - if (docChange.document && relevant) { - self.documentUpdates[docChange.documentKey] = docChange.document; - } -} - -- (void)addTargetChange:(FSTWatchTargetChange *)targetChange { - for (FSTBoxedTargetID *targetID in targetChange.targetIDs) { - FSTTargetChange *change = [self targetChangeForTargetID:targetID]; - switch (targetChange.state) { - case FSTWatchTargetChangeStateNoChange: - if ([self isActiveTarget:targetID]) { - // Creating the change above satisfies the semantics of no-change. - change.resumeToken = targetChange.resumeToken; - } - break; - case FSTWatchTargetChangeStateAdded: - [self recordResponseForTargetID:targetID]; - if (![self.pendingTargetResponses objectForKey:targetID]) { - // We have a freshly added target, so we need to reset any state that we had previously - // This can happen e.g. when remove and add back a target for existence filter - // mismatches. - change.mapping = nil; - change.currentStatusUpdate = FSTCurrentStatusUpdateNone; - [_existenceFilters removeObjectForKey:targetID]; - } - change.resumeToken = targetChange.resumeToken; - break; - case FSTWatchTargetChangeStateRemoved: - // We need to keep track of removed targets to we can post-filter and remove any target - // changes. - [self recordResponseForTargetID:targetID]; - FSTAssert(!targetChange.cause, @"WatchChangeAggregator does not handle errored targets."); - break; - case FSTWatchTargetChangeStateCurrent: - if ([self isActiveTarget:targetID]) { - change.currentStatusUpdate = FSTCurrentStatusUpdateMarkCurrent; - change.resumeToken = targetChange.resumeToken; - } - break; - case FSTWatchTargetChangeStateReset: - if ([self isActiveTarget:targetID]) { - // Overwrite any existing target mapping with a reset mapping. Every subsequent update - // will modify the reset mapping, not an update mapping. - change.mapping = [[FSTResetMapping alloc] init]; - change.resumeToken = targetChange.resumeToken; - } - break; - default: - FSTWarn(@"Unknown target watch change type: %ld", (long)targetChange.state); - } - } -} - -/** - * Records that we got a watch target add/remove by decrementing the number of pending target - * responses that we have. - */ -- (void)recordResponseForTargetID:(FSTBoxedTargetID *)targetID { - NSNumber *count = [self.pendingTargetResponses objectForKey:targetID]; - int newCount = count ? [count intValue] - 1 : -1; - if (newCount == 0) { - [self.pendingTargetResponses removeObjectForKey:targetID]; - } else { - [self.pendingTargetResponses setObject:[NSNumber numberWithInt:newCount] forKey:targetID]; - } -} - -/** - * Returns true if the given targetId is active. Active targets are those for which there are no - * pending requests to add a listen and are in the current list of targets the client cares about. - * - * Clients can repeatedly listen and stop listening to targets, so this check is useful in - * preventing in preventing race conditions for a target where events arrive but the server hasn't - * yet acknowledged the intended change in state. - */ -- (BOOL)isActiveTarget:(FSTBoxedTargetID *)targetID { - return [self.listenTargets objectForKey:targetID] && - ![self.pendingTargetResponses objectForKey:targetID]; -} - -- (void)addExistenceFilterChange:(FSTExistenceFilterWatchChange *)existenceFilterChange { - FSTBoxedTargetID *targetID = @(existenceFilterChange.targetID); - if ([self isActiveTarget:targetID]) { - _existenceFilters[targetID] = existenceFilterChange.filter; - } -} - -- (FSTRemoteEvent *)remoteEvent { - NSMutableDictionary *targetChanges = self.targetChanges; - - NSMutableArray *targetsToRemove = [NSMutableArray array]; - - // Apply any inactive targets. - for (FSTBoxedTargetID *targetID in [targetChanges keyEnumerator]) { - if (![self isActiveTarget:targetID]) { - [targetsToRemove addObject:targetID]; - } - } - - [targetChanges removeObjectsForKeys:targetsToRemove]; - - // Mark this aggregator as frozen so no further modifications are made. - self.frozen = YES; - return [FSTRemoteEvent eventWithSnapshotVersion:self.snapshotVersion - targetChanges:targetChanges - documentUpdates:self.documentUpdates]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTRemoteEvent.mm b/Firestore/Source/Remote/FSTRemoteEvent.mm new file mode 100644 index 0000000..88999e4 --- /dev/null +++ b/Firestore/Source/Remote/FSTRemoteEvent.mm @@ -0,0 +1,528 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Remote/FSTRemoteEvent.h" + +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Remote/FSTWatchChange.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTClasses.h" +#import "Firestore/Source/Util/FSTLogger.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTTargetMapping + +@interface FSTTargetMapping () + +/** Private mutator method to add a document key to the mapping */ +- (void)addDocumentKey:(FSTDocumentKey *)documentKey; + +/** Private mutator method to remove a document key from the mapping */ +- (void)removeDocumentKey:(FSTDocumentKey *)documentKey; + +@end + +@implementation FSTTargetMapping + +- (void)addDocumentKey:(FSTDocumentKey *)documentKey { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (void)removeDocumentKey:(FSTDocumentKey *)documentKey { + @throw FSTAbstractMethodException(); // NOLINT +} + +@end + +#pragma mark - FSTResetMapping + +@interface FSTResetMapping () +@property(nonatomic, strong) FSTDocumentKeySet *documents; +@end + +@implementation FSTResetMapping + ++ (instancetype)mappingWithDocuments:(NSArray *)documents { + FSTResetMapping *mapping = [[FSTResetMapping alloc] init]; + for (FSTDocument *doc in documents) { + mapping.documents = [mapping.documents setByAddingObject:doc.key]; + } + return mapping; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _documents = [FSTDocumentKeySet keySet]; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTResetMapping class]]) { + return NO; + } + + FSTResetMapping *otherMapping = (FSTResetMapping *)other; + return [self.documents isEqual:otherMapping.documents]; +} + +- (NSUInteger)hash { + return self.documents.hash; +} + +- (void)addDocumentKey:(FSTDocumentKey *)documentKey { + self.documents = [self.documents setByAddingObject:documentKey]; +} + +- (void)removeDocumentKey:(FSTDocumentKey *)documentKey { + self.documents = [self.documents setByRemovingObject:documentKey]; +} + +@end + +#pragma mark - FSTUpdateMapping + +@interface FSTUpdateMapping () +@property(nonatomic, strong) FSTDocumentKeySet *addedDocuments; +@property(nonatomic, strong) FSTDocumentKeySet *removedDocuments; +@end + +@implementation FSTUpdateMapping + ++ (FSTUpdateMapping *)mappingWithAddedDocuments:(NSArray *)added + removedDocuments:(NSArray *)removed { + FSTUpdateMapping *mapping = [[FSTUpdateMapping alloc] init]; + for (FSTDocument *doc in added) { + mapping.addedDocuments = [mapping.addedDocuments setByAddingObject:doc.key]; + } + for (FSTDocument *doc in removed) { + mapping.removedDocuments = [mapping.removedDocuments setByAddingObject:doc.key]; + } + return mapping; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _addedDocuments = [FSTDocumentKeySet keySet]; + _removedDocuments = [FSTDocumentKeySet keySet]; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTUpdateMapping class]]) { + return NO; + } + + FSTUpdateMapping *otherMapping = (FSTUpdateMapping *)other; + return [self.addedDocuments isEqual:otherMapping.addedDocuments] && + [self.removedDocuments isEqual:otherMapping.removedDocuments]; +} + +- (NSUInteger)hash { + return self.addedDocuments.hash * 31 + self.removedDocuments.hash; +} + +- (FSTDocumentKeySet *)applyTo:(FSTDocumentKeySet *)keys { + __block FSTDocumentKeySet *result = keys; + [self.addedDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + result = [result setByAddingObject:key]; + }]; + [self.removedDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + result = [result setByRemovingObject:key]; + }]; + return result; +} + +- (void)addDocumentKey:(FSTDocumentKey *)documentKey { + self.addedDocuments = [self.addedDocuments setByAddingObject:documentKey]; + self.removedDocuments = [self.removedDocuments setByRemovingObject:documentKey]; +} + +- (void)removeDocumentKey:(FSTDocumentKey *)documentKey { + self.addedDocuments = [self.addedDocuments setByRemovingObject:documentKey]; + self.removedDocuments = [self.removedDocuments setByAddingObject:documentKey]; +} + +@end + +#pragma mark - FSTTargetChange + +@interface FSTTargetChange () +@property(nonatomic, assign) FSTCurrentStatusUpdate currentStatusUpdate; +@property(nonatomic, strong, nullable) FSTTargetMapping *mapping; +@property(nonatomic, strong) FSTSnapshotVersion *snapshotVersion; +@property(nonatomic, strong) NSData *resumeToken; +@end + +@implementation FSTTargetChange + +- (instancetype)init { + if (self = [super init]) { + _currentStatusUpdate = FSTCurrentStatusUpdateNone; + _resumeToken = [NSData data]; + } + return self; +} + ++ (instancetype)changeWithDocuments:(NSArray *)docs + currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate { + FSTUpdateMapping *mapping = [[FSTUpdateMapping alloc] init]; + for (FSTMaybeDocument *doc in docs) { + if ([doc isKindOfClass:[FSTDeletedDocument class]]) { + mapping.removedDocuments = [mapping.removedDocuments setByAddingObject:doc.key]; + } else { + mapping.addedDocuments = [mapping.addedDocuments setByAddingObject:doc.key]; + } + } + FSTTargetChange *change = [[FSTTargetChange alloc] init]; + change.mapping = mapping; + change.currentStatusUpdate = currentStatusUpdate; + return change; +} + ++ (instancetype)changeWithMapping:(FSTTargetMapping *)mapping + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate { + FSTTargetChange *change = [[FSTTargetChange alloc] init]; + change.mapping = mapping; + change.snapshotVersion = snapshotVersion; + change.currentStatusUpdate = currentStatusUpdate; + return change; +} + +- (FSTTargetMapping *)mapping { + if (!_mapping) { + // Create an FSTUpdateMapping by default, since resets are always explicit + _mapping = [[FSTUpdateMapping alloc] init]; + } + return _mapping; +} + +/** + * Sets the resume token but only when it has a new value. Empty resumeTokens are + * discarded. + */ +- (void)setResumeToken:(NSData *)resumeToken { + if (resumeToken.length > 0) { + _resumeToken = resumeToken; + } +} + +@end + +#pragma mark - FSTRemoteEvent + +@interface FSTRemoteEvent () { + NSMutableDictionary *_documentUpdates; + NSMutableDictionary *_targetChanges; +} + +- (instancetype) +initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + targetChanges:(NSMutableDictionary *)targetChanges + documentUpdates: + (NSMutableDictionary *)documentUpdates; + +@property(nonatomic, strong) FSTSnapshotVersion *snapshotVersion; + +@end + +@implementation FSTRemoteEvent + ++ (instancetype) +eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + targetChanges:(NSMutableDictionary *)targetChanges + documentUpdates: + (NSMutableDictionary *)documentUpdates { + return [[FSTRemoteEvent alloc] initWithSnapshotVersion:snapshotVersion + targetChanges:targetChanges + documentUpdates:documentUpdates]; +} + +- (instancetype) +initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + targetChanges:(NSMutableDictionary *)targetChanges + documentUpdates: + (NSMutableDictionary *)documentUpdates { + self = [super init]; + if (self) { + _snapshotVersion = snapshotVersion; + _targetChanges = targetChanges; + _documentUpdates = documentUpdates; + } + return self; +} + +- (NSDictionary *)targetChanges { + return static_cast *>(_targetChanges); +} + +- (NSDictionary *)documentUpdates { + return static_cast *>(_documentUpdates); +} + +/** Adds a document update to this remote event */ +- (void)addDocumentUpdate:(FSTMaybeDocument *)document { + _documentUpdates[document.key] = document; +} + +/** Handles an existence filter mismatch */ +- (void)handleExistenceFilterMismatchForTargetID:(FSTBoxedTargetID *)targetID { + // An existence filter mismatch will reset the query and we need to reset the mapping to contain + // no documents and an empty resume token. + // + // Note: + // * The reset mapping is empty, specifically forcing the consumer of the change to + // forget all keys for this targetID; + // * The resume snapshot for this target must be reset + // * The target must be unacked because unwatching and rewatching introduces a race for + // changes. + // + // TODO(dimond): keep track of reset targets not to raise. + FSTTargetChange *targetChange = + [FSTTargetChange changeWithMapping:[[FSTResetMapping alloc] init] + snapshotVersion:[FSTSnapshotVersion noVersion] + currentStatusUpdate:FSTCurrentStatusUpdateMarkNotCurrent]; + _targetChanges[targetID] = targetChange; +} + +@end + +#pragma mark - FSTWatchChangeAggregator + +@interface FSTWatchChangeAggregator () + +/** The snapshot version for every target change this creates. */ +@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion; + +/** Keeps track of the current target mappings */ +@property(nonatomic, strong, readonly) + NSMutableDictionary *targetChanges; + +/** Keeps track of document to update */ +@property(nonatomic, strong, readonly) + NSMutableDictionary *documentUpdates; + +/** The set of open listens on the client */ +@property(nonatomic, strong, readonly) + NSDictionary *listenTargets; + +/** Whether this aggregator was frozen and can no longer be modified */ +@property(nonatomic, assign) BOOL frozen; + +@end + +@implementation FSTWatchChangeAggregator { + NSMutableDictionary *_existenceFilters; +} + +- (instancetype) +initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + listenTargets:(NSDictionary *)listenTargets + pendingTargetResponses:(NSDictionary *)pendingTargetResponses { + self = [super init]; + if (self) { + _snapshotVersion = snapshotVersion; + + _frozen = NO; + _targetChanges = [NSMutableDictionary dictionary]; + _listenTargets = listenTargets; + _pendingTargetResponses = [NSMutableDictionary dictionaryWithDictionary:pendingTargetResponses]; + + _existenceFilters = [NSMutableDictionary dictionary]; + _documentUpdates = [NSMutableDictionary dictionary]; + } + return self; +} + +- (NSDictionary *)existenceFilters { + return static_cast *>(_existenceFilters); +} + +- (FSTTargetChange *)targetChangeForTargetID:(FSTBoxedTargetID *)targetID { + FSTTargetChange *change = self.targetChanges[targetID]; + if (!change) { + change = [[FSTTargetChange alloc] init]; + change.snapshotVersion = self.snapshotVersion; + self.targetChanges[targetID] = change; + } + return change; +} + +- (void)addWatchChanges:(NSArray *)watchChanges { + FSTAssert(!self.frozen, @"Trying to modify frozen FSTWatchChangeAggregator"); + for (FSTWatchChange *watchChange in watchChanges) { + [self addWatchChange:watchChange]; + } +} + +- (void)addWatchChange:(FSTWatchChange *)watchChange { + FSTAssert(!self.frozen, @"Trying to modify frozen FSTWatchChangeAggregator"); + if ([watchChange isKindOfClass:[FSTDocumentWatchChange class]]) { + [self addDocumentChange:(FSTDocumentWatchChange *)watchChange]; + } else if ([watchChange isKindOfClass:[FSTWatchTargetChange class]]) { + [self addTargetChange:(FSTWatchTargetChange *)watchChange]; + } else if ([watchChange isKindOfClass:[FSTExistenceFilterWatchChange class]]) { + [self addExistenceFilterChange:(FSTExistenceFilterWatchChange *)watchChange]; + } else { + FSTFail(@"Unknown watch change: %@", watchChange); + } +} + +- (void)addDocumentChange:(FSTDocumentWatchChange *)docChange { + BOOL relevant = NO; + + for (FSTBoxedTargetID *targetID in docChange.updatedTargetIDs) { + if ([self isActiveTarget:targetID]) { + FSTTargetChange *change = [self targetChangeForTargetID:targetID]; + [change.mapping addDocumentKey:docChange.documentKey]; + relevant = YES; + } + } + + for (FSTBoxedTargetID *targetID in docChange.removedTargetIDs) { + if ([self isActiveTarget:targetID]) { + FSTTargetChange *change = [self targetChangeForTargetID:targetID]; + [change.mapping removeDocumentKey:docChange.documentKey]; + relevant = YES; + } + } + + // Only update the document if there is a new document to replace, this might be just a target + // update instead. + if (docChange.document && relevant) { + self.documentUpdates[docChange.documentKey] = docChange.document; + } +} + +- (void)addTargetChange:(FSTWatchTargetChange *)targetChange { + for (FSTBoxedTargetID *targetID in targetChange.targetIDs) { + FSTTargetChange *change = [self targetChangeForTargetID:targetID]; + switch (targetChange.state) { + case FSTWatchTargetChangeStateNoChange: + if ([self isActiveTarget:targetID]) { + // Creating the change above satisfies the semantics of no-change. + change.resumeToken = targetChange.resumeToken; + } + break; + case FSTWatchTargetChangeStateAdded: + [self recordResponseForTargetID:targetID]; + if (![self.pendingTargetResponses objectForKey:targetID]) { + // We have a freshly added target, so we need to reset any state that we had previously + // This can happen e.g. when remove and add back a target for existence filter + // mismatches. + change.mapping = nil; + change.currentStatusUpdate = FSTCurrentStatusUpdateNone; + [_existenceFilters removeObjectForKey:targetID]; + } + change.resumeToken = targetChange.resumeToken; + break; + case FSTWatchTargetChangeStateRemoved: + // We need to keep track of removed targets to we can post-filter and remove any target + // changes. + [self recordResponseForTargetID:targetID]; + FSTAssert(!targetChange.cause, @"WatchChangeAggregator does not handle errored targets."); + break; + case FSTWatchTargetChangeStateCurrent: + if ([self isActiveTarget:targetID]) { + change.currentStatusUpdate = FSTCurrentStatusUpdateMarkCurrent; + change.resumeToken = targetChange.resumeToken; + } + break; + case FSTWatchTargetChangeStateReset: + if ([self isActiveTarget:targetID]) { + // Overwrite any existing target mapping with a reset mapping. Every subsequent update + // will modify the reset mapping, not an update mapping. + change.mapping = [[FSTResetMapping alloc] init]; + change.resumeToken = targetChange.resumeToken; + } + break; + default: + FSTWarn(@"Unknown target watch change type: %ld", (long)targetChange.state); + } + } +} + +/** + * Records that we got a watch target add/remove by decrementing the number of pending target + * responses that we have. + */ +- (void)recordResponseForTargetID:(FSTBoxedTargetID *)targetID { + NSNumber *count = [self.pendingTargetResponses objectForKey:targetID]; + int newCount = count ? [count intValue] - 1 : -1; + if (newCount == 0) { + [self.pendingTargetResponses removeObjectForKey:targetID]; + } else { + [self.pendingTargetResponses setObject:[NSNumber numberWithInt:newCount] forKey:targetID]; + } +} + +/** + * Returns true if the given targetId is active. Active targets are those for which there are no + * pending requests to add a listen and are in the current list of targets the client cares about. + * + * Clients can repeatedly listen and stop listening to targets, so this check is useful in + * preventing in preventing race conditions for a target where events arrive but the server hasn't + * yet acknowledged the intended change in state. + */ +- (BOOL)isActiveTarget:(FSTBoxedTargetID *)targetID { + return [self.listenTargets objectForKey:targetID] && + ![self.pendingTargetResponses objectForKey:targetID]; +} + +- (void)addExistenceFilterChange:(FSTExistenceFilterWatchChange *)existenceFilterChange { + FSTBoxedTargetID *targetID = @(existenceFilterChange.targetID); + if ([self isActiveTarget:targetID]) { + _existenceFilters[targetID] = existenceFilterChange.filter; + } +} + +- (FSTRemoteEvent *)remoteEvent { + NSMutableDictionary *targetChanges = self.targetChanges; + + NSMutableArray *targetsToRemove = [NSMutableArray array]; + + // Apply any inactive targets. + for (FSTBoxedTargetID *targetID in [targetChanges keyEnumerator]) { + if (![self isActiveTarget:targetID]) { + [targetsToRemove addObject:targetID]; + } + } + + [targetChanges removeObjectsForKeys:targetsToRemove]; + + // Mark this aggregator as frozen so no further modifications are made. + self.frozen = YES; + return [FSTRemoteEvent eventWithSnapshotVersion:self.snapshotVersion + targetChanges:targetChanges + documentUpdates:self.documentUpdates]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTRemoteStore.m b/Firestore/Source/Remote/FSTRemoteStore.m deleted file mode 100644 index 1201049..0000000 --- a/Firestore/Source/Remote/FSTRemoteStore.m +++ /dev/null @@ -1,710 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Remote/FSTRemoteStore.h" - -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Core/FSTTransaction.h" -#import "Firestore/Source/Local/FSTLocalStore.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Remote/FSTDatastore.h" -#import "Firestore/Source/Remote/FSTExistenceFilter.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" -#import "Firestore/Source/Remote/FSTStream.h" -#import "Firestore/Source/Remote/FSTWatchChange.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTLogger.h" - -NS_ASSUME_NONNULL_BEGIN - -/** - * The maximum number of pending writes to allow. - * TODO(bjornick): Negotiate this value with the backend. - */ -static const int kMaxPendingWrites = 10; - -/** - * The FSTRemoteStore notifies an onlineStateDelegate with FSTOnlineStateFailed if we fail to - * connect to the backend. This subsequently triggers get() requests to fail or use cached data, - * etc. Unfortunately, our connections have historically been subject to various transient failures. - * So we wait for multiple failures before notifying the onlineStateDelegate. - */ -static const int kOnlineAttemptsBeforeFailure = 2; - -#pragma mark - FSTRemoteStore - -@interface FSTRemoteStore () - -- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore - datastore:(FSTDatastore *)datastore NS_DESIGNATED_INITIALIZER; - -/** - * The local store, used to fill the write pipeline with outbound mutations and resolve existence - * filter mismatches. Immutable after initialization. - */ -@property(nonatomic, strong, readonly) FSTLocalStore *localStore; - -/** The client-side proxy for interacting with the backend. Immutable after initialization. */ -@property(nonatomic, strong, readonly) FSTDatastore *datastore; - -#pragma mark Watch Stream -// The watchStream is null when the network is disabled. The non-null check is performed by -// isNetworkEnabled. -@property(nonatomic, strong, nullable) FSTWatchStream *watchStream; - -/** - * A mapping of watched targets that the client cares about tracking and the - * user has explicitly called a 'listen' for this target. - * - * These targets may or may not have been sent to or acknowledged by the - * server. On re-establishing the listen stream, these targets should be sent - * to the server. The targets removed with unlistens are removed eagerly - * without waiting for confirmation from the listen stream. */ -@property(nonatomic, strong, readonly) - NSMutableDictionary *listenTargets; - -/** - * A mapping of targetId to pending acks needed. - * - * If a targetId is present in this map, then we're waiting for watch to - * acknowledge a removal or addition of the target. If a target is not in this - * mapping, and it's in the listenTargets map, then we consider the target to - * be active. - * - * We increment the count here everytime we issue a request over the stream to - * watch or unwatch. We then decrement the count everytime we get a target - * added or target removed message from the server. Once the count is equal to - * 0 we know that the client and server are in the same state (once this state - * is reached the targetId is removed from the map to free the memory). - */ -@property(nonatomic, strong, readonly) - NSMutableDictionary *pendingTargetResponses; - -@property(nonatomic, strong) NSMutableArray *accumulatedChanges; -@property(nonatomic, assign) FSTBatchID lastBatchSeen; - -/** - * The online state of the watch stream. The state is set to healthy if and only if there are - * messages received by the backend. - */ -@property(nonatomic, assign) FSTOnlineState watchStreamOnlineState; - -/** A count of consecutive failures to open the stream. */ -@property(nonatomic, assign) int watchStreamFailures; - -/** Whether the client should fire offline warning. */ -@property(nonatomic, assign) BOOL shouldWarnOffline; - -#pragma mark Write Stream -// The writeStream is null when the network is disabled. The non-null check is performed by -// isNetworkEnabled. -@property(nonatomic, strong, nullable) FSTWriteStream *writeStream; - -/** - * The approximate time the StreamingWrite stream was opened. Used to estimate if stream was - * closed due to an auth expiration (a recoverable error) or some other more permanent error. - */ -@property(nonatomic, strong, nullable) NSDate *writeStreamOpenTime; - -/** - * A FIFO queue of in-flight writes. This is in-flight from the point of view of the caller of - * writeMutations, not from the point of view from the Datastore itself. In particular, these - * requests may not have been sent to the Datastore server if the write stream is not yet running. - */ -@property(nonatomic, strong, readonly) NSMutableArray *pendingWrites; -@end - -@implementation FSTRemoteStore - -+ (instancetype)remoteStoreWithLocalStore:(FSTLocalStore *)localStore - datastore:(FSTDatastore *)datastore { - return [[FSTRemoteStore alloc] initWithLocalStore:localStore datastore:datastore]; -} - -- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore datastore:(FSTDatastore *)datastore { - if (self = [super init]) { - _localStore = localStore; - _datastore = datastore; - _listenTargets = [NSMutableDictionary dictionary]; - _pendingTargetResponses = [NSMutableDictionary dictionary]; - _accumulatedChanges = [NSMutableArray array]; - - _lastBatchSeen = kFSTBatchIDUnknown; - _watchStreamOnlineState = FSTOnlineStateUnknown; - _shouldWarnOffline = YES; - _pendingWrites = [NSMutableArray array]; - } - return self; -} - -- (void)start { - // For now, all setup is handled by enableNetwork(). We might expand on this in the future. - [self enableNetwork]; -} - -/** - * Updates our OnlineState to the new state, updating local state and notifying the - * onlineStateHandler as appropriate. - */ -- (void)updateOnlineState:(FSTOnlineState)newState { - // Update and broadcast the new state. - if (newState != self.watchStreamOnlineState) { - if (newState == FSTOnlineStateHealthy) { - // We've connected to watch at least once. Don't warn the developer about being offline going - // forward. - self.shouldWarnOffline = NO; - } else if (newState == FSTOnlineStateUnknown) { - // The state is set to unknown when a healthy stream is closed (e.g. due to a token timeout) - // or when we have no active listens and therefore there's no need to start the stream. - // Assuming there is (possibly in the future) an active listen, then we will eventually move - // to state Online or Failed, but we always want to make at least kOnlineAttemptsBeforeFailure - // attempts before failing, so we reset the count here. - self.watchStreamFailures = 0; - } - self.watchStreamOnlineState = newState; - [self.onlineStateDelegate applyChangedOnlineState:newState]; - } -} - -/** - * Updates our FSTOnlineState as appropriate after the watch stream reports a failure. The first - * failure moves us to the 'Unknown' state. We then may allow multiple failures (based on - * kOnlineAttemptsBeforeFailure) before we actually transition to FSTOnlineStateFailed. - */ -- (void)updateOnlineStateAfterFailure { - if (self.watchStreamOnlineState == FSTOnlineStateHealthy) { - [self updateOnlineState:FSTOnlineStateUnknown]; - } else { - self.watchStreamFailures++; - if (self.watchStreamFailures >= kOnlineAttemptsBeforeFailure) { - if (self.shouldWarnOffline) { - FSTWarn(@"Could not reach Firestore backend."); - self.shouldWarnOffline = NO; - } - [self updateOnlineState:FSTOnlineStateFailed]; - } - } -} - -#pragma mark Online/Offline state - -- (BOOL)isNetworkEnabled { - FSTAssert((self.watchStream == nil) == (self.writeStream == nil), - @"WatchStream and WriteStream should both be null or non-null"); - return self.watchStream != nil; -} - -- (void)enableNetwork { - if ([self isNetworkEnabled]) { - return; - } - - // Create new streams (but note they're not started yet). - self.watchStream = [self.datastore createWatchStream]; - self.writeStream = [self.datastore createWriteStream]; - - // Load any saved stream token from persistent storage - self.writeStream.lastStreamToken = [self.localStore lastStreamToken]; - - if ([self shouldStartWatchStream]) { - [self startWatchStream]; - } - - [self fillWritePipeline]; // This may start the writeStream. - - // We move back to the unknown state because we might not want to re-open the stream - [self updateOnlineState:FSTOnlineStateUnknown]; -} - -- (void)disableNetwork { - [self disableNetworkInternal]; - // Set the FSTOnlineState to failed so get()'s return from cache, etc. - [self updateOnlineState:FSTOnlineStateFailed]; -} - -/** Disables the network, setting the FSTOnlineState to the specified targetOnlineState. */ -- (void)disableNetworkInternal { - if ([self isNetworkEnabled]) { - // NOTE: We're guaranteed not to get any further events from these streams (not even a close - // event). - [self.watchStream stop]; - [self.writeStream stop]; - - [self cleanUpWatchStreamState]; - [self cleanUpWriteStreamState]; - - self.writeStream = nil; - self.watchStream = nil; - } -} - -#pragma mark Shutdown - -- (void)shutdown { - FSTLog(@"FSTRemoteStore %p shutting down", (__bridge void *)self); - [self disableNetworkInternal]; - // Set the FSTOnlineState to Unknown (rather than Failed) to avoid potentially triggering - // spurious listener events with cached data, etc. - [self updateOnlineState:FSTOnlineStateUnknown]; -} - -- (void)userDidChange:(FSTUser *)user { - FSTLog(@"FSTRemoteStore %p changing users: %@", (__bridge void *)self, user); - if ([self isNetworkEnabled]) { - // Tear down and re-create our network streams. This will ensure we get a fresh auth token - // for the new user and re-fill the write pipeline with new mutations from the LocalStore - // (since mutations are per-user). - [self disableNetworkInternal]; - [self updateOnlineState:FSTOnlineStateUnknown]; - [self enableNetwork]; - } -} - -#pragma mark Watch Stream - -- (void)startWatchStream { - FSTAssert([self shouldStartWatchStream], - @"startWatchStream: called when shouldStartWatchStream: is false."); - [self.watchStream startWithDelegate:self]; -} - -- (void)listenToTargetWithQueryData:(FSTQueryData *)queryData { - NSNumber *targetKey = @(queryData.targetID); - FSTAssert(!self.listenTargets[targetKey], @"listenToQuery called with duplicate target id: %@", - targetKey); - - self.listenTargets[targetKey] = queryData; - - if ([self shouldStartWatchStream]) { - [self startWatchStream]; - } else if ([self isNetworkEnabled] && [self.watchStream isOpen]) { - [self sendWatchRequestWithQueryData:queryData]; - } -} - -- (void)sendWatchRequestWithQueryData:(FSTQueryData *)queryData { - [self recordPendingRequestForTargetID:@(queryData.targetID)]; - [self.watchStream watchQuery:queryData]; -} - -- (void)stopListeningToTargetID:(FSTTargetID)targetID { - FSTBoxedTargetID *targetKey = @(targetID); - FSTQueryData *queryData = self.listenTargets[targetKey]; - FSTAssert(queryData, @"unlistenToTarget: target not currently watched: %@", targetKey); - - [self.listenTargets removeObjectForKey:targetKey]; - if ([self isNetworkEnabled] && [self.watchStream isOpen]) { - [self sendUnwatchRequestForTargetID:targetKey]; - if ([self.listenTargets count] == 0) { - [self.watchStream markIdle]; - } - } -} - -- (void)sendUnwatchRequestForTargetID:(FSTBoxedTargetID *)targetID { - [self recordPendingRequestForTargetID:targetID]; - [self.watchStream unwatchTargetID:[targetID intValue]]; -} - -- (void)recordPendingRequestForTargetID:(FSTBoxedTargetID *)targetID { - NSNumber *count = [self.pendingTargetResponses objectForKey:targetID]; - count = @([count intValue] + 1); - [self.pendingTargetResponses setObject:count forKey:targetID]; -} - -/** - * Returns YES if the network is enabled, the watch stream has not yet been started and there are - * active watch targets. - */ -- (BOOL)shouldStartWatchStream { - return [self isNetworkEnabled] && ![self.watchStream isStarted] && self.listenTargets.count > 0; -} - -- (void)cleanUpWatchStreamState { - // If the connection is closed then we'll never get a snapshot version for the accumulated - // changes and so we'll never be able to complete the batch. When we start up again the server - // is going to resend these changes anyway, so just toss the accumulated state. - [self.accumulatedChanges removeAllObjects]; - [self.pendingTargetResponses removeAllObjects]; -} - -- (void)watchStreamDidOpen { - // Restore any existing watches. - for (FSTQueryData *queryData in [self.listenTargets objectEnumerator]) { - [self sendWatchRequestWithQueryData:queryData]; - } -} - -- (void)watchStreamDidChange:(FSTWatchChange *)change - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion { - // Mark the connection as healthy because we got a message from the server. - [self updateOnlineState:FSTOnlineStateHealthy]; - - FSTWatchTargetChange *watchTargetChange = - [change isKindOfClass:[FSTWatchTargetChange class]] ? (FSTWatchTargetChange *)change : nil; - - if (watchTargetChange && watchTargetChange.state == FSTWatchTargetChangeStateRemoved && - watchTargetChange.cause) { - // There was an error on a target, don't wait for a consistent snapshot to raise events - [self processTargetErrorForWatchChange:(FSTWatchTargetChange *)change]; - } else { - // Accumulate watch changes but don't process them if there's no snapshotVersion or it's - // older than a previous snapshot we've processed (can happen after we resume a target - // using a resume token). - [self.accumulatedChanges addObject:change]; - FSTAssert(snapshotVersion, @"snapshotVersion must not be nil."); - if ([snapshotVersion isEqual:[FSTSnapshotVersion noVersion]] || - [snapshotVersion compare:[self.localStore lastRemoteSnapshotVersion]] == - NSOrderedAscending) { - return; - } - - // Create a batch, giving it the accumulatedChanges array. - NSArray *changes = self.accumulatedChanges; - self.accumulatedChanges = [NSMutableArray array]; - - [self processBatchedWatchChanges:changes snapshotVersion:snapshotVersion]; - } -} - -- (void)watchStreamWasInterruptedWithError:(nullable NSError *)error { - FSTAssert([self isNetworkEnabled], - @"watchStreamDidClose should only be called when the network is enabled"); - - [self cleanUpWatchStreamState]; - - // If the watch stream closed due to an error, retry the connection if there are any active - // watch targets. - if ([self shouldStartWatchStream]) { - [self updateOnlineStateAfterFailure]; - [self startWatchStream]; - } else { - // We don't need to restart the watch stream because there are no active targets. The online - // state is set to unknown because there is no active attempt at establishing a connection. - [self updateOnlineState:FSTOnlineStateUnknown]; - } -} - -/** - * Takes a batch of changes from the Datastore, repackages them as a RemoteEvent, and passes that - * on to the SyncEngine. - */ -- (void)processBatchedWatchChanges:(NSArray *)changes - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion { - FSTWatchChangeAggregator *aggregator = - [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:snapshotVersion - listenTargets:self.listenTargets - pendingTargetResponses:self.pendingTargetResponses]; - [aggregator addWatchChanges:changes]; - FSTRemoteEvent *remoteEvent = [aggregator remoteEvent]; - [self.pendingTargetResponses removeAllObjects]; - [self.pendingTargetResponses setDictionary:aggregator.pendingTargetResponses]; - - // Handle existence filters and existence filter mismatches - [aggregator.existenceFilters enumerateKeysAndObjectsUsingBlock:^(FSTBoxedTargetID *target, - FSTExistenceFilter *filter, - BOOL *stop) { - FSTTargetID targetID = target.intValue; - - FSTQueryData *queryData = self.listenTargets[target]; - FSTQuery *query = queryData.query; - if (!queryData) { - // A watched target might have been removed already. - return; - - } else if ([query isDocumentQuery]) { - if (filter.count == 0) { - // The existence filter told us the document does not exist. - // We need to deduce that this document does not exist and apply a deleted document to our - // updates. Without applying a deleted document there might be another query that will - // raise this document as part of a snapshot until it is resolved, essentially exposing - // inconsistency between queries - FSTDocumentKey *key = [FSTDocumentKey keyWithPath:query.path]; - FSTDeletedDocument *deletedDoc = - [FSTDeletedDocument documentWithKey:key version:snapshotVersion]; - [remoteEvent addDocumentUpdate:deletedDoc]; - } else { - FSTAssert(filter.count == 1, @"Single document existence filter with count: %" PRId32, - filter.count); - } - - } else { - // Not a document query. - FSTDocumentKeySet *trackedRemote = [self.localStore remoteDocumentKeysForTarget:targetID]; - FSTTargetMapping *mapping = remoteEvent.targetChanges[target].mapping; - if (mapping) { - if ([mapping isKindOfClass:[FSTUpdateMapping class]]) { - FSTUpdateMapping *update = (FSTUpdateMapping *)mapping; - trackedRemote = [update applyTo:trackedRemote]; - } else { - FSTAssert([mapping isKindOfClass:[FSTResetMapping class]], - @"Expected either reset or update mapping but got something else %@", mapping); - trackedRemote = ((FSTResetMapping *)mapping).documents; - } - } - - if (trackedRemote.count != (NSUInteger)filter.count) { - FSTLog(@"Existence filter mismatch, resetting mapping"); - - // Make sure the mismatch is exposed in the remote event - [remoteEvent handleExistenceFilterMismatchForTargetID:target]; - - // Clear the resume token for the query, since we're in a known mismatch state. - queryData = [[FSTQueryData alloc] initWithQuery:query - targetID:targetID - listenSequenceNumber:queryData.sequenceNumber - purpose:queryData.purpose]; - self.listenTargets[target] = queryData; - - // Cause a hard reset by unwatching and rewatching immediately, but deliberately don't - // send a resume token so that we get a full update. - [self sendUnwatchRequestForTargetID:@(targetID)]; - - // Mark the query we send as being on behalf of an existence filter mismatch, but don't - // actually retain that in listenTargets. This ensures that we flag the first re-listen - // this way without impacting future listens of this target (that might happen e.g. on - // reconnect). - FSTQueryData *requestQueryData = - [[FSTQueryData alloc] initWithQuery:query - targetID:targetID - listenSequenceNumber:queryData.sequenceNumber - purpose:FSTQueryPurposeExistenceFilterMismatch]; - [self sendWatchRequestWithQueryData:requestQueryData]; - } - } - }]; - - // Update in-memory resume tokens. FSTLocalStore will update the persistent view of these when - // applying the completed FSTRemoteEvent. - [remoteEvent.targetChanges enumerateKeysAndObjectsUsingBlock:^( - FSTBoxedTargetID *target, FSTTargetChange *change, BOOL *stop) { - NSData *resumeToken = change.resumeToken; - if (resumeToken.length > 0) { - FSTQueryData *queryData = _listenTargets[target]; - // A watched target might have been removed already. - if (queryData) { - _listenTargets[target] = - [queryData queryDataByReplacingSnapshotVersion:change.snapshotVersion - resumeToken:resumeToken]; - } - } - }]; - - // Finally handle remote event - [self.syncEngine applyRemoteEvent:remoteEvent]; -} - -/** Process a target error and passes the error along to SyncEngine. */ -- (void)processTargetErrorForWatchChange:(FSTWatchTargetChange *)change { - FSTAssert(change.cause, @"Handling target error without a cause"); - // Ignore targets that have been removed already. - for (FSTBoxedTargetID *targetID in change.targetIDs) { - if (self.listenTargets[targetID]) { - [self.listenTargets removeObjectForKey:targetID]; - [self.syncEngine rejectListenWithTargetID:targetID error:change.cause]; - } - } -} - -#pragma mark Write Stream - -/** - * Returns YES if the network is enabled, the write stream has not yet been started and there are - * pending writes. - */ -- (BOOL)shouldStartWriteStream { - return [self isNetworkEnabled] && ![self.writeStream isStarted] && self.pendingWrites.count > 0; -} - -- (void)startWriteStream { - FSTAssert([self shouldStartWriteStream], - @"startWriteStream: called when shouldStartWriteStream: is false."); - - [self.writeStream startWithDelegate:self]; -} - -- (void)cleanUpWriteStreamState { - self.lastBatchSeen = kFSTBatchIDUnknown; - FSTLog(@"Stopping write stream with %lu pending writes", - (unsigned long)[self.pendingWrites count]); - [self.pendingWrites removeAllObjects]; -} - -- (void)fillWritePipeline { - if ([self isNetworkEnabled]) { - while ([self canWriteMutations]) { - FSTMutationBatch *batch = [self.localStore nextMutationBatchAfterBatchID:self.lastBatchSeen]; - if (!batch) { - break; - } - [self commitBatch:batch]; - } - - if ([self.pendingWrites count] == 0) { - [self.writeStream markIdle]; - } - } -} - -/** - * Returns YES if the backend can accept additional write requests. - * - * When sending mutations to the write stream (e.g. in -fillWritePipeline), call this method first - * to check if more mutations can be sent. - * - * Currently the only thing that can prevent the backend from accepting write requests is if - * there are too many requests already outstanding. As writes complete the backend will be able - * to accept more. - */ -- (BOOL)canWriteMutations { - return [self isNetworkEnabled] && self.pendingWrites.count < kMaxPendingWrites; -} - -/** Given mutations to commit, actually commits them to the backend. */ -- (void)commitBatch:(FSTMutationBatch *)batch { - FSTAssert([self canWriteMutations], @"commitBatch called when mutations can't be written"); - self.lastBatchSeen = batch.batchID; - - [self.pendingWrites addObject:batch]; - - if ([self shouldStartWriteStream]) { - [self startWriteStream]; - } else if ([self isNetworkEnabled] && self.writeStream.handshakeComplete) { - [self.writeStream writeMutations:batch.mutations]; - } -} - -- (void)writeStreamDidOpen { - self.writeStreamOpenTime = [NSDate date]; - - [self.writeStream writeHandshake]; -} - -/** - * Handles a successful handshake response from the server, which is our cue to send any pending - * writes. - */ -- (void)writeStreamDidCompleteHandshake { - // Record the stream token. - [self.localStore setLastStreamToken:self.writeStream.lastStreamToken]; - - // Drain any pending writes. - // - // Note that at this point pendingWrites contains mutations that have already been accepted by - // fillWritePipeline/commitBatch. If the pipeline is full, canWriteMutations will be NO, despite - // the fact that we actually need to send mutations over. - // - // This also means that this method indirectly respects the limits imposed by canWriteMutations - // since writes can't be added to the pendingWrites array when canWriteMutations is NO. If the - // limits imposed by canWriteMutations actually protect us from DOSing ourselves then those limits - // won't be exceeded here and we'll continue to make progress. - for (FSTMutationBatch *write in self.pendingWrites) { - [self.writeStream writeMutations:write.mutations]; - } -} - -/** Handles a successful StreamingWriteResponse from the server that contains a mutation result. */ -- (void)writeStreamDidReceiveResponseWithVersion:(FSTSnapshotVersion *)commitVersion - mutationResults:(NSArray *)results { - // This is a response to a write containing mutations and should be correlated to the first - // pending write. - NSMutableArray *pendingWrites = self.pendingWrites; - FSTMutationBatch *batch = pendingWrites[0]; - [pendingWrites removeObjectAtIndex:0]; - - FSTMutationBatchResult *batchResult = - [FSTMutationBatchResult resultWithBatch:batch - commitVersion:commitVersion - mutationResults:results - streamToken:self.writeStream.lastStreamToken]; - [self.syncEngine applySuccessfulWriteWithResult:batchResult]; - - // It's possible that with the completion of this mutation another slot has freed up. - [self fillWritePipeline]; -} - -/** - * Handles the closing of the StreamingWrite RPC, either because of an error or because the RPC - * has been terminated by the client or the server. - */ -- (void)writeStreamWasInterruptedWithError:(nullable NSError *)error { - FSTAssert([self isNetworkEnabled], - @"writeStreamDidClose: should only be called when the network is enabled"); - - // If the write stream closed due to an error, invoke the error callbacks if there are pending - // writes. - if (error != nil && self.pendingWrites.count > 0) { - if (self.writeStream.handshakeComplete) { - // This error affects the actual writes. - [self handleWriteError:error]; - } else { - // If there was an error before the handshake finished, it's possible that the server is - // unable to process the stream token we're sending. (Perhaps it's too old?) - [self handleHandshakeError:error]; - } - } - - // The write stream might have been started by refilling the write pipeline for failed writes - if ([self shouldStartWriteStream]) { - [self startWriteStream]; - } -} - -- (void)handleHandshakeError:(NSError *)error { - // Reset the token if it's a permanent error or the error code is ABORTED, signaling the write - // stream is no longer valid. - if ([FSTDatastore isPermanentWriteError:error] || [FSTDatastore isAbortedError:error]) { - NSString *token = [self.writeStream.lastStreamToken base64EncodedStringWithOptions:0]; - FSTLog(@"FSTRemoteStore %p error before completed handshake; resetting stream token %@: %@", - (__bridge void *)self, token, error); - self.writeStream.lastStreamToken = nil; - [self.localStore setLastStreamToken:nil]; - } -} - -- (void)handleWriteError:(NSError *)error { - // Only handle permanent error. If it's transient, just let the retry logic kick in. - if (![FSTDatastore isPermanentWriteError:error]) { - return; - } - - // If this was a permanent error, the request itself was the problem so it's not going to - // succeed if we resend it. - FSTMutationBatch *batch = self.pendingWrites[0]; - [self.pendingWrites removeObjectAtIndex:0]; - - // In this case it's also unlikely that the server itself is melting down--this was just a - // bad request so inhibit backoff on the next restart. - [self.writeStream inhibitBackoff]; - - [self.syncEngine rejectFailedWriteWithBatchID:batch.batchID error:error]; - - // It's possible that with the completion of this mutation another slot has freed up. - [self fillWritePipeline]; -} - -- (FSTTransaction *)transaction { - return [FSTTransaction transactionWithDatastore:self.datastore]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTRemoteStore.mm b/Firestore/Source/Remote/FSTRemoteStore.mm new file mode 100644 index 0000000..123df49 --- /dev/null +++ b/Firestore/Source/Remote/FSTRemoteStore.mm @@ -0,0 +1,712 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Remote/FSTRemoteStore.h" + +#include + +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Core/FSTTransaction.h" +#import "Firestore/Source/Local/FSTLocalStore.h" +#import "Firestore/Source/Local/FSTQueryData.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Model/FSTMutationBatch.h" +#import "Firestore/Source/Remote/FSTDatastore.h" +#import "Firestore/Source/Remote/FSTExistenceFilter.h" +#import "Firestore/Source/Remote/FSTRemoteEvent.h" +#import "Firestore/Source/Remote/FSTStream.h" +#import "Firestore/Source/Remote/FSTWatchChange.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTLogger.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The maximum number of pending writes to allow. + * TODO(bjornick): Negotiate this value with the backend. + */ +static const int kMaxPendingWrites = 10; + +/** + * The FSTRemoteStore notifies an onlineStateDelegate with FSTOnlineStateFailed if we fail to + * connect to the backend. This subsequently triggers get() requests to fail or use cached data, + * etc. Unfortunately, our connections have historically been subject to various transient failures. + * So we wait for multiple failures before notifying the onlineStateDelegate. + */ +static const int kOnlineAttemptsBeforeFailure = 2; + +#pragma mark - FSTRemoteStore + +@interface FSTRemoteStore () + +- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore + datastore:(FSTDatastore *)datastore NS_DESIGNATED_INITIALIZER; + +/** + * The local store, used to fill the write pipeline with outbound mutations and resolve existence + * filter mismatches. Immutable after initialization. + */ +@property(nonatomic, strong, readonly) FSTLocalStore *localStore; + +/** The client-side proxy for interacting with the backend. Immutable after initialization. */ +@property(nonatomic, strong, readonly) FSTDatastore *datastore; + +#pragma mark Watch Stream +// The watchStream is null when the network is disabled. The non-null check is performed by +// isNetworkEnabled. +@property(nonatomic, strong, nullable) FSTWatchStream *watchStream; + +/** + * A mapping of watched targets that the client cares about tracking and the + * user has explicitly called a 'listen' for this target. + * + * These targets may or may not have been sent to or acknowledged by the + * server. On re-establishing the listen stream, these targets should be sent + * to the server. The targets removed with unlistens are removed eagerly + * without waiting for confirmation from the listen stream. */ +@property(nonatomic, strong, readonly) + NSMutableDictionary *listenTargets; + +/** + * A mapping of targetId to pending acks needed. + * + * If a targetId is present in this map, then we're waiting for watch to + * acknowledge a removal or addition of the target. If a target is not in this + * mapping, and it's in the listenTargets map, then we consider the target to + * be active. + * + * We increment the count here everytime we issue a request over the stream to + * watch or unwatch. We then decrement the count everytime we get a target + * added or target removed message from the server. Once the count is equal to + * 0 we know that the client and server are in the same state (once this state + * is reached the targetId is removed from the map to free the memory). + */ +@property(nonatomic, strong, readonly) + NSMutableDictionary *pendingTargetResponses; + +@property(nonatomic, strong) NSMutableArray *accumulatedChanges; +@property(nonatomic, assign) FSTBatchID lastBatchSeen; + +/** + * The online state of the watch stream. The state is set to healthy if and only if there are + * messages received by the backend. + */ +@property(nonatomic, assign) FSTOnlineState watchStreamOnlineState; + +/** A count of consecutive failures to open the stream. */ +@property(nonatomic, assign) int watchStreamFailures; + +/** Whether the client should fire offline warning. */ +@property(nonatomic, assign) BOOL shouldWarnOffline; + +#pragma mark Write Stream +// The writeStream is null when the network is disabled. The non-null check is performed by +// isNetworkEnabled. +@property(nonatomic, strong, nullable) FSTWriteStream *writeStream; + +/** + * The approximate time the StreamingWrite stream was opened. Used to estimate if stream was + * closed due to an auth expiration (a recoverable error) or some other more permanent error. + */ +@property(nonatomic, strong, nullable) NSDate *writeStreamOpenTime; + +/** + * A FIFO queue of in-flight writes. This is in-flight from the point of view of the caller of + * writeMutations, not from the point of view from the Datastore itself. In particular, these + * requests may not have been sent to the Datastore server if the write stream is not yet running. + */ +@property(nonatomic, strong, readonly) NSMutableArray *pendingWrites; +@end + +@implementation FSTRemoteStore + ++ (instancetype)remoteStoreWithLocalStore:(FSTLocalStore *)localStore + datastore:(FSTDatastore *)datastore { + return [[FSTRemoteStore alloc] initWithLocalStore:localStore datastore:datastore]; +} + +- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore datastore:(FSTDatastore *)datastore { + if (self = [super init]) { + _localStore = localStore; + _datastore = datastore; + _listenTargets = [NSMutableDictionary dictionary]; + _pendingTargetResponses = [NSMutableDictionary dictionary]; + _accumulatedChanges = [NSMutableArray array]; + + _lastBatchSeen = kFSTBatchIDUnknown; + _watchStreamOnlineState = FSTOnlineStateUnknown; + _shouldWarnOffline = YES; + _pendingWrites = [NSMutableArray array]; + } + return self; +} + +- (void)start { + // For now, all setup is handled by enableNetwork(). We might expand on this in the future. + [self enableNetwork]; +} + +/** + * Updates our OnlineState to the new state, updating local state and notifying the + * onlineStateHandler as appropriate. + */ +- (void)updateOnlineState:(FSTOnlineState)newState { + // Update and broadcast the new state. + if (newState != self.watchStreamOnlineState) { + if (newState == FSTOnlineStateHealthy) { + // We've connected to watch at least once. Don't warn the developer about being offline going + // forward. + self.shouldWarnOffline = NO; + } else if (newState == FSTOnlineStateUnknown) { + // The state is set to unknown when a healthy stream is closed (e.g. due to a token timeout) + // or when we have no active listens and therefore there's no need to start the stream. + // Assuming there is (possibly in the future) an active listen, then we will eventually move + // to state Online or Failed, but we always want to make at least kOnlineAttemptsBeforeFailure + // attempts before failing, so we reset the count here. + self.watchStreamFailures = 0; + } + self.watchStreamOnlineState = newState; + [self.onlineStateDelegate applyChangedOnlineState:newState]; + } +} + +/** + * Updates our FSTOnlineState as appropriate after the watch stream reports a failure. The first + * failure moves us to the 'Unknown' state. We then may allow multiple failures (based on + * kOnlineAttemptsBeforeFailure) before we actually transition to FSTOnlineStateFailed. + */ +- (void)updateOnlineStateAfterFailure { + if (self.watchStreamOnlineState == FSTOnlineStateHealthy) { + [self updateOnlineState:FSTOnlineStateUnknown]; + } else { + self.watchStreamFailures++; + if (self.watchStreamFailures >= kOnlineAttemptsBeforeFailure) { + if (self.shouldWarnOffline) { + FSTWarn(@"Could not reach Firestore backend."); + self.shouldWarnOffline = NO; + } + [self updateOnlineState:FSTOnlineStateFailed]; + } + } +} + +#pragma mark Online/Offline state + +- (BOOL)isNetworkEnabled { + FSTAssert((self.watchStream == nil) == (self.writeStream == nil), + @"WatchStream and WriteStream should both be null or non-null"); + return self.watchStream != nil; +} + +- (void)enableNetwork { + if ([self isNetworkEnabled]) { + return; + } + + // Create new streams (but note they're not started yet). + self.watchStream = [self.datastore createWatchStream]; + self.writeStream = [self.datastore createWriteStream]; + + // Load any saved stream token from persistent storage + self.writeStream.lastStreamToken = [self.localStore lastStreamToken]; + + if ([self shouldStartWatchStream]) { + [self startWatchStream]; + } + + [self fillWritePipeline]; // This may start the writeStream. + + // We move back to the unknown state because we might not want to re-open the stream + [self updateOnlineState:FSTOnlineStateUnknown]; +} + +- (void)disableNetwork { + [self disableNetworkInternal]; + // Set the FSTOnlineState to failed so get()'s return from cache, etc. + [self updateOnlineState:FSTOnlineStateFailed]; +} + +/** Disables the network, setting the FSTOnlineState to the specified targetOnlineState. */ +- (void)disableNetworkInternal { + if ([self isNetworkEnabled]) { + // NOTE: We're guaranteed not to get any further events from these streams (not even a close + // event). + [self.watchStream stop]; + [self.writeStream stop]; + + [self cleanUpWatchStreamState]; + [self cleanUpWriteStreamState]; + + self.writeStream = nil; + self.watchStream = nil; + } +} + +#pragma mark Shutdown + +- (void)shutdown { + FSTLog(@"FSTRemoteStore %p shutting down", (__bridge void *)self); + [self disableNetworkInternal]; + // Set the FSTOnlineState to Unknown (rather than Failed) to avoid potentially triggering + // spurious listener events with cached data, etc. + [self updateOnlineState:FSTOnlineStateUnknown]; +} + +- (void)userDidChange:(FSTUser *)user { + FSTLog(@"FSTRemoteStore %p changing users: %@", (__bridge void *)self, user); + if ([self isNetworkEnabled]) { + // Tear down and re-create our network streams. This will ensure we get a fresh auth token + // for the new user and re-fill the write pipeline with new mutations from the LocalStore + // (since mutations are per-user). + [self disableNetworkInternal]; + [self updateOnlineState:FSTOnlineStateUnknown]; + [self enableNetwork]; + } +} + +#pragma mark Watch Stream + +- (void)startWatchStream { + FSTAssert([self shouldStartWatchStream], + @"startWatchStream: called when shouldStartWatchStream: is false."); + [self.watchStream startWithDelegate:self]; +} + +- (void)listenToTargetWithQueryData:(FSTQueryData *)queryData { + NSNumber *targetKey = @(queryData.targetID); + FSTAssert(!self.listenTargets[targetKey], @"listenToQuery called with duplicate target id: %@", + targetKey); + + self.listenTargets[targetKey] = queryData; + + if ([self shouldStartWatchStream]) { + [self startWatchStream]; + } else if ([self isNetworkEnabled] && [self.watchStream isOpen]) { + [self sendWatchRequestWithQueryData:queryData]; + } +} + +- (void)sendWatchRequestWithQueryData:(FSTQueryData *)queryData { + [self recordPendingRequestForTargetID:@(queryData.targetID)]; + [self.watchStream watchQuery:queryData]; +} + +- (void)stopListeningToTargetID:(FSTTargetID)targetID { + FSTBoxedTargetID *targetKey = @(targetID); + FSTQueryData *queryData = self.listenTargets[targetKey]; + FSTAssert(queryData, @"unlistenToTarget: target not currently watched: %@", targetKey); + + [self.listenTargets removeObjectForKey:targetKey]; + if ([self isNetworkEnabled] && [self.watchStream isOpen]) { + [self sendUnwatchRequestForTargetID:targetKey]; + if ([self.listenTargets count] == 0) { + [self.watchStream markIdle]; + } + } +} + +- (void)sendUnwatchRequestForTargetID:(FSTBoxedTargetID *)targetID { + [self recordPendingRequestForTargetID:targetID]; + [self.watchStream unwatchTargetID:[targetID intValue]]; +} + +- (void)recordPendingRequestForTargetID:(FSTBoxedTargetID *)targetID { + NSNumber *count = [self.pendingTargetResponses objectForKey:targetID]; + count = @([count intValue] + 1); + [self.pendingTargetResponses setObject:count forKey:targetID]; +} + +/** + * Returns YES if the network is enabled, the watch stream has not yet been started and there are + * active watch targets. + */ +- (BOOL)shouldStartWatchStream { + return [self isNetworkEnabled] && ![self.watchStream isStarted] && self.listenTargets.count > 0; +} + +- (void)cleanUpWatchStreamState { + // If the connection is closed then we'll never get a snapshot version for the accumulated + // changes and so we'll never be able to complete the batch. When we start up again the server + // is going to resend these changes anyway, so just toss the accumulated state. + [self.accumulatedChanges removeAllObjects]; + [self.pendingTargetResponses removeAllObjects]; +} + +- (void)watchStreamDidOpen { + // Restore any existing watches. + for (FSTQueryData *queryData in [self.listenTargets objectEnumerator]) { + [self sendWatchRequestWithQueryData:queryData]; + } +} + +- (void)watchStreamDidChange:(FSTWatchChange *)change + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion { + // Mark the connection as healthy because we got a message from the server. + [self updateOnlineState:FSTOnlineStateHealthy]; + + FSTWatchTargetChange *watchTargetChange = + [change isKindOfClass:[FSTWatchTargetChange class]] ? (FSTWatchTargetChange *)change : nil; + + if (watchTargetChange && watchTargetChange.state == FSTWatchTargetChangeStateRemoved && + watchTargetChange.cause) { + // There was an error on a target, don't wait for a consistent snapshot to raise events + [self processTargetErrorForWatchChange:(FSTWatchTargetChange *)change]; + } else { + // Accumulate watch changes but don't process them if there's no snapshotVersion or it's + // older than a previous snapshot we've processed (can happen after we resume a target + // using a resume token). + [self.accumulatedChanges addObject:change]; + FSTAssert(snapshotVersion, @"snapshotVersion must not be nil."); + if ([snapshotVersion isEqual:[FSTSnapshotVersion noVersion]] || + [snapshotVersion compare:[self.localStore lastRemoteSnapshotVersion]] == + NSOrderedAscending) { + return; + } + + // Create a batch, giving it the accumulatedChanges array. + NSArray *changes = self.accumulatedChanges; + self.accumulatedChanges = [NSMutableArray array]; + + [self processBatchedWatchChanges:changes snapshotVersion:snapshotVersion]; + } +} + +- (void)watchStreamWasInterruptedWithError:(nullable NSError *)error { + FSTAssert([self isNetworkEnabled], + @"watchStreamDidClose should only be called when the network is enabled"); + + [self cleanUpWatchStreamState]; + + // If the watch stream closed due to an error, retry the connection if there are any active + // watch targets. + if ([self shouldStartWatchStream]) { + [self updateOnlineStateAfterFailure]; + [self startWatchStream]; + } else { + // We don't need to restart the watch stream because there are no active targets. The online + // state is set to unknown because there is no active attempt at establishing a connection. + [self updateOnlineState:FSTOnlineStateUnknown]; + } +} + +/** + * Takes a batch of changes from the Datastore, repackages them as a RemoteEvent, and passes that + * on to the SyncEngine. + */ +- (void)processBatchedWatchChanges:(NSArray *)changes + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion { + FSTWatchChangeAggregator *aggregator = + [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:snapshotVersion + listenTargets:self.listenTargets + pendingTargetResponses:self.pendingTargetResponses]; + [aggregator addWatchChanges:changes]; + FSTRemoteEvent *remoteEvent = [aggregator remoteEvent]; + [self.pendingTargetResponses removeAllObjects]; + [self.pendingTargetResponses setDictionary:aggregator.pendingTargetResponses]; + + // Handle existence filters and existence filter mismatches + [aggregator.existenceFilters enumerateKeysAndObjectsUsingBlock:^(FSTBoxedTargetID *target, + FSTExistenceFilter *filter, + BOOL *stop) { + FSTTargetID targetID = target.intValue; + + FSTQueryData *queryData = self.listenTargets[target]; + FSTQuery *query = queryData.query; + if (!queryData) { + // A watched target might have been removed already. + return; + + } else if ([query isDocumentQuery]) { + if (filter.count == 0) { + // The existence filter told us the document does not exist. + // We need to deduce that this document does not exist and apply a deleted document to our + // updates. Without applying a deleted document there might be another query that will + // raise this document as part of a snapshot until it is resolved, essentially exposing + // inconsistency between queries + FSTDocumentKey *key = [FSTDocumentKey keyWithPath:query.path]; + FSTDeletedDocument *deletedDoc = + [FSTDeletedDocument documentWithKey:key version:snapshotVersion]; + [remoteEvent addDocumentUpdate:deletedDoc]; + } else { + FSTAssert(filter.count == 1, @"Single document existence filter with count: %" PRId32, + filter.count); + } + + } else { + // Not a document query. + FSTDocumentKeySet *trackedRemote = [self.localStore remoteDocumentKeysForTarget:targetID]; + FSTTargetMapping *mapping = remoteEvent.targetChanges[target].mapping; + if (mapping) { + if ([mapping isKindOfClass:[FSTUpdateMapping class]]) { + FSTUpdateMapping *update = (FSTUpdateMapping *)mapping; + trackedRemote = [update applyTo:trackedRemote]; + } else { + FSTAssert([mapping isKindOfClass:[FSTResetMapping class]], + @"Expected either reset or update mapping but got something else %@", mapping); + trackedRemote = ((FSTResetMapping *)mapping).documents; + } + } + + if (trackedRemote.count != (NSUInteger)filter.count) { + FSTLog(@"Existence filter mismatch, resetting mapping"); + + // Make sure the mismatch is exposed in the remote event + [remoteEvent handleExistenceFilterMismatchForTargetID:target]; + + // Clear the resume token for the query, since we're in a known mismatch state. + queryData = [[FSTQueryData alloc] initWithQuery:query + targetID:targetID + listenSequenceNumber:queryData.sequenceNumber + purpose:queryData.purpose]; + self.listenTargets[target] = queryData; + + // Cause a hard reset by unwatching and rewatching immediately, but deliberately don't + // send a resume token so that we get a full update. + [self sendUnwatchRequestForTargetID:@(targetID)]; + + // Mark the query we send as being on behalf of an existence filter mismatch, but don't + // actually retain that in listenTargets. This ensures that we flag the first re-listen + // this way without impacting future listens of this target (that might happen e.g. on + // reconnect). + FSTQueryData *requestQueryData = + [[FSTQueryData alloc] initWithQuery:query + targetID:targetID + listenSequenceNumber:queryData.sequenceNumber + purpose:FSTQueryPurposeExistenceFilterMismatch]; + [self sendWatchRequestWithQueryData:requestQueryData]; + } + } + }]; + + // Update in-memory resume tokens. FSTLocalStore will update the persistent view of these when + // applying the completed FSTRemoteEvent. + [remoteEvent.targetChanges enumerateKeysAndObjectsUsingBlock:^( + FSTBoxedTargetID *target, FSTTargetChange *change, BOOL *stop) { + NSData *resumeToken = change.resumeToken; + if (resumeToken.length > 0) { + FSTQueryData *queryData = _listenTargets[target]; + // A watched target might have been removed already. + if (queryData) { + _listenTargets[target] = + [queryData queryDataByReplacingSnapshotVersion:change.snapshotVersion + resumeToken:resumeToken]; + } + } + }]; + + // Finally handle remote event + [self.syncEngine applyRemoteEvent:remoteEvent]; +} + +/** Process a target error and passes the error along to SyncEngine. */ +- (void)processTargetErrorForWatchChange:(FSTWatchTargetChange *)change { + FSTAssert(change.cause, @"Handling target error without a cause"); + // Ignore targets that have been removed already. + for (FSTBoxedTargetID *targetID in change.targetIDs) { + if (self.listenTargets[targetID]) { + [self.listenTargets removeObjectForKey:targetID]; + [self.syncEngine rejectListenWithTargetID:targetID error:change.cause]; + } + } +} + +#pragma mark Write Stream + +/** + * Returns YES if the network is enabled, the write stream has not yet been started and there are + * pending writes. + */ +- (BOOL)shouldStartWriteStream { + return [self isNetworkEnabled] && ![self.writeStream isStarted] && self.pendingWrites.count > 0; +} + +- (void)startWriteStream { + FSTAssert([self shouldStartWriteStream], + @"startWriteStream: called when shouldStartWriteStream: is false."); + + [self.writeStream startWithDelegate:self]; +} + +- (void)cleanUpWriteStreamState { + self.lastBatchSeen = kFSTBatchIDUnknown; + FSTLog(@"Stopping write stream with %lu pending writes", + (unsigned long)[self.pendingWrites count]); + [self.pendingWrites removeAllObjects]; +} + +- (void)fillWritePipeline { + if ([self isNetworkEnabled]) { + while ([self canWriteMutations]) { + FSTMutationBatch *batch = [self.localStore nextMutationBatchAfterBatchID:self.lastBatchSeen]; + if (!batch) { + break; + } + [self commitBatch:batch]; + } + + if ([self.pendingWrites count] == 0) { + [self.writeStream markIdle]; + } + } +} + +/** + * Returns YES if the backend can accept additional write requests. + * + * When sending mutations to the write stream (e.g. in -fillWritePipeline), call this method first + * to check if more mutations can be sent. + * + * Currently the only thing that can prevent the backend from accepting write requests is if + * there are too many requests already outstanding. As writes complete the backend will be able + * to accept more. + */ +- (BOOL)canWriteMutations { + return [self isNetworkEnabled] && self.pendingWrites.count < kMaxPendingWrites; +} + +/** Given mutations to commit, actually commits them to the backend. */ +- (void)commitBatch:(FSTMutationBatch *)batch { + FSTAssert([self canWriteMutations], @"commitBatch called when mutations can't be written"); + self.lastBatchSeen = batch.batchID; + + [self.pendingWrites addObject:batch]; + + if ([self shouldStartWriteStream]) { + [self startWriteStream]; + } else if ([self isNetworkEnabled] && self.writeStream.handshakeComplete) { + [self.writeStream writeMutations:batch.mutations]; + } +} + +- (void)writeStreamDidOpen { + self.writeStreamOpenTime = [NSDate date]; + + [self.writeStream writeHandshake]; +} + +/** + * Handles a successful handshake response from the server, which is our cue to send any pending + * writes. + */ +- (void)writeStreamDidCompleteHandshake { + // Record the stream token. + [self.localStore setLastStreamToken:self.writeStream.lastStreamToken]; + + // Drain any pending writes. + // + // Note that at this point pendingWrites contains mutations that have already been accepted by + // fillWritePipeline/commitBatch. If the pipeline is full, canWriteMutations will be NO, despite + // the fact that we actually need to send mutations over. + // + // This also means that this method indirectly respects the limits imposed by canWriteMutations + // since writes can't be added to the pendingWrites array when canWriteMutations is NO. If the + // limits imposed by canWriteMutations actually protect us from DOSing ourselves then those limits + // won't be exceeded here and we'll continue to make progress. + for (FSTMutationBatch *write in self.pendingWrites) { + [self.writeStream writeMutations:write.mutations]; + } +} + +/** Handles a successful StreamingWriteResponse from the server that contains a mutation result. */ +- (void)writeStreamDidReceiveResponseWithVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray *)results { + // This is a response to a write containing mutations and should be correlated to the first + // pending write. + NSMutableArray *pendingWrites = self.pendingWrites; + FSTMutationBatch *batch = pendingWrites[0]; + [pendingWrites removeObjectAtIndex:0]; + + FSTMutationBatchResult *batchResult = + [FSTMutationBatchResult resultWithBatch:batch + commitVersion:commitVersion + mutationResults:results + streamToken:self.writeStream.lastStreamToken]; + [self.syncEngine applySuccessfulWriteWithResult:batchResult]; + + // It's possible that with the completion of this mutation another slot has freed up. + [self fillWritePipeline]; +} + +/** + * Handles the closing of the StreamingWrite RPC, either because of an error or because the RPC + * has been terminated by the client or the server. + */ +- (void)writeStreamWasInterruptedWithError:(nullable NSError *)error { + FSTAssert([self isNetworkEnabled], + @"writeStreamDidClose: should only be called when the network is enabled"); + + // If the write stream closed due to an error, invoke the error callbacks if there are pending + // writes. + if (error != nil && self.pendingWrites.count > 0) { + if (self.writeStream.handshakeComplete) { + // This error affects the actual writes. + [self handleWriteError:error]; + } else { + // If there was an error before the handshake finished, it's possible that the server is + // unable to process the stream token we're sending. (Perhaps it's too old?) + [self handleHandshakeError:error]; + } + } + + // The write stream might have been started by refilling the write pipeline for failed writes + if ([self shouldStartWriteStream]) { + [self startWriteStream]; + } +} + +- (void)handleHandshakeError:(NSError *)error { + // Reset the token if it's a permanent error or the error code is ABORTED, signaling the write + // stream is no longer valid. + if ([FSTDatastore isPermanentWriteError:error] || [FSTDatastore isAbortedError:error]) { + NSString *token = [self.writeStream.lastStreamToken base64EncodedStringWithOptions:0]; + FSTLog(@"FSTRemoteStore %p error before completed handshake; resetting stream token %@: %@", + (__bridge void *)self, token, error); + self.writeStream.lastStreamToken = nil; + [self.localStore setLastStreamToken:nil]; + } +} + +- (void)handleWriteError:(NSError *)error { + // Only handle permanent error. If it's transient, just let the retry logic kick in. + if (![FSTDatastore isPermanentWriteError:error]) { + return; + } + + // If this was a permanent error, the request itself was the problem so it's not going to + // succeed if we resend it. + FSTMutationBatch *batch = self.pendingWrites[0]; + [self.pendingWrites removeObjectAtIndex:0]; + + // In this case it's also unlikely that the server itself is melting down--this was just a + // bad request so inhibit backoff on the next restart. + [self.writeStream inhibitBackoff]; + + [self.syncEngine rejectFailedWriteWithBatchID:batch.batchID error:error]; + + // It's possible that with the completion of this mutation another slot has freed up. + [self fillWritePipeline]; +} + +- (FSTTransaction *)transaction { + return [FSTTransaction transactionWithDatastore:self.datastore]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTSerializerBeta.m b/Firestore/Source/Remote/FSTSerializerBeta.m deleted file mode 100644 index 04785c2..0000000 --- a/Firestore/Source/Remote/FSTSerializerBeta.m +++ /dev/null @@ -1,1084 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Remote/FSTSerializerBeta.h" - -#import - -#import "Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.h" -#import "Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.h" -#import "Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h" -#import "Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.h" -#import "Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.h" -#import "Firestore/Protos/objc/google/rpc/Status.pbobjc.h" -#import "Firestore/Protos/objc/google/type/Latlng.pbobjc.h" - -#import "FIRFirestoreErrors.h" -#import "FIRGeoPoint.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Core/FSTTimestamp.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTDatabaseID.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Remote/FSTExistenceFilter.h" -#import "Firestore/Source/Remote/FSTWatchChange.h" -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTSerializerBeta () -@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID; -@end - -@implementation FSTSerializerBeta - -- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID { - self = [super init]; - if (self) { - _databaseID = databaseID; - } - return self; -} - -#pragma mark - FSTSnapshotVersion <=> GPBTimestamp - -- (GPBTimestamp *)encodedTimestamp:(FSTTimestamp *)timestamp { - GPBTimestamp *result = [GPBTimestamp message]; - result.seconds = timestamp.seconds; - result.nanos = timestamp.nanos; - return result; -} - -- (FSTTimestamp *)decodedTimestamp:(GPBTimestamp *)timestamp { - return [[FSTTimestamp alloc] initWithSeconds:timestamp.seconds nanos:timestamp.nanos]; -} - -- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version { - return [self encodedTimestamp:version.timestamp]; -} - -- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version { - return [FSTSnapshotVersion versionWithTimestamp:[self decodedTimestamp:version]]; -} - -#pragma mark - FIRGeoPoint <=> GTPLatLng - -- (GTPLatLng *)encodedGeoPoint:(FIRGeoPoint *)geoPoint { - GTPLatLng *latLng = [GTPLatLng message]; - latLng.latitude = geoPoint.latitude; - latLng.longitude = geoPoint.longitude; - return latLng; -} - -- (FIRGeoPoint *)decodedGeoPoint:(GTPLatLng *)latLng { - return [[FIRGeoPoint alloc] initWithLatitude:latLng.latitude longitude:latLng.longitude]; -} - -#pragma mark - FSTDocumentKey <=> Key proto - -- (NSString *)encodedDocumentKey:(FSTDocumentKey *)key { - return [self encodedResourcePathForDatabaseID:self.databaseID path:key.path]; -} - -- (FSTDocumentKey *)decodedDocumentKey:(NSString *)name { - FSTResourcePath *path = [self decodedResourcePathWithDatabaseID:name]; - FSTAssert([[path segmentAtIndex:1] isEqualToString:self.databaseID.projectID], - @"Tried to deserialize key from different project."); - FSTAssert([[path segmentAtIndex:3] isEqualToString:self.databaseID.databaseID], - @"Tried to deserialize key from different datbase."); - return [FSTDocumentKey keyWithPath:[self localResourcePathForQualifiedResourcePath:path]]; -} - -- (NSString *)encodedResourcePathForDatabaseID:(FSTDatabaseID *)databaseID - path:(FSTResourcePath *)path { - return [[[[self encodedResourcePathForDatabaseID:databaseID] pathByAppendingSegment:@"documents"] - pathByAppendingPath:path] canonicalString]; -} - -- (FSTResourcePath *)decodedResourcePathWithDatabaseID:(NSString *)name { - FSTResourcePath *path = [FSTResourcePath pathWithString:name]; - FSTAssert([self validQualifiedResourcePath:path], @"Tried to deserialize invalid key %@", path); - return path; -} - -- (NSString *)encodedQueryPath:(FSTResourcePath *)path { - if (path.length == 0) { - // If the path is empty, the backend requires we leave off the /documents at the end. - return [self encodedDatabaseID]; - } - return [self encodedResourcePathForDatabaseID:self.databaseID path:path]; -} - -- (FSTResourcePath *)decodedQueryPath:(NSString *)name { - FSTResourcePath *resource = [self decodedResourcePathWithDatabaseID:name]; - if (resource.length == 4) { - return [FSTResourcePath pathWithSegments:@[]]; - } else { - return [self localResourcePathForQualifiedResourcePath:resource]; - } -} - -- (FSTResourcePath *)encodedResourcePathForDatabaseID:(FSTDatabaseID *)databaseID { - return [FSTResourcePath - pathWithSegments:@[ @"projects", databaseID.projectID, @"databases", databaseID.databaseID ]]; -} - -- (FSTResourcePath *)localResourcePathForQualifiedResourcePath:(FSTResourcePath *)resourceName { - FSTAssert( - resourceName.length > 4 && [[resourceName segmentAtIndex:4] isEqualToString:@"documents"], - @"Tried to deserialize invalid key %@", resourceName); - return [resourceName pathByRemovingFirstSegments:5]; -} - -- (BOOL)validQualifiedResourcePath:(FSTResourcePath *)path { - return path.length >= 4 && [[path segmentAtIndex:0] isEqualToString:@"projects"] && - [[path segmentAtIndex:2] isEqualToString:@"databases"]; -} - -- (NSString *)encodedDatabaseID { - return [[self encodedResourcePathForDatabaseID:self.databaseID] canonicalString]; -} - -#pragma mark - FSTFieldValue <=> Value proto - -- (GCFSValue *)encodedFieldValue:(FSTFieldValue *)fieldValue { - Class class = [fieldValue class]; - if (class == [FSTNullValue class]) { - return [self encodedNull]; - - } else if (class == [FSTBooleanValue class]) { - return [self encodedBool:[[fieldValue value] boolValue]]; - - } else if (class == [FSTIntegerValue class]) { - return [self encodedInteger:[[fieldValue value] longLongValue]]; - - } else if (class == [FSTDoubleValue class]) { - return [self encodedDouble:[[fieldValue value] doubleValue]]; - - } else if (class == [FSTStringValue class]) { - return [self encodedString:[fieldValue value]]; - - } else if (class == [FSTTimestampValue class]) { - return [self encodedTimestampValue:((FSTTimestampValue *)fieldValue).internalValue]; - - } else if (class == [FSTGeoPointValue class]) { - return [self encodedGeoPointValue:[fieldValue value]]; - - } else if (class == [FSTBlobValue class]) { - return [self encodedBlobValue:[fieldValue value]]; - - } else if (class == [FSTReferenceValue class]) { - FSTReferenceValue *ref = (FSTReferenceValue *)fieldValue; - return [self encodedReferenceValueForDatabaseID:[ref databaseID] key:[ref value]]; - - } else if (class == [FSTObjectValue class]) { - GCFSValue *result = [GCFSValue message]; - result.mapValue = [self encodedMapValue:(FSTObjectValue *)fieldValue]; - return result; - - } else if (class == [FSTArrayValue class]) { - GCFSValue *result = [GCFSValue message]; - result.arrayValue = [self encodedArrayValue:(FSTArrayValue *)fieldValue]; - return result; - - } else { - FSTFail(@"Unhandled type %@ on %@", NSStringFromClass([fieldValue class]), fieldValue); - } -} - -- (FSTFieldValue *)decodedFieldValue:(GCFSValue *)valueProto { - switch (valueProto.valueTypeOneOfCase) { - case GCFSValue_ValueType_OneOfCase_NullValue: - return [FSTNullValue nullValue]; - - case GCFSValue_ValueType_OneOfCase_BooleanValue: - return [FSTBooleanValue booleanValue:valueProto.booleanValue]; - - case GCFSValue_ValueType_OneOfCase_IntegerValue: - return [FSTIntegerValue integerValue:valueProto.integerValue]; - - case GCFSValue_ValueType_OneOfCase_DoubleValue: - return [FSTDoubleValue doubleValue:valueProto.doubleValue]; - - case GCFSValue_ValueType_OneOfCase_StringValue: - return [FSTStringValue stringValue:valueProto.stringValue]; - - case GCFSValue_ValueType_OneOfCase_TimestampValue: - return [FSTTimestampValue timestampValue:[self decodedTimestamp:valueProto.timestampValue]]; - - case GCFSValue_ValueType_OneOfCase_GeoPointValue: - return [FSTGeoPointValue geoPointValue:[self decodedGeoPoint:valueProto.geoPointValue]]; - - case GCFSValue_ValueType_OneOfCase_BytesValue: - return [FSTBlobValue blobValue:valueProto.bytesValue]; - - case GCFSValue_ValueType_OneOfCase_ReferenceValue: - return [self decodedReferenceValue:valueProto.referenceValue]; - - case GCFSValue_ValueType_OneOfCase_ArrayValue: - return [self decodedArrayValue:valueProto.arrayValue]; - - case GCFSValue_ValueType_OneOfCase_MapValue: - return [self decodedMapValue:valueProto.mapValue]; - - default: - FSTFail(@"Unhandled type %d on %@", valueProto.valueTypeOneOfCase, valueProto); - } -} - -- (GCFSValue *)encodedNull { - GCFSValue *result = [GCFSValue message]; - result.nullValue = GPBNullValue_NullValue; - return result; -} - -- (GCFSValue *)encodedBool:(BOOL)value { - GCFSValue *result = [GCFSValue message]; - result.booleanValue = value; - return result; -} - -- (GCFSValue *)encodedDouble:(double)value { - GCFSValue *result = [GCFSValue message]; - result.doubleValue = value; - return result; -} - -- (GCFSValue *)encodedInteger:(int64_t)value { - GCFSValue *result = [GCFSValue message]; - result.integerValue = value; - return result; -} - -- (GCFSValue *)encodedString:(NSString *)value { - GCFSValue *result = [GCFSValue message]; - result.stringValue = value; - return result; -} - -- (GCFSValue *)encodedTimestampValue:(FSTTimestamp *)value { - GCFSValue *result = [GCFSValue message]; - result.timestampValue = [self encodedTimestamp:value]; - return result; -} - -- (GCFSValue *)encodedGeoPointValue:(FIRGeoPoint *)value { - GCFSValue *result = [GCFSValue message]; - result.geoPointValue = [self encodedGeoPoint:value]; - return result; -} - -- (GCFSValue *)encodedBlobValue:(NSData *)value { - GCFSValue *result = [GCFSValue message]; - result.bytesValue = value; - return result; -} - -- (GCFSValue *)encodedReferenceValueForDatabaseID:(FSTDatabaseID *)databaseID - key:(FSTDocumentKey *)key { - GCFSValue *result = [GCFSValue message]; - result.referenceValue = [self encodedResourcePathForDatabaseID:databaseID path:key.path]; - return result; -} - -- (FSTReferenceValue *)decodedReferenceValue:(NSString *)resourceName { - FSTResourcePath *path = [self decodedResourcePathWithDatabaseID:resourceName]; - NSString *project = [path segmentAtIndex:1]; - NSString *database = [path segmentAtIndex:3]; - FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:project database:database]; - FSTDocumentKey *key = - [FSTDocumentKey keyWithPath:[self localResourcePathForQualifiedResourcePath:path]]; - return [FSTReferenceValue referenceValue:key databaseID:databaseID]; -} - -- (GCFSArrayValue *)encodedArrayValue:(FSTArrayValue *)arrayValue { - GCFSArrayValue *proto = [GCFSArrayValue message]; - NSMutableArray *protoContents = [proto valuesArray]; - - [[arrayValue internalValue] - enumerateObjectsUsingBlock:^(FSTFieldValue *value, NSUInteger idx, BOOL *stop) { - GCFSValue *converted = [self encodedFieldValue:value]; - [protoContents addObject:converted]; - }]; - return proto; -} - -- (FSTArrayValue *)decodedArrayValue:(GCFSArrayValue *)arrayValue { - NSMutableArray *contents = - [NSMutableArray arrayWithCapacity:arrayValue.valuesArray_Count]; - - [arrayValue.valuesArray - enumerateObjectsUsingBlock:^(GCFSValue *value, NSUInteger idx, BOOL *stop) { - [contents addObject:[self decodedFieldValue:value]]; - }]; - return [[FSTArrayValue alloc] initWithValueNoCopy:contents]; -} - -- (GCFSMapValue *)encodedMapValue:(FSTObjectValue *)value { - GCFSMapValue *result = [GCFSMapValue message]; - result.fields = [self encodedFields:value]; - return result; -} - -- (FSTObjectValue *)decodedMapValue:(GCFSMapValue *)map { - return [self decodedFields:map.fields]; -} - -/** - * Encodes an FSTObjectValue into a dictionary. - * @return a new dictionary that can be assigned to a field in another proto. - */ -- (NSMutableDictionary *)encodedFields:(FSTObjectValue *)value { - FSTImmutableSortedDictionary *fields = value.internalValue; - NSMutableDictionary *result = [NSMutableDictionary dictionary]; - [fields enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *obj, BOOL *stop) { - GCFSValue *converted = [self encodedFieldValue:obj]; - result[key] = converted; - }]; - return result; -} - -- (FSTObjectValue *)decodedFields:(NSDictionary *)fields { - __block FSTObjectValue *result = [FSTObjectValue objectValue]; - [fields enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, GCFSValue *_Nonnull obj, - BOOL *_Nonnull stop) { - FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ key ]]; - FSTFieldValue *value = [self decodedFieldValue:obj]; - result = [result objectBySettingValue:value forPath:path]; - }]; - return result; -} - -#pragma mark - FSTObjectValue <=> Document proto - -- (GCFSDocument *)encodedDocumentWithFields:(FSTObjectValue *)objectValue - key:(FSTDocumentKey *)key { - GCFSDocument *proto = [GCFSDocument message]; - proto.name = [self encodedDocumentKey:key]; - proto.fields = [self encodedFields:objectValue]; - return proto; -} - -#pragma mark - FSTMaybeDocument <= BatchGetDocumentsResponse proto - -- (FSTMaybeDocument *)decodedMaybeDocumentFromBatch:(GCFSBatchGetDocumentsResponse *)response { - switch (response.resultOneOfCase) { - case GCFSBatchGetDocumentsResponse_Result_OneOfCase_Found: - return [self decodedFoundDocument:response]; - case GCFSBatchGetDocumentsResponse_Result_OneOfCase_Missing: - return [self decodedDeletedDocument:response]; - default: - FSTFail(@"Unknown document type: %@", response); - } -} - -- (FSTDocument *)decodedFoundDocument:(GCFSBatchGetDocumentsResponse *)response { - FSTAssert(!!response.found, @"Tried to deserialize a found document from a deleted document."); - FSTDocumentKey *key = [self decodedDocumentKey:response.found.name]; - FSTObjectValue *value = [self decodedFields:response.found.fields]; - FSTSnapshotVersion *version = [self decodedVersion:response.found.updateTime]; - FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]], - @"Got a document response with no snapshot version"); - - return [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; -} - -- (FSTDeletedDocument *)decodedDeletedDocument:(GCFSBatchGetDocumentsResponse *)response { - FSTAssert(!!response.missing, @"Tried to deserialize a deleted document from a found document."); - FSTDocumentKey *key = [self decodedDocumentKey:response.missing]; - FSTSnapshotVersion *version = [self decodedVersion:response.readTime]; - FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]], - @"Got a no document response with no snapshot version"); - return [FSTDeletedDocument documentWithKey:key version:version]; -} - -#pragma mark - FSTMutation => GCFSWrite proto - -- (GCFSWrite *)encodedMutation:(FSTMutation *)mutation { - GCFSWrite *proto = [GCFSWrite message]; - - Class mutationClass = [mutation class]; - if (mutationClass == [FSTSetMutation class]) { - FSTSetMutation *set = (FSTSetMutation *)mutation; - proto.update = [self encodedDocumentWithFields:set.value key:set.key]; - - } else if (mutationClass == [FSTPatchMutation class]) { - FSTPatchMutation *patch = (FSTPatchMutation *)mutation; - proto.update = [self encodedDocumentWithFields:patch.value key:patch.key]; - proto.updateMask = [self encodedFieldMask:patch.fieldMask]; - - } else if (mutationClass == [FSTTransformMutation class]) { - FSTTransformMutation *transform = (FSTTransformMutation *)mutation; - - proto.transform = [GCFSDocumentTransform message]; - proto.transform.document = [self encodedDocumentKey:transform.key]; - proto.transform.fieldTransformsArray = [self encodedFieldTransforms:transform.fieldTransforms]; - // NOTE: We set a precondition of exists: true as a safety-check, since we always combine - // FSTTransformMutations with an FSTSetMutation or FSTPatchMutation which (if successful) should - // end up with an existing document. - proto.currentDocument.exists = YES; - - } else if (mutationClass == [FSTDeleteMutation class]) { - FSTDeleteMutation *delete = (FSTDeleteMutation *)mutation; - proto.delete_p = [self encodedDocumentKey:delete.key]; - - } else { - FSTFail(@"Unknown mutation type %@", NSStringFromClass(mutationClass)); - } - - if (!mutation.precondition.isNone) { - proto.currentDocument = [self encodedPrecondition:mutation.precondition]; - } - - return proto; -} - -- (FSTMutation *)decodedMutation:(GCFSWrite *)mutation { - FSTPrecondition *precondition = [mutation hasCurrentDocument] - ? [self decodedPrecondition:mutation.currentDocument] - : [FSTPrecondition none]; - - switch (mutation.operationOneOfCase) { - case GCFSWrite_Operation_OneOfCase_Update: - if (mutation.hasUpdateMask) { - return [[FSTPatchMutation alloc] initWithKey:[self decodedDocumentKey:mutation.update.name] - fieldMask:[self decodedFieldMask:mutation.updateMask] - value:[self decodedFields:mutation.update.fields] - precondition:precondition]; - } else { - return [[FSTSetMutation alloc] initWithKey:[self decodedDocumentKey:mutation.update.name] - value:[self decodedFields:mutation.update.fields] - precondition:precondition]; - } - - case GCFSWrite_Operation_OneOfCase_Delete_p: - return [[FSTDeleteMutation alloc] initWithKey:[self decodedDocumentKey:mutation.delete_p] - precondition:precondition]; - - case GCFSWrite_Operation_OneOfCase_Transform: { - FSTPreconditionExists exists = precondition.exists; - FSTAssert(exists == FSTPreconditionExistsYes, - @"Transforms must have precondition \"exists == true\""); - - return [[FSTTransformMutation alloc] - initWithKey:[self decodedDocumentKey:mutation.transform.document] - fieldTransforms:[self decodedFieldTransforms:mutation.transform.fieldTransformsArray]]; - } - - default: - // Note that insert is intentionally unhandled, since we don't ever deal in them. - FSTFail(@"Unknown mutation operation: %d", mutation.operationOneOfCase); - } -} - -- (GCFSPrecondition *)encodedPrecondition:(FSTPrecondition *)precondition { - FSTAssert(!precondition.isNone, @"Can't serialize an empty precondition"); - GCFSPrecondition *message = [GCFSPrecondition message]; - if (precondition.updateTime) { - message.updateTime = [self encodedVersion:precondition.updateTime]; - } else if (precondition.exists != FSTPreconditionExistsNotSet) { - message.exists = precondition.exists == FSTPreconditionExistsYes; - } else { - FSTFail(@"Unknown precondition: %@", precondition); - } - return message; -} - -- (FSTPrecondition *)decodedPrecondition:(GCFSPrecondition *)precondition { - switch (precondition.conditionTypeOneOfCase) { - case GCFSPrecondition_ConditionType_OneOfCase_GPBUnsetOneOfCase: - return [FSTPrecondition none]; - - case GCFSPrecondition_ConditionType_OneOfCase_Exists: - return [FSTPrecondition preconditionWithExists:precondition.exists]; - - case GCFSPrecondition_ConditionType_OneOfCase_UpdateTime: - return [FSTPrecondition - preconditionWithUpdateTime:[self decodedVersion:precondition.updateTime]]; - - default: - FSTFail(@"Unrecognized Precondition one-of case %@", precondition); - } -} - -- (GCFSDocumentMask *)encodedFieldMask:(FSTFieldMask *)fieldMask { - GCFSDocumentMask *mask = [GCFSDocumentMask message]; - for (FSTFieldPath *field in fieldMask.fields) { - [mask.fieldPathsArray addObject:field.canonicalString]; - } - return mask; -} - -- (FSTFieldMask *)decodedFieldMask:(GCFSDocumentMask *)fieldMask { - NSMutableArray *fields = - [NSMutableArray arrayWithCapacity:fieldMask.fieldPathsArray_Count]; - for (NSString *path in fieldMask.fieldPathsArray) { - [fields addObject:[FSTFieldPath pathWithServerFormat:path]]; - } - return [[FSTFieldMask alloc] initWithFields:fields]; -} - -- (NSMutableArray *)encodedFieldTransforms: - (NSArray *)fieldTransforms { - NSMutableArray *protos = [NSMutableArray array]; - for (FSTFieldTransform *fieldTransform in fieldTransforms) { - FSTAssert([fieldTransform.transform isKindOfClass:[FSTServerTimestampTransform class]], - @"Unknown transform: %@", fieldTransform.transform); - GCFSDocumentTransform_FieldTransform *proto = [GCFSDocumentTransform_FieldTransform message]; - proto.fieldPath = fieldTransform.path.canonicalString; - proto.setToServerValue = GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime; - [protos addObject:proto]; - } - return protos; -} - -- (NSArray *)decodedFieldTransforms: - (NSArray *)protos { - NSMutableArray *fieldTransforms = [NSMutableArray array]; - for (GCFSDocumentTransform_FieldTransform *proto in protos) { - FSTAssert( - proto.setToServerValue == GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime, - @"Unknown transform setToServerValue: %d", proto.setToServerValue); - [fieldTransforms - addObject:[[FSTFieldTransform alloc] - initWithPath:[FSTFieldPath pathWithServerFormat:proto.fieldPath] - transform:[FSTServerTimestampTransform serverTimestampTransform]]]; - } - return fieldTransforms; -} - -#pragma mark - FSTMutationResult <= GCFSWriteResult proto - -- (FSTMutationResult *)decodedMutationResult:(GCFSWriteResult *)mutation { - // NOTE: Deletes don't have an updateTime. - FSTSnapshotVersion *_Nullable version = - mutation.updateTime ? [self decodedVersion:mutation.updateTime] : nil; - NSMutableArray *_Nullable transformResults = nil; - if (mutation.transformResultsArray.count > 0) { - transformResults = [NSMutableArray array]; - for (GCFSValue *result in mutation.transformResultsArray) { - [transformResults addObject:[self decodedFieldValue:result]]; - } - } - return [[FSTMutationResult alloc] initWithVersion:version transformResults:transformResults]; -} - -#pragma mark - FSTQueryData => GCFSTarget proto - -- (nullable NSMutableDictionary *)encodedListenRequestLabelsForQueryData: - (FSTQueryData *)queryData { - NSString *value = [self encodedLabelForPurpose:queryData.purpose]; - if (!value) { - return nil; - } - - NSMutableDictionary *result = - [NSMutableDictionary dictionaryWithCapacity:1]; - [result setObject:value forKey:@"goog-listen-tags"]; - return result; -} - -- (nullable NSString *)encodedLabelForPurpose:(FSTQueryPurpose)purpose { - switch (purpose) { - case FSTQueryPurposeListen: - return nil; - case FSTQueryPurposeExistenceFilterMismatch: - return @"existence-filter-mismatch"; - case FSTQueryPurposeLimboResolution: - return @"limbo-document"; - default: - FSTFail(@"Unrecognized query purpose: %lu", (unsigned long)purpose); - } -} - -- (GCFSTarget *)encodedTarget:(FSTQueryData *)queryData { - GCFSTarget *result = [GCFSTarget message]; - FSTQuery *query = queryData.query; - - if ([query isDocumentQuery]) { - result.documents = [self encodedDocumentsTarget:query]; - } else { - result.query = [self encodedQueryTarget:query]; - } - - result.targetId = queryData.targetID; - if (queryData.resumeToken.length > 0) { - result.resumeToken = queryData.resumeToken; - } - - return result; -} - -- (GCFSTarget_DocumentsTarget *)encodedDocumentsTarget:(FSTQuery *)query { - GCFSTarget_DocumentsTarget *result = [GCFSTarget_DocumentsTarget message]; - NSMutableArray *docs = result.documentsArray; - [docs addObject:[self encodedQueryPath:query.path]]; - return result; -} - -- (FSTQuery *)decodedQueryFromDocumentsTarget:(GCFSTarget_DocumentsTarget *)target { - NSArray *documents = target.documentsArray; - FSTAssert(documents.count == 1, @"DocumentsTarget contained other than 1 document %lu", - (unsigned long)documents.count); - - NSString *name = documents[0]; - return [FSTQuery queryWithPath:[self decodedQueryPath:name]]; -} - -- (GCFSTarget_QueryTarget *)encodedQueryTarget:(FSTQuery *)query { - // Dissect the path into parent, collectionId, and optional key filter. - GCFSTarget_QueryTarget *queryTarget = [GCFSTarget_QueryTarget message]; - if (query.path.length == 0) { - queryTarget.parent = [self encodedQueryPath:query.path]; - } else { - FSTResourcePath *path = query.path; - FSTAssert(path.length % 2 != 0, @"Document queries with filters are not supported."); - queryTarget.parent = [self encodedQueryPath:[path pathByRemovingLastSegment]]; - GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; - from.collectionId = path.lastSegment; - [queryTarget.structuredQuery.fromArray addObject:from]; - } - - // Encode the filters. - GCFSStructuredQuery_Filter *_Nullable where = [self encodedFilters:query.filters]; - if (where) { - queryTarget.structuredQuery.where = where; - } - - NSArray *orders = [self encodedSortOrders:query.sortOrders]; - if (orders.count) { - [queryTarget.structuredQuery.orderByArray addObjectsFromArray:orders]; - } - - if (query.limit != NSNotFound) { - queryTarget.structuredQuery.limit.value = (int32_t)query.limit; - } - - if (query.startAt) { - queryTarget.structuredQuery.startAt = [self encodedBound:query.startAt]; - } - - if (query.endAt) { - queryTarget.structuredQuery.endAt = [self encodedBound:query.endAt]; - } - - return queryTarget; -} - -- (FSTQuery *)decodedQueryFromQueryTarget:(GCFSTarget_QueryTarget *)target { - FSTResourcePath *path = [self decodedQueryPath:target.parent]; - - GCFSStructuredQuery *query = target.structuredQuery; - NSUInteger fromCount = query.fromArray_Count; - if (fromCount > 0) { - FSTAssert(fromCount == 1, - @"StructuredQuery.from with more than one collection is not supported."); - - GCFSStructuredQuery_CollectionSelector *from = query.fromArray[0]; - path = [path pathByAppendingSegment:from.collectionId]; - } - - NSArray> *filterBy; - if (query.hasWhere) { - filterBy = [self decodedFilters:query.where]; - } else { - filterBy = @[]; - } - - NSArray *orderBy; - if (query.orderByArray_Count > 0) { - orderBy = [self decodedSortOrders:query.orderByArray]; - } else { - orderBy = @[]; - } - - NSInteger limit = NSNotFound; - if (query.hasLimit) { - limit = query.limit.value; - } - - FSTBound *_Nullable startAt; - if (query.hasStartAt) { - startAt = [self decodedBound:query.startAt]; - } - - FSTBound *_Nullable endAt; - if (query.hasEndAt) { - endAt = [self decodedBound:query.endAt]; - } - - return [[FSTQuery alloc] initWithPath:path - filterBy:filterBy - orderBy:orderBy - limit:limit - startAt:startAt - endAt:endAt]; -} - -#pragma mark Filters - -- (GCFSStructuredQuery_Filter *_Nullable)encodedFilters:(NSArray> *)filters { - if (filters.count == 0) { - return nil; - } - NSMutableArray *protos = [NSMutableArray array]; - for (id filter in filters) { - if ([filter isKindOfClass:[FSTRelationFilter class]]) { - [protos addObject:[self encodedRelationFilter:filter]]; - } else { - [protos addObject:[self encodedUnaryFilter:filter]]; - } - } - if (protos.count == 1) { - // Special case: no existing filters and we only need to add one filter. This can be made the - // single root filter without a composite filter. - return protos[0]; - } - GCFSStructuredQuery_Filter *composite = [GCFSStructuredQuery_Filter message]; - composite.compositeFilter.op = GCFSStructuredQuery_CompositeFilter_Operator_And; - composite.compositeFilter.filtersArray = protos; - return composite; -} - -- (NSArray> *)decodedFilters:(GCFSStructuredQuery_Filter *)proto { - NSMutableArray> *result = [NSMutableArray array]; - - NSArray *filters; - if (proto.filterTypeOneOfCase == - GCFSStructuredQuery_Filter_FilterType_OneOfCase_CompositeFilter) { - FSTAssert(proto.compositeFilter.op == GCFSStructuredQuery_CompositeFilter_Operator_And, - @"Only AND-type composite filters are supported, got %d", proto.compositeFilter.op); - filters = proto.compositeFilter.filtersArray; - } else { - filters = @[ proto ]; - } - - for (GCFSStructuredQuery_Filter *filter in filters) { - switch (filter.filterTypeOneOfCase) { - case GCFSStructuredQuery_Filter_FilterType_OneOfCase_CompositeFilter: - FSTFail(@"Nested composite filters are not supported"); - - case GCFSStructuredQuery_Filter_FilterType_OneOfCase_FieldFilter: - [result addObject:[self decodedRelationFilter:filter.fieldFilter]]; - break; - - case GCFSStructuredQuery_Filter_FilterType_OneOfCase_UnaryFilter: - [result addObject:[self decodedUnaryFilter:filter.unaryFilter]]; - break; - - default: - FSTFail(@"Unrecognized Filter.filterType %d", filter.filterTypeOneOfCase); - } - } - return result; -} - -- (GCFSStructuredQuery_Filter *)encodedRelationFilter:(FSTRelationFilter *)filter { - GCFSStructuredQuery_Filter *proto = [GCFSStructuredQuery_Filter message]; - GCFSStructuredQuery_FieldFilter *fieldFilter = proto.fieldFilter; - fieldFilter.field = [self encodedFieldPath:filter.field]; - fieldFilter.op = [self encodedRelationFilterOperator:filter.filterOperator]; - fieldFilter.value = [self encodedFieldValue:filter.value]; - return proto; -} - -- (FSTRelationFilter *)decodedRelationFilter:(GCFSStructuredQuery_FieldFilter *)proto { - FSTFieldPath *fieldPath = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath]; - FSTRelationFilterOperator filterOperator = [self decodedRelationFilterOperator:proto.op]; - FSTFieldValue *value = [self decodedFieldValue:proto.value]; - return [FSTRelationFilter filterWithField:fieldPath filterOperator:filterOperator value:value]; -} - -- (GCFSStructuredQuery_Filter *)encodedUnaryFilter:(id)filter { - GCFSStructuredQuery_Filter *proto = [GCFSStructuredQuery_Filter message]; - proto.unaryFilter.field = [self encodedFieldPath:filter.field]; - if ([filter isKindOfClass:[FSTNanFilter class]]) { - proto.unaryFilter.op = GCFSStructuredQuery_UnaryFilter_Operator_IsNan; - } else if ([filter isKindOfClass:[FSTNullFilter class]]) { - proto.unaryFilter.op = GCFSStructuredQuery_UnaryFilter_Operator_IsNull; - } else { - FSTFail(@"Unrecognized filter: %@", filter); - } - return proto; -} - -- (id)decodedUnaryFilter:(GCFSStructuredQuery_UnaryFilter *)proto { - FSTFieldPath *field = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath]; - switch (proto.op) { - case GCFSStructuredQuery_UnaryFilter_Operator_IsNan: - return [[FSTNanFilter alloc] initWithField:field]; - - case GCFSStructuredQuery_UnaryFilter_Operator_IsNull: - return [[FSTNullFilter alloc] initWithField:field]; - - default: - FSTFail(@"Unrecognized UnaryFilter.operator %d", proto.op); - } -} - -- (GCFSStructuredQuery_FieldReference *)encodedFieldPath:(FSTFieldPath *)fieldPath { - GCFSStructuredQuery_FieldReference *ref = [GCFSStructuredQuery_FieldReference message]; - ref.fieldPath = fieldPath.canonicalString; - return ref; -} - -- (GCFSStructuredQuery_FieldFilter_Operator)encodedRelationFilterOperator: - (FSTRelationFilterOperator)filterOperator { - switch (filterOperator) { - case FSTRelationFilterOperatorLessThan: - return GCFSStructuredQuery_FieldFilter_Operator_LessThan; - case FSTRelationFilterOperatorLessThanOrEqual: - return GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual; - case FSTRelationFilterOperatorEqual: - return GCFSStructuredQuery_FieldFilter_Operator_Equal; - case FSTRelationFilterOperatorGreaterThanOrEqual: - return GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual; - case FSTRelationFilterOperatorGreaterThan: - return GCFSStructuredQuery_FieldFilter_Operator_GreaterThan; - default: - FSTFail(@"Unhandled FSTRelationFilterOperator: %ld", (long)filterOperator); - } -} - -- (FSTRelationFilterOperator)decodedRelationFilterOperator: - (GCFSStructuredQuery_FieldFilter_Operator)filterOperator { - switch (filterOperator) { - case GCFSStructuredQuery_FieldFilter_Operator_LessThan: - return FSTRelationFilterOperatorLessThan; - case GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual: - return FSTRelationFilterOperatorLessThanOrEqual; - case GCFSStructuredQuery_FieldFilter_Operator_Equal: - return FSTRelationFilterOperatorEqual; - case GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual: - return FSTRelationFilterOperatorGreaterThanOrEqual; - case GCFSStructuredQuery_FieldFilter_Operator_GreaterThan: - return FSTRelationFilterOperatorGreaterThan; - default: - FSTFail(@"Unhandled FieldFilter.operator: %d", filterOperator); - } -} - -#pragma mark Property Orders - -- (NSArray *)encodedSortOrders:(NSArray *)orders { - NSMutableArray *protos = [NSMutableArray array]; - for (FSTSortOrder *order in orders) { - [protos addObject:[self encodedSortOrder:order]]; - } - return protos; -} - -- (NSArray *)decodedSortOrders:(NSArray *)protos { - NSMutableArray *result = [NSMutableArray arrayWithCapacity:protos.count]; - for (GCFSStructuredQuery_Order *orderProto in protos) { - [result addObject:[self decodedSortOrder:orderProto]]; - } - return result; -} - -- (GCFSStructuredQuery_Order *)encodedSortOrder:(FSTSortOrder *)sortOrder { - GCFSStructuredQuery_Order *proto = [GCFSStructuredQuery_Order message]; - proto.field = [self encodedFieldPath:sortOrder.field]; - if (sortOrder.ascending) { - proto.direction = GCFSStructuredQuery_Direction_Ascending; - } else { - proto.direction = GCFSStructuredQuery_Direction_Descending; - } - return proto; -} - -- (FSTSortOrder *)decodedSortOrder:(GCFSStructuredQuery_Order *)proto { - FSTFieldPath *fieldPath = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath]; - BOOL ascending; - switch (proto.direction) { - case GCFSStructuredQuery_Direction_Ascending: - ascending = YES; - break; - case GCFSStructuredQuery_Direction_Descending: - ascending = NO; - break; - default: - FSTFail(@"Unrecognized GCFSStructuredQuery_Direction %d", proto.direction); - } - return [FSTSortOrder sortOrderWithFieldPath:fieldPath ascending:ascending]; -} - -#pragma mark - Bounds/Cursors - -- (GCFSCursor *)encodedBound:(FSTBound *)bound { - GCFSCursor *proto = [GCFSCursor message]; - proto.before = bound.isBefore; - for (FSTFieldValue *fieldValue in bound.position) { - GCFSValue *value = [self encodedFieldValue:fieldValue]; - [proto.valuesArray addObject:value]; - } - return proto; -} - -- (FSTBound *)decodedBound:(GCFSCursor *)proto { - NSMutableArray *indexComponents = [NSMutableArray array]; - - for (GCFSValue *valueProto in proto.valuesArray) { - FSTFieldValue *value = [self decodedFieldValue:valueProto]; - [indexComponents addObject:value]; - } - - return [FSTBound boundWithPosition:indexComponents isBefore:proto.before]; -} - -#pragma mark - FSTWatchChange <= GCFSListenResponse proto - -- (FSTWatchChange *)decodedWatchChange:(GCFSListenResponse *)watchChange { - switch (watchChange.responseTypeOneOfCase) { - case GCFSListenResponse_ResponseType_OneOfCase_TargetChange: - return [self decodedTargetChangeFromWatchChange:watchChange.targetChange]; - - case GCFSListenResponse_ResponseType_OneOfCase_DocumentChange: - return [self decodedDocumentChange:watchChange.documentChange]; - - case GCFSListenResponse_ResponseType_OneOfCase_DocumentDelete: - return [self decodedDocumentDelete:watchChange.documentDelete]; - - case GCFSListenResponse_ResponseType_OneOfCase_DocumentRemove: - return [self decodedDocumentRemove:watchChange.documentRemove]; - - case GCFSListenResponse_ResponseType_OneOfCase_Filter: - return [self decodedExistenceFilterWatchChange:watchChange.filter]; - - default: - FSTFail(@"Unknown WatchChange.changeType %" PRId32, watchChange.responseTypeOneOfCase); - } -} - -- (FSTSnapshotVersion *)versionFromListenResponse:(GCFSListenResponse *)watchChange { - // We have only reached a consistent snapshot for the entire stream if there is a read_time set - // and it applies to all targets (i.e. the list of targets is empty). The backend is guaranteed to - // send such responses. - if (watchChange.responseTypeOneOfCase != GCFSListenResponse_ResponseType_OneOfCase_TargetChange) { - return [FSTSnapshotVersion noVersion]; - } - if (watchChange.targetChange.targetIdsArray.count != 0) { - return [FSTSnapshotVersion noVersion]; - } - return [self decodedVersion:watchChange.targetChange.readTime]; -} - -- (FSTWatchTargetChange *)decodedTargetChangeFromWatchChange:(GCFSTargetChange *)change { - FSTWatchTargetChangeState state = [self decodedWatchTargetChangeState:change.targetChangeType]; - NSMutableArray *targetIDs = - [NSMutableArray arrayWithCapacity:change.targetIdsArray_Count]; - - [change.targetIdsArray enumerateValuesWithBlock:^(int32_t value, NSUInteger idx, BOOL *stop) { - [targetIDs addObject:@(value)]; - }]; - - NSError *cause = nil; - if (change.hasCause) { - cause = [NSError errorWithDomain:FIRFirestoreErrorDomain - code:change.cause.code - userInfo:@{NSLocalizedDescriptionKey : change.cause.message}]; - } - - return [[FSTWatchTargetChange alloc] initWithState:state - targetIDs:targetIDs - resumeToken:change.resumeToken - cause:cause]; -} - -- (FSTWatchTargetChangeState)decodedWatchTargetChangeState: - (GCFSTargetChange_TargetChangeType)state { - switch (state) { - case GCFSTargetChange_TargetChangeType_NoChange: - return FSTWatchTargetChangeStateNoChange; - case GCFSTargetChange_TargetChangeType_Add: - return FSTWatchTargetChangeStateAdded; - case GCFSTargetChange_TargetChangeType_Remove: - return FSTWatchTargetChangeStateRemoved; - case GCFSTargetChange_TargetChangeType_Current: - return FSTWatchTargetChangeStateCurrent; - case GCFSTargetChange_TargetChangeType_Reset: - return FSTWatchTargetChangeStateReset; - default: - FSTFail(@"Unexpected TargetChange.state: %" PRId32, state); - } -} - -- (NSArray *)decodedIntegerArray:(GPBInt32Array *)values { - NSMutableArray *result = [NSMutableArray arrayWithCapacity:values.count]; - [values enumerateValuesWithBlock:^(int32_t value, NSUInteger idx, BOOL *stop) { - [result addObject:@(value)]; - }]; - return result; -} - -- (FSTDocumentWatchChange *)decodedDocumentChange:(GCFSDocumentChange *)change { - FSTObjectValue *value = [self decodedFields:change.document.fields]; - FSTDocumentKey *key = [self decodedDocumentKey:change.document.name]; - FSTSnapshotVersion *version = [self decodedVersion:change.document.updateTime]; - FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]], - @"Got a document change with no snapshot version"); - FSTMaybeDocument *document = - [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; - - NSArray *updatedTargetIds = [self decodedIntegerArray:change.targetIdsArray]; - NSArray *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray]; - - return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:updatedTargetIds - removedTargetIDs:removedTargetIds - documentKey:document.key - document:document]; -} - -- (FSTDocumentWatchChange *)decodedDocumentDelete:(GCFSDocumentDelete *)change { - FSTDocumentKey *key = [self decodedDocumentKey:change.document]; - // Note that version might be unset in which case we use [FSTSnapshotVersion noVersion] - FSTSnapshotVersion *version = [self decodedVersion:change.readTime]; - FSTMaybeDocument *document = [FSTDeletedDocument documentWithKey:key version:version]; - - NSArray *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray]; - - return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] - removedTargetIDs:removedTargetIds - documentKey:document.key - document:document]; -} - -- (FSTDocumentWatchChange *)decodedDocumentRemove:(GCFSDocumentRemove *)change { - FSTDocumentKey *key = [self decodedDocumentKey:change.document]; - NSArray *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray]; - - return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] - removedTargetIDs:removedTargetIds - documentKey:key - document:nil]; -} - -- (FSTExistenceFilterWatchChange *)decodedExistenceFilterWatchChange:(GCFSExistenceFilter *)filter { - // TODO(dimond): implement existence filter parsing - FSTExistenceFilter *existenceFilter = [FSTExistenceFilter filterWithCount:filter.count]; - FSTTargetID targetID = filter.targetId; - return [FSTExistenceFilterWatchChange changeWithFilter:existenceFilter targetID:targetID]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTSerializerBeta.mm b/Firestore/Source/Remote/FSTSerializerBeta.mm new file mode 100644 index 0000000..cf200ca --- /dev/null +++ b/Firestore/Source/Remote/FSTSerializerBeta.mm @@ -0,0 +1,1086 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Remote/FSTSerializerBeta.h" + +#include + +#import + +#import "Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.h" +#import "Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.h" +#import "Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h" +#import "Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.h" +#import "Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.h" +#import "Firestore/Protos/objc/google/rpc/Status.pbobjc.h" +#import "Firestore/Protos/objc/google/type/Latlng.pbobjc.h" + +#import "FIRFirestoreErrors.h" +#import "FIRGeoPoint.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Core/FSTTimestamp.h" +#import "Firestore/Source/Local/FSTQueryData.h" +#import "Firestore/Source/Model/FSTDatabaseID.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Model/FSTMutationBatch.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Remote/FSTExistenceFilter.h" +#import "Firestore/Source/Remote/FSTWatchChange.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTSerializerBeta () +@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID; +@end + +@implementation FSTSerializerBeta + +- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID { + self = [super init]; + if (self) { + _databaseID = databaseID; + } + return self; +} + +#pragma mark - FSTSnapshotVersion <=> GPBTimestamp + +- (GPBTimestamp *)encodedTimestamp:(FSTTimestamp *)timestamp { + GPBTimestamp *result = [GPBTimestamp message]; + result.seconds = timestamp.seconds; + result.nanos = timestamp.nanos; + return result; +} + +- (FSTTimestamp *)decodedTimestamp:(GPBTimestamp *)timestamp { + return [[FSTTimestamp alloc] initWithSeconds:timestamp.seconds nanos:timestamp.nanos]; +} + +- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version { + return [self encodedTimestamp:version.timestamp]; +} + +- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version { + return [FSTSnapshotVersion versionWithTimestamp:[self decodedTimestamp:version]]; +} + +#pragma mark - FIRGeoPoint <=> GTPLatLng + +- (GTPLatLng *)encodedGeoPoint:(FIRGeoPoint *)geoPoint { + GTPLatLng *latLng = [GTPLatLng message]; + latLng.latitude = geoPoint.latitude; + latLng.longitude = geoPoint.longitude; + return latLng; +} + +- (FIRGeoPoint *)decodedGeoPoint:(GTPLatLng *)latLng { + return [[FIRGeoPoint alloc] initWithLatitude:latLng.latitude longitude:latLng.longitude]; +} + +#pragma mark - FSTDocumentKey <=> Key proto + +- (NSString *)encodedDocumentKey:(FSTDocumentKey *)key { + return [self encodedResourcePathForDatabaseID:self.databaseID path:key.path]; +} + +- (FSTDocumentKey *)decodedDocumentKey:(NSString *)name { + FSTResourcePath *path = [self decodedResourcePathWithDatabaseID:name]; + FSTAssert([[path segmentAtIndex:1] isEqualToString:self.databaseID.projectID], + @"Tried to deserialize key from different project."); + FSTAssert([[path segmentAtIndex:3] isEqualToString:self.databaseID.databaseID], + @"Tried to deserialize key from different datbase."); + return [FSTDocumentKey keyWithPath:[self localResourcePathForQualifiedResourcePath:path]]; +} + +- (NSString *)encodedResourcePathForDatabaseID:(FSTDatabaseID *)databaseID + path:(FSTResourcePath *)path { + return [[[[self encodedResourcePathForDatabaseID:databaseID] pathByAppendingSegment:@"documents"] + pathByAppendingPath:path] canonicalString]; +} + +- (FSTResourcePath *)decodedResourcePathWithDatabaseID:(NSString *)name { + FSTResourcePath *path = [FSTResourcePath pathWithString:name]; + FSTAssert([self validQualifiedResourcePath:path], @"Tried to deserialize invalid key %@", path); + return path; +} + +- (NSString *)encodedQueryPath:(FSTResourcePath *)path { + if (path.length == 0) { + // If the path is empty, the backend requires we leave off the /documents at the end. + return [self encodedDatabaseID]; + } + return [self encodedResourcePathForDatabaseID:self.databaseID path:path]; +} + +- (FSTResourcePath *)decodedQueryPath:(NSString *)name { + FSTResourcePath *resource = [self decodedResourcePathWithDatabaseID:name]; + if (resource.length == 4) { + return [FSTResourcePath pathWithSegments:@[]]; + } else { + return [self localResourcePathForQualifiedResourcePath:resource]; + } +} + +- (FSTResourcePath *)encodedResourcePathForDatabaseID:(FSTDatabaseID *)databaseID { + return [FSTResourcePath + pathWithSegments:@[ @"projects", databaseID.projectID, @"databases", databaseID.databaseID ]]; +} + +- (FSTResourcePath *)localResourcePathForQualifiedResourcePath:(FSTResourcePath *)resourceName { + FSTAssert( + resourceName.length > 4 && [[resourceName segmentAtIndex:4] isEqualToString:@"documents"], + @"Tried to deserialize invalid key %@", resourceName); + return [resourceName pathByRemovingFirstSegments:5]; +} + +- (BOOL)validQualifiedResourcePath:(FSTResourcePath *)path { + return path.length >= 4 && [[path segmentAtIndex:0] isEqualToString:@"projects"] && + [[path segmentAtIndex:2] isEqualToString:@"databases"]; +} + +- (NSString *)encodedDatabaseID { + return [[self encodedResourcePathForDatabaseID:self.databaseID] canonicalString]; +} + +#pragma mark - FSTFieldValue <=> Value proto + +- (GCFSValue *)encodedFieldValue:(FSTFieldValue *)fieldValue { + Class fieldClass = [fieldValue class]; + if (fieldClass == [FSTNullValue class]) { + return [self encodedNull]; + + } else if (fieldClass == [FSTBooleanValue class]) { + return [self encodedBool:[[fieldValue value] boolValue]]; + + } else if (fieldClass == [FSTIntegerValue class]) { + return [self encodedInteger:[[fieldValue value] longLongValue]]; + + } else if (fieldClass == [FSTDoubleValue class]) { + return [self encodedDouble:[[fieldValue value] doubleValue]]; + + } else if (fieldClass == [FSTStringValue class]) { + return [self encodedString:[fieldValue value]]; + + } else if (fieldClass == [FSTTimestampValue class]) { + return [self encodedTimestampValue:((FSTTimestampValue *)fieldValue).internalValue]; + + } else if (fieldClass == [FSTGeoPointValue class]) { + return [self encodedGeoPointValue:[fieldValue value]]; + + } else if (fieldClass == [FSTBlobValue class]) { + return [self encodedBlobValue:[fieldValue value]]; + + } else if (fieldClass == [FSTReferenceValue class]) { + FSTReferenceValue *ref = (FSTReferenceValue *)fieldValue; + return [self encodedReferenceValueForDatabaseID:[ref databaseID] key:[ref value]]; + + } else if (fieldClass == [FSTObjectValue class]) { + GCFSValue *result = [GCFSValue message]; + result.mapValue = [self encodedMapValue:(FSTObjectValue *)fieldValue]; + return result; + + } else if (fieldClass == [FSTArrayValue class]) { + GCFSValue *result = [GCFSValue message]; + result.arrayValue = [self encodedArrayValue:(FSTArrayValue *)fieldValue]; + return result; + + } else { + FSTFail(@"Unhandled type %@ on %@", NSStringFromClass([fieldValue class]), fieldValue); + } +} + +- (FSTFieldValue *)decodedFieldValue:(GCFSValue *)valueProto { + switch (valueProto.valueTypeOneOfCase) { + case GCFSValue_ValueType_OneOfCase_NullValue: + return [FSTNullValue nullValue]; + + case GCFSValue_ValueType_OneOfCase_BooleanValue: + return [FSTBooleanValue booleanValue:valueProto.booleanValue]; + + case GCFSValue_ValueType_OneOfCase_IntegerValue: + return [FSTIntegerValue integerValue:valueProto.integerValue]; + + case GCFSValue_ValueType_OneOfCase_DoubleValue: + return [FSTDoubleValue doubleValue:valueProto.doubleValue]; + + case GCFSValue_ValueType_OneOfCase_StringValue: + return [FSTStringValue stringValue:valueProto.stringValue]; + + case GCFSValue_ValueType_OneOfCase_TimestampValue: + return [FSTTimestampValue timestampValue:[self decodedTimestamp:valueProto.timestampValue]]; + + case GCFSValue_ValueType_OneOfCase_GeoPointValue: + return [FSTGeoPointValue geoPointValue:[self decodedGeoPoint:valueProto.geoPointValue]]; + + case GCFSValue_ValueType_OneOfCase_BytesValue: + return [FSTBlobValue blobValue:valueProto.bytesValue]; + + case GCFSValue_ValueType_OneOfCase_ReferenceValue: + return [self decodedReferenceValue:valueProto.referenceValue]; + + case GCFSValue_ValueType_OneOfCase_ArrayValue: + return [self decodedArrayValue:valueProto.arrayValue]; + + case GCFSValue_ValueType_OneOfCase_MapValue: + return [self decodedMapValue:valueProto.mapValue]; + + default: + FSTFail(@"Unhandled type %d on %@", valueProto.valueTypeOneOfCase, valueProto); + } +} + +- (GCFSValue *)encodedNull { + GCFSValue *result = [GCFSValue message]; + result.nullValue = GPBNullValue_NullValue; + return result; +} + +- (GCFSValue *)encodedBool:(BOOL)value { + GCFSValue *result = [GCFSValue message]; + result.booleanValue = value; + return result; +} + +- (GCFSValue *)encodedDouble:(double)value { + GCFSValue *result = [GCFSValue message]; + result.doubleValue = value; + return result; +} + +- (GCFSValue *)encodedInteger:(int64_t)value { + GCFSValue *result = [GCFSValue message]; + result.integerValue = value; + return result; +} + +- (GCFSValue *)encodedString:(NSString *)value { + GCFSValue *result = [GCFSValue message]; + result.stringValue = value; + return result; +} + +- (GCFSValue *)encodedTimestampValue:(FSTTimestamp *)value { + GCFSValue *result = [GCFSValue message]; + result.timestampValue = [self encodedTimestamp:value]; + return result; +} + +- (GCFSValue *)encodedGeoPointValue:(FIRGeoPoint *)value { + GCFSValue *result = [GCFSValue message]; + result.geoPointValue = [self encodedGeoPoint:value]; + return result; +} + +- (GCFSValue *)encodedBlobValue:(NSData *)value { + GCFSValue *result = [GCFSValue message]; + result.bytesValue = value; + return result; +} + +- (GCFSValue *)encodedReferenceValueForDatabaseID:(FSTDatabaseID *)databaseID + key:(FSTDocumentKey *)key { + GCFSValue *result = [GCFSValue message]; + result.referenceValue = [self encodedResourcePathForDatabaseID:databaseID path:key.path]; + return result; +} + +- (FSTReferenceValue *)decodedReferenceValue:(NSString *)resourceName { + FSTResourcePath *path = [self decodedResourcePathWithDatabaseID:resourceName]; + NSString *project = [path segmentAtIndex:1]; + NSString *database = [path segmentAtIndex:3]; + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:project database:database]; + FSTDocumentKey *key = + [FSTDocumentKey keyWithPath:[self localResourcePathForQualifiedResourcePath:path]]; + return [FSTReferenceValue referenceValue:key databaseID:databaseID]; +} + +- (GCFSArrayValue *)encodedArrayValue:(FSTArrayValue *)arrayValue { + GCFSArrayValue *proto = [GCFSArrayValue message]; + NSMutableArray *protoContents = [proto valuesArray]; + + [[arrayValue internalValue] + enumerateObjectsUsingBlock:^(FSTFieldValue *value, NSUInteger idx, BOOL *stop) { + GCFSValue *converted = [self encodedFieldValue:value]; + [protoContents addObject:converted]; + }]; + return proto; +} + +- (FSTArrayValue *)decodedArrayValue:(GCFSArrayValue *)arrayValue { + NSMutableArray *contents = + [NSMutableArray arrayWithCapacity:arrayValue.valuesArray_Count]; + + [arrayValue.valuesArray + enumerateObjectsUsingBlock:^(GCFSValue *value, NSUInteger idx, BOOL *stop) { + [contents addObject:[self decodedFieldValue:value]]; + }]; + return [[FSTArrayValue alloc] initWithValueNoCopy:contents]; +} + +- (GCFSMapValue *)encodedMapValue:(FSTObjectValue *)value { + GCFSMapValue *result = [GCFSMapValue message]; + result.fields = [self encodedFields:value]; + return result; +} + +- (FSTObjectValue *)decodedMapValue:(GCFSMapValue *)map { + return [self decodedFields:map.fields]; +} + +/** + * Encodes an FSTObjectValue into a dictionary. + * @return a new dictionary that can be assigned to a field in another proto. + */ +- (NSMutableDictionary *)encodedFields:(FSTObjectValue *)value { + FSTImmutableSortedDictionary *fields = value.internalValue; + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + [fields enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *obj, BOOL *stop) { + GCFSValue *converted = [self encodedFieldValue:obj]; + result[key] = converted; + }]; + return result; +} + +- (FSTObjectValue *)decodedFields:(NSDictionary *)fields { + __block FSTObjectValue *result = [FSTObjectValue objectValue]; + [fields enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, GCFSValue *_Nonnull obj, + BOOL *_Nonnull stop) { + FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ key ]]; + FSTFieldValue *value = [self decodedFieldValue:obj]; + result = [result objectBySettingValue:value forPath:path]; + }]; + return result; +} + +#pragma mark - FSTObjectValue <=> Document proto + +- (GCFSDocument *)encodedDocumentWithFields:(FSTObjectValue *)objectValue + key:(FSTDocumentKey *)key { + GCFSDocument *proto = [GCFSDocument message]; + proto.name = [self encodedDocumentKey:key]; + proto.fields = [self encodedFields:objectValue]; + return proto; +} + +#pragma mark - FSTMaybeDocument <= BatchGetDocumentsResponse proto + +- (FSTMaybeDocument *)decodedMaybeDocumentFromBatch:(GCFSBatchGetDocumentsResponse *)response { + switch (response.resultOneOfCase) { + case GCFSBatchGetDocumentsResponse_Result_OneOfCase_Found: + return [self decodedFoundDocument:response]; + case GCFSBatchGetDocumentsResponse_Result_OneOfCase_Missing: + return [self decodedDeletedDocument:response]; + default: + FSTFail(@"Unknown document type: %@", response); + } +} + +- (FSTDocument *)decodedFoundDocument:(GCFSBatchGetDocumentsResponse *)response { + FSTAssert(!!response.found, @"Tried to deserialize a found document from a deleted document."); + FSTDocumentKey *key = [self decodedDocumentKey:response.found.name]; + FSTObjectValue *value = [self decodedFields:response.found.fields]; + FSTSnapshotVersion *version = [self decodedVersion:response.found.updateTime]; + FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]], + @"Got a document response with no snapshot version"); + + return [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; +} + +- (FSTDeletedDocument *)decodedDeletedDocument:(GCFSBatchGetDocumentsResponse *)response { + FSTAssert(!!response.missing, @"Tried to deserialize a deleted document from a found document."); + FSTDocumentKey *key = [self decodedDocumentKey:response.missing]; + FSTSnapshotVersion *version = [self decodedVersion:response.readTime]; + FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]], + @"Got a no document response with no snapshot version"); + return [FSTDeletedDocument documentWithKey:key version:version]; +} + +#pragma mark - FSTMutation => GCFSWrite proto + +- (GCFSWrite *)encodedMutation:(FSTMutation *)mutation { + GCFSWrite *proto = [GCFSWrite message]; + + Class mutationClass = [mutation class]; + if (mutationClass == [FSTSetMutation class]) { + FSTSetMutation *set = (FSTSetMutation *)mutation; + proto.update = [self encodedDocumentWithFields:set.value key:set.key]; + + } else if (mutationClass == [FSTPatchMutation class]) { + FSTPatchMutation *patch = (FSTPatchMutation *)mutation; + proto.update = [self encodedDocumentWithFields:patch.value key:patch.key]; + proto.updateMask = [self encodedFieldMask:patch.fieldMask]; + + } else if (mutationClass == [FSTTransformMutation class]) { + FSTTransformMutation *transform = (FSTTransformMutation *)mutation; + + proto.transform = [GCFSDocumentTransform message]; + proto.transform.document = [self encodedDocumentKey:transform.key]; + proto.transform.fieldTransformsArray = [self encodedFieldTransforms:transform.fieldTransforms]; + // NOTE: We set a precondition of exists: true as a safety-check, since we always combine + // FSTTransformMutations with an FSTSetMutation or FSTPatchMutation which (if successful) should + // end up with an existing document. + proto.currentDocument.exists = YES; + + } else if (mutationClass == [FSTDeleteMutation class]) { + FSTDeleteMutation *deleteMutation = (FSTDeleteMutation *)mutation; + proto.delete_p = [self encodedDocumentKey:deleteMutation.key]; + + } else { + FSTFail(@"Unknown mutation type %@", NSStringFromClass(mutationClass)); + } + + if (!mutation.precondition.isNone) { + proto.currentDocument = [self encodedPrecondition:mutation.precondition]; + } + + return proto; +} + +- (FSTMutation *)decodedMutation:(GCFSWrite *)mutation { + FSTPrecondition *precondition = [mutation hasCurrentDocument] + ? [self decodedPrecondition:mutation.currentDocument] + : [FSTPrecondition none]; + + switch (mutation.operationOneOfCase) { + case GCFSWrite_Operation_OneOfCase_Update: + if (mutation.hasUpdateMask) { + return [[FSTPatchMutation alloc] initWithKey:[self decodedDocumentKey:mutation.update.name] + fieldMask:[self decodedFieldMask:mutation.updateMask] + value:[self decodedFields:mutation.update.fields] + precondition:precondition]; + } else { + return [[FSTSetMutation alloc] initWithKey:[self decodedDocumentKey:mutation.update.name] + value:[self decodedFields:mutation.update.fields] + precondition:precondition]; + } + + case GCFSWrite_Operation_OneOfCase_Delete_p: + return [[FSTDeleteMutation alloc] initWithKey:[self decodedDocumentKey:mutation.delete_p] + precondition:precondition]; + + case GCFSWrite_Operation_OneOfCase_Transform: { + FSTPreconditionExists exists = precondition.exists; + FSTAssert(exists == FSTPreconditionExistsYes, + @"Transforms must have precondition \"exists == true\""); + + return [[FSTTransformMutation alloc] + initWithKey:[self decodedDocumentKey:mutation.transform.document] + fieldTransforms:[self decodedFieldTransforms:mutation.transform.fieldTransformsArray]]; + } + + default: + // Note that insert is intentionally unhandled, since we don't ever deal in them. + FSTFail(@"Unknown mutation operation: %d", mutation.operationOneOfCase); + } +} + +- (GCFSPrecondition *)encodedPrecondition:(FSTPrecondition *)precondition { + FSTAssert(!precondition.isNone, @"Can't serialize an empty precondition"); + GCFSPrecondition *message = [GCFSPrecondition message]; + if (precondition.updateTime) { + message.updateTime = [self encodedVersion:precondition.updateTime]; + } else if (precondition.exists != FSTPreconditionExistsNotSet) { + message.exists = precondition.exists == FSTPreconditionExistsYes; + } else { + FSTFail(@"Unknown precondition: %@", precondition); + } + return message; +} + +- (FSTPrecondition *)decodedPrecondition:(GCFSPrecondition *)precondition { + switch (precondition.conditionTypeOneOfCase) { + case GCFSPrecondition_ConditionType_OneOfCase_GPBUnsetOneOfCase: + return [FSTPrecondition none]; + + case GCFSPrecondition_ConditionType_OneOfCase_Exists: + return [FSTPrecondition preconditionWithExists:precondition.exists]; + + case GCFSPrecondition_ConditionType_OneOfCase_UpdateTime: + return [FSTPrecondition + preconditionWithUpdateTime:[self decodedVersion:precondition.updateTime]]; + + default: + FSTFail(@"Unrecognized Precondition one-of case %@", precondition); + } +} + +- (GCFSDocumentMask *)encodedFieldMask:(FSTFieldMask *)fieldMask { + GCFSDocumentMask *mask = [GCFSDocumentMask message]; + for (FSTFieldPath *field in fieldMask.fields) { + [mask.fieldPathsArray addObject:field.canonicalString]; + } + return mask; +} + +- (FSTFieldMask *)decodedFieldMask:(GCFSDocumentMask *)fieldMask { + NSMutableArray *fields = + [NSMutableArray arrayWithCapacity:fieldMask.fieldPathsArray_Count]; + for (NSString *path in fieldMask.fieldPathsArray) { + [fields addObject:[FSTFieldPath pathWithServerFormat:path]]; + } + return [[FSTFieldMask alloc] initWithFields:fields]; +} + +- (NSMutableArray *)encodedFieldTransforms: + (NSArray *)fieldTransforms { + NSMutableArray *protos = [NSMutableArray array]; + for (FSTFieldTransform *fieldTransform in fieldTransforms) { + FSTAssert([fieldTransform.transform isKindOfClass:[FSTServerTimestampTransform class]], + @"Unknown transform: %@", fieldTransform.transform); + GCFSDocumentTransform_FieldTransform *proto = [GCFSDocumentTransform_FieldTransform message]; + proto.fieldPath = fieldTransform.path.canonicalString; + proto.setToServerValue = GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime; + [protos addObject:proto]; + } + return protos; +} + +- (NSArray *)decodedFieldTransforms: + (NSArray *)protos { + NSMutableArray *fieldTransforms = [NSMutableArray array]; + for (GCFSDocumentTransform_FieldTransform *proto in protos) { + FSTAssert( + proto.setToServerValue == GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime, + @"Unknown transform setToServerValue: %d", proto.setToServerValue); + [fieldTransforms + addObject:[[FSTFieldTransform alloc] + initWithPath:[FSTFieldPath pathWithServerFormat:proto.fieldPath] + transform:[FSTServerTimestampTransform serverTimestampTransform]]]; + } + return fieldTransforms; +} + +#pragma mark - FSTMutationResult <= GCFSWriteResult proto + +- (FSTMutationResult *)decodedMutationResult:(GCFSWriteResult *)mutation { + // NOTE: Deletes don't have an updateTime. + FSTSnapshotVersion *_Nullable version = + mutation.updateTime ? [self decodedVersion:mutation.updateTime] : nil; + NSMutableArray *_Nullable transformResults = nil; + if (mutation.transformResultsArray.count > 0) { + transformResults = [NSMutableArray array]; + for (GCFSValue *result in mutation.transformResultsArray) { + [transformResults addObject:[self decodedFieldValue:result]]; + } + } + return [[FSTMutationResult alloc] initWithVersion:version transformResults:transformResults]; +} + +#pragma mark - FSTQueryData => GCFSTarget proto + +- (nullable NSMutableDictionary *)encodedListenRequestLabelsForQueryData: + (FSTQueryData *)queryData { + NSString *value = [self encodedLabelForPurpose:queryData.purpose]; + if (!value) { + return nil; + } + + NSMutableDictionary *result = + [NSMutableDictionary dictionaryWithCapacity:1]; + [result setObject:value forKey:@"goog-listen-tags"]; + return result; +} + +- (nullable NSString *)encodedLabelForPurpose:(FSTQueryPurpose)purpose { + switch (purpose) { + case FSTQueryPurposeListen: + return nil; + case FSTQueryPurposeExistenceFilterMismatch: + return @"existence-filter-mismatch"; + case FSTQueryPurposeLimboResolution: + return @"limbo-document"; + default: + FSTFail(@"Unrecognized query purpose: %lu", (unsigned long)purpose); + } +} + +- (GCFSTarget *)encodedTarget:(FSTQueryData *)queryData { + GCFSTarget *result = [GCFSTarget message]; + FSTQuery *query = queryData.query; + + if ([query isDocumentQuery]) { + result.documents = [self encodedDocumentsTarget:query]; + } else { + result.query = [self encodedQueryTarget:query]; + } + + result.targetId = queryData.targetID; + if (queryData.resumeToken.length > 0) { + result.resumeToken = queryData.resumeToken; + } + + return result; +} + +- (GCFSTarget_DocumentsTarget *)encodedDocumentsTarget:(FSTQuery *)query { + GCFSTarget_DocumentsTarget *result = [GCFSTarget_DocumentsTarget message]; + NSMutableArray *docs = result.documentsArray; + [docs addObject:[self encodedQueryPath:query.path]]; + return result; +} + +- (FSTQuery *)decodedQueryFromDocumentsTarget:(GCFSTarget_DocumentsTarget *)target { + NSArray *documents = target.documentsArray; + FSTAssert(documents.count == 1, @"DocumentsTarget contained other than 1 document %lu", + (unsigned long)documents.count); + + NSString *name = documents[0]; + return [FSTQuery queryWithPath:[self decodedQueryPath:name]]; +} + +- (GCFSTarget_QueryTarget *)encodedQueryTarget:(FSTQuery *)query { + // Dissect the path into parent, collectionId, and optional key filter. + GCFSTarget_QueryTarget *queryTarget = [GCFSTarget_QueryTarget message]; + if (query.path.length == 0) { + queryTarget.parent = [self encodedQueryPath:query.path]; + } else { + FSTResourcePath *path = query.path; + FSTAssert(path.length % 2 != 0, @"Document queries with filters are not supported."); + queryTarget.parent = [self encodedQueryPath:[path pathByRemovingLastSegment]]; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = path.lastSegment; + [queryTarget.structuredQuery.fromArray addObject:from]; + } + + // Encode the filters. + GCFSStructuredQuery_Filter *_Nullable where = [self encodedFilters:query.filters]; + if (where) { + queryTarget.structuredQuery.where = where; + } + + NSArray *orders = [self encodedSortOrders:query.sortOrders]; + if (orders.count) { + [queryTarget.structuredQuery.orderByArray addObjectsFromArray:orders]; + } + + if (query.limit != NSNotFound) { + queryTarget.structuredQuery.limit.value = (int32_t)query.limit; + } + + if (query.startAt) { + queryTarget.structuredQuery.startAt = [self encodedBound:query.startAt]; + } + + if (query.endAt) { + queryTarget.structuredQuery.endAt = [self encodedBound:query.endAt]; + } + + return queryTarget; +} + +- (FSTQuery *)decodedQueryFromQueryTarget:(GCFSTarget_QueryTarget *)target { + FSTResourcePath *path = [self decodedQueryPath:target.parent]; + + GCFSStructuredQuery *query = target.structuredQuery; + NSUInteger fromCount = query.fromArray_Count; + if (fromCount > 0) { + FSTAssert(fromCount == 1, + @"StructuredQuery.from with more than one collection is not supported."); + + GCFSStructuredQuery_CollectionSelector *from = query.fromArray[0]; + path = [path pathByAppendingSegment:from.collectionId]; + } + + NSArray> *filterBy; + if (query.hasWhere) { + filterBy = [self decodedFilters:query.where]; + } else { + filterBy = @[]; + } + + NSArray *orderBy; + if (query.orderByArray_Count > 0) { + orderBy = [self decodedSortOrders:query.orderByArray]; + } else { + orderBy = @[]; + } + + NSInteger limit = NSNotFound; + if (query.hasLimit) { + limit = query.limit.value; + } + + FSTBound *_Nullable startAt; + if (query.hasStartAt) { + startAt = [self decodedBound:query.startAt]; + } + + FSTBound *_Nullable endAt; + if (query.hasEndAt) { + endAt = [self decodedBound:query.endAt]; + } + + return [[FSTQuery alloc] initWithPath:path + filterBy:filterBy + orderBy:orderBy + limit:limit + startAt:startAt + endAt:endAt]; +} + +#pragma mark Filters + +- (GCFSStructuredQuery_Filter *_Nullable)encodedFilters:(NSArray> *)filters { + if (filters.count == 0) { + return nil; + } + NSMutableArray *protos = [NSMutableArray array]; + for (id filter in filters) { + if ([filter isKindOfClass:[FSTRelationFilter class]]) { + [protos addObject:[self encodedRelationFilter:filter]]; + } else { + [protos addObject:[self encodedUnaryFilter:filter]]; + } + } + if (protos.count == 1) { + // Special case: no existing filters and we only need to add one filter. This can be made the + // single root filter without a composite filter. + return protos[0]; + } + GCFSStructuredQuery_Filter *composite = [GCFSStructuredQuery_Filter message]; + composite.compositeFilter.op = GCFSStructuredQuery_CompositeFilter_Operator_And; + composite.compositeFilter.filtersArray = protos; + return composite; +} + +- (NSArray> *)decodedFilters:(GCFSStructuredQuery_Filter *)proto { + NSMutableArray> *result = [NSMutableArray array]; + + NSArray *filters; + if (proto.filterTypeOneOfCase == + GCFSStructuredQuery_Filter_FilterType_OneOfCase_CompositeFilter) { + FSTAssert(proto.compositeFilter.op == GCFSStructuredQuery_CompositeFilter_Operator_And, + @"Only AND-type composite filters are supported, got %d", proto.compositeFilter.op); + filters = proto.compositeFilter.filtersArray; + } else { + filters = @[ proto ]; + } + + for (GCFSStructuredQuery_Filter *filter in filters) { + switch (filter.filterTypeOneOfCase) { + case GCFSStructuredQuery_Filter_FilterType_OneOfCase_CompositeFilter: + FSTFail(@"Nested composite filters are not supported"); + + case GCFSStructuredQuery_Filter_FilterType_OneOfCase_FieldFilter: + [result addObject:[self decodedRelationFilter:filter.fieldFilter]]; + break; + + case GCFSStructuredQuery_Filter_FilterType_OneOfCase_UnaryFilter: + [result addObject:[self decodedUnaryFilter:filter.unaryFilter]]; + break; + + default: + FSTFail(@"Unrecognized Filter.filterType %d", filter.filterTypeOneOfCase); + } + } + return result; +} + +- (GCFSStructuredQuery_Filter *)encodedRelationFilter:(FSTRelationFilter *)filter { + GCFSStructuredQuery_Filter *proto = [GCFSStructuredQuery_Filter message]; + GCFSStructuredQuery_FieldFilter *fieldFilter = proto.fieldFilter; + fieldFilter.field = [self encodedFieldPath:filter.field]; + fieldFilter.op = [self encodedRelationFilterOperator:filter.filterOperator]; + fieldFilter.value = [self encodedFieldValue:filter.value]; + return proto; +} + +- (FSTRelationFilter *)decodedRelationFilter:(GCFSStructuredQuery_FieldFilter *)proto { + FSTFieldPath *fieldPath = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath]; + FSTRelationFilterOperator filterOperator = [self decodedRelationFilterOperator:proto.op]; + FSTFieldValue *value = [self decodedFieldValue:proto.value]; + return [FSTRelationFilter filterWithField:fieldPath filterOperator:filterOperator value:value]; +} + +- (GCFSStructuredQuery_Filter *)encodedUnaryFilter:(id)filter { + GCFSStructuredQuery_Filter *proto = [GCFSStructuredQuery_Filter message]; + proto.unaryFilter.field = [self encodedFieldPath:filter.field]; + if ([filter isKindOfClass:[FSTNanFilter class]]) { + proto.unaryFilter.op = GCFSStructuredQuery_UnaryFilter_Operator_IsNan; + } else if ([filter isKindOfClass:[FSTNullFilter class]]) { + proto.unaryFilter.op = GCFSStructuredQuery_UnaryFilter_Operator_IsNull; + } else { + FSTFail(@"Unrecognized filter: %@", filter); + } + return proto; +} + +- (id)decodedUnaryFilter:(GCFSStructuredQuery_UnaryFilter *)proto { + FSTFieldPath *field = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath]; + switch (proto.op) { + case GCFSStructuredQuery_UnaryFilter_Operator_IsNan: + return [[FSTNanFilter alloc] initWithField:field]; + + case GCFSStructuredQuery_UnaryFilter_Operator_IsNull: + return [[FSTNullFilter alloc] initWithField:field]; + + default: + FSTFail(@"Unrecognized UnaryFilter.operator %d", proto.op); + } +} + +- (GCFSStructuredQuery_FieldReference *)encodedFieldPath:(FSTFieldPath *)fieldPath { + GCFSStructuredQuery_FieldReference *ref = [GCFSStructuredQuery_FieldReference message]; + ref.fieldPath = fieldPath.canonicalString; + return ref; +} + +- (GCFSStructuredQuery_FieldFilter_Operator)encodedRelationFilterOperator: + (FSTRelationFilterOperator)filterOperator { + switch (filterOperator) { + case FSTRelationFilterOperatorLessThan: + return GCFSStructuredQuery_FieldFilter_Operator_LessThan; + case FSTRelationFilterOperatorLessThanOrEqual: + return GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual; + case FSTRelationFilterOperatorEqual: + return GCFSStructuredQuery_FieldFilter_Operator_Equal; + case FSTRelationFilterOperatorGreaterThanOrEqual: + return GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual; + case FSTRelationFilterOperatorGreaterThan: + return GCFSStructuredQuery_FieldFilter_Operator_GreaterThan; + default: + FSTFail(@"Unhandled FSTRelationFilterOperator: %ld", (long)filterOperator); + } +} + +- (FSTRelationFilterOperator)decodedRelationFilterOperator: + (GCFSStructuredQuery_FieldFilter_Operator)filterOperator { + switch (filterOperator) { + case GCFSStructuredQuery_FieldFilter_Operator_LessThan: + return FSTRelationFilterOperatorLessThan; + case GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual: + return FSTRelationFilterOperatorLessThanOrEqual; + case GCFSStructuredQuery_FieldFilter_Operator_Equal: + return FSTRelationFilterOperatorEqual; + case GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual: + return FSTRelationFilterOperatorGreaterThanOrEqual; + case GCFSStructuredQuery_FieldFilter_Operator_GreaterThan: + return FSTRelationFilterOperatorGreaterThan; + default: + FSTFail(@"Unhandled FieldFilter.operator: %d", filterOperator); + } +} + +#pragma mark Property Orders + +- (NSArray *)encodedSortOrders:(NSArray *)orders { + NSMutableArray *protos = [NSMutableArray array]; + for (FSTSortOrder *order in orders) { + [protos addObject:[self encodedSortOrder:order]]; + } + return protos; +} + +- (NSArray *)decodedSortOrders:(NSArray *)protos { + NSMutableArray *result = [NSMutableArray arrayWithCapacity:protos.count]; + for (GCFSStructuredQuery_Order *orderProto in protos) { + [result addObject:[self decodedSortOrder:orderProto]]; + } + return result; +} + +- (GCFSStructuredQuery_Order *)encodedSortOrder:(FSTSortOrder *)sortOrder { + GCFSStructuredQuery_Order *proto = [GCFSStructuredQuery_Order message]; + proto.field = [self encodedFieldPath:sortOrder.field]; + if (sortOrder.ascending) { + proto.direction = GCFSStructuredQuery_Direction_Ascending; + } else { + proto.direction = GCFSStructuredQuery_Direction_Descending; + } + return proto; +} + +- (FSTSortOrder *)decodedSortOrder:(GCFSStructuredQuery_Order *)proto { + FSTFieldPath *fieldPath = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath]; + BOOL ascending; + switch (proto.direction) { + case GCFSStructuredQuery_Direction_Ascending: + ascending = YES; + break; + case GCFSStructuredQuery_Direction_Descending: + ascending = NO; + break; + default: + FSTFail(@"Unrecognized GCFSStructuredQuery_Direction %d", proto.direction); + } + return [FSTSortOrder sortOrderWithFieldPath:fieldPath ascending:ascending]; +} + +#pragma mark - Bounds/Cursors + +- (GCFSCursor *)encodedBound:(FSTBound *)bound { + GCFSCursor *proto = [GCFSCursor message]; + proto.before = bound.isBefore; + for (FSTFieldValue *fieldValue in bound.position) { + GCFSValue *value = [self encodedFieldValue:fieldValue]; + [proto.valuesArray addObject:value]; + } + return proto; +} + +- (FSTBound *)decodedBound:(GCFSCursor *)proto { + NSMutableArray *indexComponents = [NSMutableArray array]; + + for (GCFSValue *valueProto in proto.valuesArray) { + FSTFieldValue *value = [self decodedFieldValue:valueProto]; + [indexComponents addObject:value]; + } + + return [FSTBound boundWithPosition:indexComponents isBefore:proto.before]; +} + +#pragma mark - FSTWatchChange <= GCFSListenResponse proto + +- (FSTWatchChange *)decodedWatchChange:(GCFSListenResponse *)watchChange { + switch (watchChange.responseTypeOneOfCase) { + case GCFSListenResponse_ResponseType_OneOfCase_TargetChange: + return [self decodedTargetChangeFromWatchChange:watchChange.targetChange]; + + case GCFSListenResponse_ResponseType_OneOfCase_DocumentChange: + return [self decodedDocumentChange:watchChange.documentChange]; + + case GCFSListenResponse_ResponseType_OneOfCase_DocumentDelete: + return [self decodedDocumentDelete:watchChange.documentDelete]; + + case GCFSListenResponse_ResponseType_OneOfCase_DocumentRemove: + return [self decodedDocumentRemove:watchChange.documentRemove]; + + case GCFSListenResponse_ResponseType_OneOfCase_Filter: + return [self decodedExistenceFilterWatchChange:watchChange.filter]; + + default: + FSTFail(@"Unknown WatchChange.changeType %" PRId32, watchChange.responseTypeOneOfCase); + } +} + +- (FSTSnapshotVersion *)versionFromListenResponse:(GCFSListenResponse *)watchChange { + // We have only reached a consistent snapshot for the entire stream if there is a read_time set + // and it applies to all targets (i.e. the list of targets is empty). The backend is guaranteed to + // send such responses. + if (watchChange.responseTypeOneOfCase != GCFSListenResponse_ResponseType_OneOfCase_TargetChange) { + return [FSTSnapshotVersion noVersion]; + } + if (watchChange.targetChange.targetIdsArray.count != 0) { + return [FSTSnapshotVersion noVersion]; + } + return [self decodedVersion:watchChange.targetChange.readTime]; +} + +- (FSTWatchTargetChange *)decodedTargetChangeFromWatchChange:(GCFSTargetChange *)change { + FSTWatchTargetChangeState state = [self decodedWatchTargetChangeState:change.targetChangeType]; + NSMutableArray *targetIDs = + [NSMutableArray arrayWithCapacity:change.targetIdsArray_Count]; + + [change.targetIdsArray enumerateValuesWithBlock:^(int32_t value, NSUInteger idx, BOOL *stop) { + [targetIDs addObject:@(value)]; + }]; + + NSError *cause = nil; + if (change.hasCause) { + cause = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:change.cause.code + userInfo:@{NSLocalizedDescriptionKey : change.cause.message}]; + } + + return [[FSTWatchTargetChange alloc] initWithState:state + targetIDs:targetIDs + resumeToken:change.resumeToken + cause:cause]; +} + +- (FSTWatchTargetChangeState)decodedWatchTargetChangeState: + (GCFSTargetChange_TargetChangeType)state { + switch (state) { + case GCFSTargetChange_TargetChangeType_NoChange: + return FSTWatchTargetChangeStateNoChange; + case GCFSTargetChange_TargetChangeType_Add: + return FSTWatchTargetChangeStateAdded; + case GCFSTargetChange_TargetChangeType_Remove: + return FSTWatchTargetChangeStateRemoved; + case GCFSTargetChange_TargetChangeType_Current: + return FSTWatchTargetChangeStateCurrent; + case GCFSTargetChange_TargetChangeType_Reset: + return FSTWatchTargetChangeStateReset; + default: + FSTFail(@"Unexpected TargetChange.state: %" PRId32, state); + } +} + +- (NSArray *)decodedIntegerArray:(GPBInt32Array *)values { + NSMutableArray *result = [NSMutableArray arrayWithCapacity:values.count]; + [values enumerateValuesWithBlock:^(int32_t value, NSUInteger idx, BOOL *stop) { + [result addObject:@(value)]; + }]; + return result; +} + +- (FSTDocumentWatchChange *)decodedDocumentChange:(GCFSDocumentChange *)change { + FSTObjectValue *value = [self decodedFields:change.document.fields]; + FSTDocumentKey *key = [self decodedDocumentKey:change.document.name]; + FSTSnapshotVersion *version = [self decodedVersion:change.document.updateTime]; + FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]], + @"Got a document change with no snapshot version"); + FSTMaybeDocument *document = + [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; + + NSArray *updatedTargetIds = [self decodedIntegerArray:change.targetIdsArray]; + NSArray *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray]; + + return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:updatedTargetIds + removedTargetIDs:removedTargetIds + documentKey:document.key + document:document]; +} + +- (FSTDocumentWatchChange *)decodedDocumentDelete:(GCFSDocumentDelete *)change { + FSTDocumentKey *key = [self decodedDocumentKey:change.document]; + // Note that version might be unset in which case we use [FSTSnapshotVersion noVersion] + FSTSnapshotVersion *version = [self decodedVersion:change.readTime]; + FSTMaybeDocument *document = [FSTDeletedDocument documentWithKey:key version:version]; + + NSArray *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray]; + + return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:removedTargetIds + documentKey:document.key + document:document]; +} + +- (FSTDocumentWatchChange *)decodedDocumentRemove:(GCFSDocumentRemove *)change { + FSTDocumentKey *key = [self decodedDocumentKey:change.document]; + NSArray *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray]; + + return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:removedTargetIds + documentKey:key + document:nil]; +} + +- (FSTExistenceFilterWatchChange *)decodedExistenceFilterWatchChange:(GCFSExistenceFilter *)filter { + // TODO(dimond): implement existence filter parsing + FSTExistenceFilter *existenceFilter = [FSTExistenceFilter filterWithCount:filter.count]; + FSTTargetID targetID = filter.targetId; + return [FSTExistenceFilterWatchChange changeWithFilter:existenceFilter targetID:targetID]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTStream.m b/Firestore/Source/Remote/FSTStream.m deleted file mode 100644 index dc7d01e..0000000 --- a/Firestore/Source/Remote/FSTStream.m +++ /dev/null @@ -1,787 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Remote/FSTDatastore.h" - -#import -#import - -#import "FIRFirestoreErrors.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/Auth/FSTCredentialsProvider.h" -#import "Firestore/Source/Core/FSTDatabaseInfo.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTDatabaseID.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Remote/FSTBufferedWriter.h" -#import "Firestore/Source/Remote/FSTExponentialBackoff.h" -#import "Firestore/Source/Remote/FSTSerializerBeta.h" -#import "Firestore/Source/Remote/FSTStream.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTClasses.h" -#import "Firestore/Source/Util/FSTDispatchQueue.h" -#import "Firestore/Source/Util/FSTLogger.h" - -#import "Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h" - -/** - * Initial backoff time in seconds after an error. - * Set to 1s according to https://cloud.google.com/apis/design/errors. - */ -static const NSTimeInterval kBackoffInitialDelay = 1; -static const NSTimeInterval kBackoffMaxDelay = 60.0; -static const double kBackoffFactor = 1.5; - -#pragma mark - FSTStream - -/** The state of a stream. */ -typedef NS_ENUM(NSInteger, FSTStreamState) { - /** - * The streaming RPC is not running and there's no error condition. Calling `start` will - * start the stream immediately without backoff. While in this state -isStarted will return NO. - */ - FSTStreamStateInitial = 0, - - /** - * The stream is starting, and is waiting for an auth token to attach to the initial request. - * While in this state, isStarted will return YES but isOpen will return NO. - */ - FSTStreamStateAuth, - - /** - * The streaming RPC is up and running. Requests and responses can flow freely. Both - * isStarted and isOpen will return YES. - */ - FSTStreamStateOpen, - - /** - * The stream encountered an error. The next start attempt will back off. While in this state - * -isStarted will return NO. - */ - FSTStreamStateError, - - /** - * An in-between state after an error where the stream is waiting before re-starting. After - * waiting is complete, the stream will try to open. While in this state -isStarted will - * return YES but isOpen will return NO. - */ - FSTStreamStateBackoff, - - /** - * The stream has been explicitly stopped; no further events will be emitted. - */ - FSTStreamStateStopped, -}; - -// We need to declare these classes first so that Datastore can alloc them. - -@interface FSTWatchStream () - -/** - * Initializes the watch stream with its dependencies. - */ -- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue - credentials:(id)credentials - serializer:(FSTSerializerBeta *)serializer NS_DESIGNATED_INITIALIZER; - -- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue - credentials:(id)credentials - responseMessageClass:(Class)responseMessageClass NS_UNAVAILABLE; - -@end - -@interface FSTStream () - -@property(nonatomic, getter=isIdle) BOOL idle; -@property(nonatomic, weak, readwrite, nullable) id delegate; - -@end - -@interface FSTStream () - -@property(nonatomic, strong, readonly) FSTDatabaseInfo *databaseInfo; -@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue; -@property(nonatomic, strong, readonly) id credentials; -@property(nonatomic, unsafe_unretained, readonly) Class responseMessageClass; -@property(nonatomic, strong, readonly) FSTExponentialBackoff *backoff; - -/** A flag tracking whether the stream received a message from the backend. */ -@property(nonatomic, assign) BOOL messageReceived; - -/** - * Stream state as exposed to consumers of FSTStream. This differs from GRXWriter's notion of the - * state of the stream. - */ -@property(nonatomic, assign) FSTStreamState state; - -/** The RPC handle. Used for cancellation. */ -@property(nonatomic, strong, nullable) GRPCCall *rpc; - -/** - * The send-side of the RPC stream in which to submit requests, but only once the underlying RPC has - * started. - */ -@property(nonatomic, strong, nullable) FSTBufferedWriter *requestsWriter; - -@end - -#pragma mark - FSTCallbackFilter - -/** Filter class that allows disabling of GRPC callbacks. */ -@interface FSTCallbackFilter : NSObject - -- (instancetype)initWithStream:(FSTStream *)stream NS_DESIGNATED_INITIALIZER; -- (instancetype)init NS_UNAVAILABLE; - -@property(atomic, readwrite) BOOL callbacksEnabled; -@property(nonatomic, strong, readonly) FSTStream *stream; - -@end - -@implementation FSTCallbackFilter - -- (instancetype)initWithStream:(FSTStream *)stream { - if (self = [super init]) { - _callbacksEnabled = YES; - _stream = stream; - } - return self; -} - -- (void)suppressCallbacks { - _callbacksEnabled = NO; -} - -- (void)writeValue:(id)value { - if (_callbacksEnabled) { - [self.stream writeValue:value]; - } -} - -- (void)writesFinishedWithError:(NSError *)errorOrNil { - if (_callbacksEnabled) { - [self.stream writesFinishedWithError:errorOrNil]; - } -} - -@end - -#pragma mark - FSTStream - -@interface FSTStream () - -@property(nonatomic, strong, readwrite) FSTCallbackFilter *callbackFilter; - -@end - -@implementation FSTStream - -/** The time a stream stays open after it is marked idle. */ -static const NSTimeInterval kIdleTimeout = 60.0; - -- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue - credentials:(id)credentials - responseMessageClass:(Class)responseMessageClass { - if (self = [super init]) { - _databaseInfo = database; - _workerDispatchQueue = workerDispatchQueue; - _credentials = credentials; - _responseMessageClass = responseMessageClass; - - _backoff = [FSTExponentialBackoff exponentialBackoffWithDispatchQueue:workerDispatchQueue - initialDelay:kBackoffInitialDelay - backoffFactor:kBackoffFactor - maxDelay:kBackoffMaxDelay]; - _state = FSTStreamStateInitial; - } - return self; -} - -- (BOOL)isStarted { - [self.workerDispatchQueue verifyIsCurrentQueue]; - FSTStreamState state = self.state; - return state == FSTStreamStateBackoff || state == FSTStreamStateAuth || - state == FSTStreamStateOpen; -} - -- (BOOL)isOpen { - [self.workerDispatchQueue verifyIsCurrentQueue]; - return self.state == FSTStreamStateOpen; -} - -- (GRPCCall *)createRPCWithRequestsWriter:(GRXWriter *)requestsWriter { - @throw FSTAbstractMethodException(); // NOLINT -} - -- (void)startWithDelegate:(id)delegate { - [self.workerDispatchQueue verifyIsCurrentQueue]; - - if (self.state == FSTStreamStateError) { - [self performBackoffWithDelegate:delegate]; - return; - } - - FSTLog(@"%@ %p start", NSStringFromClass([self class]), (__bridge void *)self); - FSTAssert(self.state == FSTStreamStateInitial, @"Already started"); - - self.state = FSTStreamStateAuth; - FSTAssert(_delegate == nil, @"Delegate must be nil"); - _delegate = delegate; - - [self.credentials - getTokenForcingRefresh:NO - completion:^(FSTGetTokenResult *_Nullable result, NSError *_Nullable error) { - error = [FSTDatastore firestoreErrorForError:error]; - [self.workerDispatchQueue dispatchAsyncAllowingSameQueue:^{ - [self resumeStartWithToken:result error:error]; - }]; - }]; -} - -/** Add an access token to our RPC, after obtaining one from the credentials provider. */ -- (void)resumeStartWithToken:(FSTGetTokenResult *)token error:(NSError *)error { - if (self.state == FSTStreamStateStopped) { - // Streams can be stopped while waiting for authorization. - return; - } - - [self.workerDispatchQueue verifyIsCurrentQueue]; - FSTAssert(self.state == FSTStreamStateAuth, @"State should still be auth (was %ld)", - (long)self.state); - - // TODO(mikelehen): We should force a refresh if the previous RPC failed due to an expired token, - // but I'm not sure how to detect that right now. http://b/32762461 - if (error) { - // RPC has not been started yet, so just invoke higher-level close handler. - [self handleStreamClose:error]; - return; - } - - self.requestsWriter = [[FSTBufferedWriter alloc] init]; - _rpc = [self createRPCWithRequestsWriter:self.requestsWriter]; - [FSTDatastore prepareHeadersForRPC:_rpc - databaseID:self.databaseInfo.databaseID - token:token.token]; - FSTAssert(_callbackFilter == nil, @"GRX Filter must be nil"); - _callbackFilter = [[FSTCallbackFilter alloc] initWithStream:self]; - [_rpc startWithWriteable:_callbackFilter]; - - self.state = FSTStreamStateOpen; - [self notifyStreamOpen]; -} - -/** Backs off after an error. */ -- (void)performBackoffWithDelegate:(id)delegate { - FSTLog(@"%@ %p backoff", NSStringFromClass([self class]), (__bridge void *)self); - [self.workerDispatchQueue verifyIsCurrentQueue]; - - FSTAssert(self.state == FSTStreamStateError, @"Should only perform backoff in an error case"); - self.state = FSTStreamStateBackoff; - - FSTWeakify(self); - [self.backoff backoffAndRunBlock:^{ - FSTStrongify(self); - [self resumeStartFromBackoffWithDelegate:delegate]; - }]; -} - -/** Resumes stream start after backing off. */ -- (void)resumeStartFromBackoffWithDelegate:(id)delegate { - if (self.state == FSTStreamStateStopped) { - // Streams can be stopped while waiting for backoff to complete. - return; - } - - // In order to have performed a backoff the stream must have been in an error state just prior - // to entering the backoff state. If we weren't stopped we must be in the backoff state. - FSTAssert(self.state == FSTStreamStateBackoff, @"State should still be backoff (was %ld)", - (long)self.state); - - // Momentarily set state to FSTStreamStateInitial as `start` expects it. - self.state = FSTStreamStateInitial; - [self startWithDelegate:delegate]; - FSTAssert([self isStarted], @"Stream should have started."); -} - -/** - * Can be overridden to perform additional cleanup before the stream is closed. Calling - * [super tearDown] is not required. - */ -- (void)tearDown { -} - -/** - * Closes the stream and cleans up as necessary: - * - * * closes the underlying GRPC stream; - * * calls the onClose handler with the given 'error'; - * * sets internal stream state to 'finalState'; - * * adjusts the backoff timer based on the error - * - * A new stream can be opened by calling `start` unless `finalState` is set to - * `FSTStreamStateStopped`. - * - * @param finalState the intended state of the stream after closing. - * @param error the NSError the connection was closed with. - */ -- (void)closeWithFinalState:(FSTStreamState)finalState error:(nullable NSError *)error { - FSTAssert(finalState == FSTStreamStateError || error == nil, - @"Can't provide an error when not in an error state."); - - [self.workerDispatchQueue verifyIsCurrentQueue]; - [self cancelIdleCheck]; - - if (finalState != FSTStreamStateError) { - // If this is an intentional close ensure we don't delay our next connection attempt. - [self.backoff reset]; - } else if (error != nil && error.code == FIRFirestoreErrorCodeResourceExhausted) { - FSTLog(@"%@ %p Using maximum backoff delay to prevent overloading the backend.", [self class], - (__bridge void *)self); - [self.backoff resetToMax]; - } - - [self tearDown]; - - if (self.requestsWriter) { - // Clean up the underlying RPC. If this close: is in response to an error, don't attempt to - // call half-close to avoid secondary failures. - if (finalState != FSTStreamStateError) { - FSTLog(@"%@ %p Closing stream client-side", [self class], (__bridge void *)self); - @synchronized(self.requestsWriter) { - [self.requestsWriter finishWithError:nil]; - } - } - _requestsWriter = nil; - } - - // This state must be assigned before calling `notifyStreamInterrupted` to allow the callback to - // inhibit backoff or otherwise manipulate the state in its non-started state. - self.state = finalState; - - [self.callbackFilter suppressCallbacks]; - _callbackFilter = nil; - - // Clean up remaining state. - _messageReceived = NO; - _rpc = nil; - - // If the caller explicitly requested a stream stop, don't notify them of a closing stream (it - // could trigger undesirable recovery logic, etc.). - if (finalState != FSTStreamStateStopped) { - [self notifyStreamInterruptedWithError:error]; - } - - // Clear the delegates to avoid any possible bleed through of events from GRPC. - _delegate = nil; -} - -- (void)stop { - FSTLog(@"%@ %p stop", NSStringFromClass([self class]), (__bridge void *)self); - if ([self isStarted]) { - [self closeWithFinalState:FSTStreamStateStopped error:nil]; - } -} - -- (void)inhibitBackoff { - FSTAssert(![self isStarted], @"Can only inhibit backoff after an error (was %ld)", - (long)self.state); - [self.workerDispatchQueue verifyIsCurrentQueue]; - - // Clear the error condition. - self.state = FSTStreamStateInitial; - [self.backoff reset]; -} - -/** Called by the idle timer when the stream should close due to inactivity. */ -- (void)handleIdleCloseTimer { - [self.workerDispatchQueue verifyIsCurrentQueue]; - if (self.state == FSTStreamStateOpen && [self isIdle]) { - // When timing out an idle stream there's no reason to force the stream into backoff when - // it restarts so set the stream state to Initial instead of Error. - [self closeWithFinalState:FSTStreamStateInitial error:nil]; - } -} - -- (void)markIdle { - [self.workerDispatchQueue verifyIsCurrentQueue]; - if (self.state == FSTStreamStateOpen) { - self.idle = YES; - [self.workerDispatchQueue dispatchAfterDelay:kIdleTimeout - block:^() { - [self handleIdleCloseTimer]; - }]; - } -} - -- (void)cancelIdleCheck { - [self.workerDispatchQueue verifyIsCurrentQueue]; - self.idle = NO; -} - -/** - * Parses a protocol buffer response from the server. If the message fails to parse, generates - * an error and closes the stream. - * - * @param protoClass A protocol buffer message class object, that responds to parseFromData:error:. - * @param data The bytes in the response as returned from GRPC. - * @return An instance of the protocol buffer message, parsed from the data if parsing was - * successful, or nil otherwise. - */ -- (nullable id)parseProto:(Class)protoClass data:(NSData *)data error:(NSError **)error { - NSError *parseError; - id parsed = [protoClass parseFromData:data error:&parseError]; - if (parsed) { - *error = nil; - return parsed; - } else { - NSDictionary *info = @{ - NSLocalizedDescriptionKey : @"Unable to parse response from the server", - NSUnderlyingErrorKey : parseError, - @"Expected class" : protoClass, - @"Received value" : data, - }; - *error = [NSError errorWithDomain:FIRFirestoreErrorDomain - code:FIRFirestoreErrorCodeInternal - userInfo:info]; - return nil; - } -} - -/** - * Writes a request proto into the stream. - */ -- (void)writeRequest:(GPBMessage *)request { - NSData *data = [request data]; - - [self cancelIdleCheck]; - - FSTBufferedWriter *requestsWriter = self.requestsWriter; - @synchronized(requestsWriter) { - [requestsWriter writeValue:data]; - } -} - -#pragma mark Template methods for subclasses - -/** - * Called by the stream after the stream has opened. - * - * Subclasses should relay to their stream-specific delegate. Calling [super notifyStreamOpen] is - * not required. - */ -- (void)notifyStreamOpen { -} - -/** - * Called by the stream after the stream has been unexpectedly interrupted, either due to an error - * or due to idleness. - * - * Subclasses should relay to their stream-specific delegate. Calling [super - * notifyStreamInterrupted] is not required. - */ -- (void)notifyStreamInterruptedWithError:(nullable NSError *)error { -} - -/** - * Called by the stream for each incoming protocol message coming from the server. - * - * Subclasses should implement this to deserialize the value and relay to their stream-specific - * delegate, if appropriate. Calling [super handleStreamMessage] is not required. - */ -- (void)handleStreamMessage:(id)value { -} - -/** - * Called by the stream when the underlying RPC has been closed for whatever reason. - */ -- (void)handleStreamClose:(nullable NSError *)error { - FSTLog(@"%@ %p close: %@", NSStringFromClass([self class]), (__bridge void *)self, error); - - if (![self isStarted]) { // The stream could have already been closed by the idle close timer. - FSTLog(@"%@ Ignoring server close for already closed stream.", NSStringFromClass([self class])); - return; - } - - // In theory the stream could close cleanly, however, in our current model we never expect this - // to happen because if we stop a stream ourselves, this callback will never be called. To - // prevent cases where we retry without a backoff accidentally, we set the stream to error - // in all cases. - [self closeWithFinalState:FSTStreamStateError error:error]; -} - -#pragma mark GRXWriteable implementation -// The GRXWriteable implementation defines the receive side of the RPC stream. - -/** - * Called by GRPC when it publishes a value. It is called from GRPC's own queue so we immediately - * redispatch back onto our own worker queue. - */ -- (void)writeValue:(id)value __used { - // TODO(mcg): remove the double-dispatch once GRPCCall at head is released. - // Once released we can set the responseDispatchQueue property on the GRPCCall and then this - // method can call handleStreamMessage directly. - FSTWeakify(self); - [self.workerDispatchQueue dispatchAsync:^{ - FSTStrongify(self); - if (![self isStarted]) { - FSTLog(@"%@ Ignoring stream message from inactive stream.", NSStringFromClass([self class])); - } - - if (!self.messageReceived) { - self.messageReceived = YES; - if ([FIRFirestore isLoggingEnabled]) { - FSTLog(@"%@ %p headers (whitelisted): %@", NSStringFromClass([self class]), - (__bridge void *)self, - [FSTDatastore extractWhiteListedHeaders:self.rpc.responseHeaders]); - } - } - NSError *error; - id proto = [self parseProto:self.responseMessageClass data:value error:&error]; - if (proto) { - [self handleStreamMessage:proto]; - } else { - [_rpc finishWithError:error]; - } - }]; -} - -/** - * Called by GRPC when it closed the stream with an error representing the final state of the - * stream. - * - * Do not call directly, since it dispatches via the worker queue. Call handleStreamClose to - * directly inform stream-specific logic, or call stop to tear down the stream. - */ -- (void)writesFinishedWithError:(nullable NSError *)error __used { - error = [FSTDatastore firestoreErrorForError:error]; - FSTWeakify(self); - [self.workerDispatchQueue dispatchAsync:^{ - FSTStrongify(self); - if (!self || self.state == FSTStreamStateStopped) { - return; - } - [self handleStreamClose:error]; - }]; -} - -@end - -#pragma mark - FSTWatchStream - -@interface FSTWatchStream () - -@property(nonatomic, strong, readonly) FSTSerializerBeta *serializer; - -@end - -@implementation FSTWatchStream - -- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue - credentials:(id)credentials - serializer:(FSTSerializerBeta *)serializer { - self = [super initWithDatabase:database - workerDispatchQueue:workerDispatchQueue - credentials:credentials - responseMessageClass:[GCFSListenResponse class]]; - if (self) { - _serializer = serializer; - } - return self; -} - -- (GRPCCall *)createRPCWithRequestsWriter:(GRXWriter *)requestsWriter { - return [[GRPCCall alloc] initWithHost:self.databaseInfo.host - path:@"/google.firestore.v1beta1.Firestore/Listen" - requestsWriter:requestsWriter]; -} - -- (void)notifyStreamOpen { - [self.delegate watchStreamDidOpen]; -} - -- (void)notifyStreamInterruptedWithError:(nullable NSError *)error { - id delegate = self.delegate; - self.delegate = nil; - [delegate watchStreamWasInterruptedWithError:error]; -} - -- (void)watchQuery:(FSTQueryData *)query { - FSTAssert([self isOpen], @"Not yet open"); - [self.workerDispatchQueue verifyIsCurrentQueue]; - - GCFSListenRequest *request = [GCFSListenRequest message]; - request.database = [_serializer encodedDatabaseID]; - request.addTarget = [_serializer encodedTarget:query]; - request.labels = [_serializer encodedListenRequestLabelsForQueryData:query]; - - FSTLog(@"FSTWatchStream %p watch: %@", (__bridge void *)self, request); - [self writeRequest:request]; -} - -- (void)unwatchTargetID:(FSTTargetID)targetID { - FSTAssert([self isOpen], @"Not yet open"); - [self.workerDispatchQueue verifyIsCurrentQueue]; - - GCFSListenRequest *request = [GCFSListenRequest message]; - request.database = [_serializer encodedDatabaseID]; - request.removeTarget = targetID; - - FSTLog(@"FSTWatchStream %p unwatch: %@", (__bridge void *)self, request); - [self writeRequest:request]; -} - -/** - * Receives an inbound message from GRPC, deserializes, and then passes that on to the delegate's - * watchStreamDidChange:snapshotVersion: callback. - */ -- (void)handleStreamMessage:(GCFSListenResponse *)proto { - FSTLog(@"FSTWatchStream %p response: %@", (__bridge void *)self, proto); - [self.workerDispatchQueue verifyIsCurrentQueue]; - - // A successful response means the stream is healthy. - [self.backoff reset]; - - FSTWatchChange *change = [_serializer decodedWatchChange:proto]; - FSTSnapshotVersion *snap = [_serializer versionFromListenResponse:proto]; - [self.delegate watchStreamDidChange:change snapshotVersion:snap]; -} - -@end - -#pragma mark - FSTWriteStream - -@interface FSTWriteStream () - -@property(nonatomic, strong, readonly) FSTSerializerBeta *serializer; - -@end - -@implementation FSTWriteStream - -- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue - credentials:(id)credentials - serializer:(FSTSerializerBeta *)serializer { - self = [super initWithDatabase:database - workerDispatchQueue:workerDispatchQueue - credentials:credentials - responseMessageClass:[GCFSWriteResponse class]]; - if (self) { - _serializer = serializer; - } - return self; -} - -- (GRPCCall *)createRPCWithRequestsWriter:(GRXWriter *)requestsWriter { - return [[GRPCCall alloc] initWithHost:self.databaseInfo.host - path:@"/google.firestore.v1beta1.Firestore/Write" - requestsWriter:requestsWriter]; -} - -- (void)startWithDelegate:(id)delegate { - self.handshakeComplete = NO; - [super startWithDelegate:delegate]; -} - -- (void)notifyStreamOpen { - [self.delegate writeStreamDidOpen]; -} - -- (void)notifyStreamInterruptedWithError:(nullable NSError *)error { - id delegate = self.delegate; - self.delegate = nil; - [delegate writeStreamWasInterruptedWithError:error]; -} - -- (void)tearDown { - if ([self isHandshakeComplete]) { - // Send an empty write request to the backend to indicate imminent stream closure. This allows - // the backend to clean up resources. - [self writeMutations:@[]]; - } -} - -- (void)writeHandshake { - // The initial request cannot contain mutations, but must contain a projectID. - FSTAssert([self isOpen], @"Not yet open"); - FSTAssert(!self.handshakeComplete, @"Handshake sent out of turn"); - [self.workerDispatchQueue verifyIsCurrentQueue]; - - GCFSWriteRequest *request = [GCFSWriteRequest message]; - request.database = [_serializer encodedDatabaseID]; - // TODO(dimond): Support stream resumption. We intentionally do not set the stream token on the - // handshake, ignoring any stream token we might have. - - FSTLog(@"FSTWriteStream %p initial request: %@", (__bridge void *)self, request); - [self writeRequest:request]; -} - -- (void)writeMutations:(NSArray *)mutations { - FSTAssert([self isOpen], @"Not yet open"); - FSTAssert(self.handshakeComplete, @"Mutations sent out of turn"); - [self.workerDispatchQueue verifyIsCurrentQueue]; - - NSMutableArray *protos = [NSMutableArray arrayWithCapacity:mutations.count]; - for (FSTMutation *mutation in mutations) { - [protos addObject:[_serializer encodedMutation:mutation]]; - }; - - GCFSWriteRequest *request = [GCFSWriteRequest message]; - request.writesArray = protos; - request.streamToken = self.lastStreamToken; - - FSTLog(@"FSTWriteStream %p mutation request: %@", (__bridge void *)self, request); - [self writeRequest:request]; -} - -/** - * Implements GRXWriteable to receive an inbound message from GRPC, deserialize, and then pass - * that on to the mutationResultsHandler. - */ -- (void)handleStreamMessage:(GCFSWriteResponse *)response { - FSTLog(@"FSTWriteStream %p response: %@", (__bridge void *)self, response); - [self.workerDispatchQueue verifyIsCurrentQueue]; - - // Always capture the last stream token. - self.lastStreamToken = response.streamToken; - - if (!self.isHandshakeComplete) { - // The first response is the handshake response - self.handshakeComplete = YES; - - [self.delegate writeStreamDidCompleteHandshake]; - } else { - // A successful first write response means the stream is healthy. - // Note that we could consider a successful handshake healthy, however, the write itself - // might be causing an error we want to back off from. - [self.backoff reset]; - - FSTSnapshotVersion *commitVersion = [_serializer decodedVersion:response.commitTime]; - NSMutableArray *protos = response.writeResultsArray; - NSMutableArray *results = [NSMutableArray arrayWithCapacity:protos.count]; - for (GCFSWriteResult *proto in protos) { - [results addObject:[_serializer decodedMutationResult:proto]]; - }; - - [self.delegate writeStreamDidReceiveResponseWithVersion:commitVersion mutationResults:results]; - } -} - -@end diff --git a/Firestore/Source/Remote/FSTStream.mm b/Firestore/Source/Remote/FSTStream.mm new file mode 100644 index 0000000..dc7d01e --- /dev/null +++ b/Firestore/Source/Remote/FSTStream.mm @@ -0,0 +1,787 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Remote/FSTDatastore.h" + +#import +#import + +#import "FIRFirestoreErrors.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/Auth/FSTCredentialsProvider.h" +#import "Firestore/Source/Core/FSTDatabaseInfo.h" +#import "Firestore/Source/Local/FSTQueryData.h" +#import "Firestore/Source/Model/FSTDatabaseID.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Remote/FSTBufferedWriter.h" +#import "Firestore/Source/Remote/FSTExponentialBackoff.h" +#import "Firestore/Source/Remote/FSTSerializerBeta.h" +#import "Firestore/Source/Remote/FSTStream.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTClasses.h" +#import "Firestore/Source/Util/FSTDispatchQueue.h" +#import "Firestore/Source/Util/FSTLogger.h" + +#import "Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h" + +/** + * Initial backoff time in seconds after an error. + * Set to 1s according to https://cloud.google.com/apis/design/errors. + */ +static const NSTimeInterval kBackoffInitialDelay = 1; +static const NSTimeInterval kBackoffMaxDelay = 60.0; +static const double kBackoffFactor = 1.5; + +#pragma mark - FSTStream + +/** The state of a stream. */ +typedef NS_ENUM(NSInteger, FSTStreamState) { + /** + * The streaming RPC is not running and there's no error condition. Calling `start` will + * start the stream immediately without backoff. While in this state -isStarted will return NO. + */ + FSTStreamStateInitial = 0, + + /** + * The stream is starting, and is waiting for an auth token to attach to the initial request. + * While in this state, isStarted will return YES but isOpen will return NO. + */ + FSTStreamStateAuth, + + /** + * The streaming RPC is up and running. Requests and responses can flow freely. Both + * isStarted and isOpen will return YES. + */ + FSTStreamStateOpen, + + /** + * The stream encountered an error. The next start attempt will back off. While in this state + * -isStarted will return NO. + */ + FSTStreamStateError, + + /** + * An in-between state after an error where the stream is waiting before re-starting. After + * waiting is complete, the stream will try to open. While in this state -isStarted will + * return YES but isOpen will return NO. + */ + FSTStreamStateBackoff, + + /** + * The stream has been explicitly stopped; no further events will be emitted. + */ + FSTStreamStateStopped, +}; + +// We need to declare these classes first so that Datastore can alloc them. + +@interface FSTWatchStream () + +/** + * Initializes the watch stream with its dependencies. + */ +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + serializer:(FSTSerializerBeta *)serializer NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + responseMessageClass:(Class)responseMessageClass NS_UNAVAILABLE; + +@end + +@interface FSTStream () + +@property(nonatomic, getter=isIdle) BOOL idle; +@property(nonatomic, weak, readwrite, nullable) id delegate; + +@end + +@interface FSTStream () + +@property(nonatomic, strong, readonly) FSTDatabaseInfo *databaseInfo; +@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue; +@property(nonatomic, strong, readonly) id credentials; +@property(nonatomic, unsafe_unretained, readonly) Class responseMessageClass; +@property(nonatomic, strong, readonly) FSTExponentialBackoff *backoff; + +/** A flag tracking whether the stream received a message from the backend. */ +@property(nonatomic, assign) BOOL messageReceived; + +/** + * Stream state as exposed to consumers of FSTStream. This differs from GRXWriter's notion of the + * state of the stream. + */ +@property(nonatomic, assign) FSTStreamState state; + +/** The RPC handle. Used for cancellation. */ +@property(nonatomic, strong, nullable) GRPCCall *rpc; + +/** + * The send-side of the RPC stream in which to submit requests, but only once the underlying RPC has + * started. + */ +@property(nonatomic, strong, nullable) FSTBufferedWriter *requestsWriter; + +@end + +#pragma mark - FSTCallbackFilter + +/** Filter class that allows disabling of GRPC callbacks. */ +@interface FSTCallbackFilter : NSObject + +- (instancetype)initWithStream:(FSTStream *)stream NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@property(atomic, readwrite) BOOL callbacksEnabled; +@property(nonatomic, strong, readonly) FSTStream *stream; + +@end + +@implementation FSTCallbackFilter + +- (instancetype)initWithStream:(FSTStream *)stream { + if (self = [super init]) { + _callbacksEnabled = YES; + _stream = stream; + } + return self; +} + +- (void)suppressCallbacks { + _callbacksEnabled = NO; +} + +- (void)writeValue:(id)value { + if (_callbacksEnabled) { + [self.stream writeValue:value]; + } +} + +- (void)writesFinishedWithError:(NSError *)errorOrNil { + if (_callbacksEnabled) { + [self.stream writesFinishedWithError:errorOrNil]; + } +} + +@end + +#pragma mark - FSTStream + +@interface FSTStream () + +@property(nonatomic, strong, readwrite) FSTCallbackFilter *callbackFilter; + +@end + +@implementation FSTStream + +/** The time a stream stays open after it is marked idle. */ +static const NSTimeInterval kIdleTimeout = 60.0; + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + responseMessageClass:(Class)responseMessageClass { + if (self = [super init]) { + _databaseInfo = database; + _workerDispatchQueue = workerDispatchQueue; + _credentials = credentials; + _responseMessageClass = responseMessageClass; + + _backoff = [FSTExponentialBackoff exponentialBackoffWithDispatchQueue:workerDispatchQueue + initialDelay:kBackoffInitialDelay + backoffFactor:kBackoffFactor + maxDelay:kBackoffMaxDelay]; + _state = FSTStreamStateInitial; + } + return self; +} + +- (BOOL)isStarted { + [self.workerDispatchQueue verifyIsCurrentQueue]; + FSTStreamState state = self.state; + return state == FSTStreamStateBackoff || state == FSTStreamStateAuth || + state == FSTStreamStateOpen; +} + +- (BOOL)isOpen { + [self.workerDispatchQueue verifyIsCurrentQueue]; + return self.state == FSTStreamStateOpen; +} + +- (GRPCCall *)createRPCWithRequestsWriter:(GRXWriter *)requestsWriter { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (void)startWithDelegate:(id)delegate { + [self.workerDispatchQueue verifyIsCurrentQueue]; + + if (self.state == FSTStreamStateError) { + [self performBackoffWithDelegate:delegate]; + return; + } + + FSTLog(@"%@ %p start", NSStringFromClass([self class]), (__bridge void *)self); + FSTAssert(self.state == FSTStreamStateInitial, @"Already started"); + + self.state = FSTStreamStateAuth; + FSTAssert(_delegate == nil, @"Delegate must be nil"); + _delegate = delegate; + + [self.credentials + getTokenForcingRefresh:NO + completion:^(FSTGetTokenResult *_Nullable result, NSError *_Nullable error) { + error = [FSTDatastore firestoreErrorForError:error]; + [self.workerDispatchQueue dispatchAsyncAllowingSameQueue:^{ + [self resumeStartWithToken:result error:error]; + }]; + }]; +} + +/** Add an access token to our RPC, after obtaining one from the credentials provider. */ +- (void)resumeStartWithToken:(FSTGetTokenResult *)token error:(NSError *)error { + if (self.state == FSTStreamStateStopped) { + // Streams can be stopped while waiting for authorization. + return; + } + + [self.workerDispatchQueue verifyIsCurrentQueue]; + FSTAssert(self.state == FSTStreamStateAuth, @"State should still be auth (was %ld)", + (long)self.state); + + // TODO(mikelehen): We should force a refresh if the previous RPC failed due to an expired token, + // but I'm not sure how to detect that right now. http://b/32762461 + if (error) { + // RPC has not been started yet, so just invoke higher-level close handler. + [self handleStreamClose:error]; + return; + } + + self.requestsWriter = [[FSTBufferedWriter alloc] init]; + _rpc = [self createRPCWithRequestsWriter:self.requestsWriter]; + [FSTDatastore prepareHeadersForRPC:_rpc + databaseID:self.databaseInfo.databaseID + token:token.token]; + FSTAssert(_callbackFilter == nil, @"GRX Filter must be nil"); + _callbackFilter = [[FSTCallbackFilter alloc] initWithStream:self]; + [_rpc startWithWriteable:_callbackFilter]; + + self.state = FSTStreamStateOpen; + [self notifyStreamOpen]; +} + +/** Backs off after an error. */ +- (void)performBackoffWithDelegate:(id)delegate { + FSTLog(@"%@ %p backoff", NSStringFromClass([self class]), (__bridge void *)self); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + FSTAssert(self.state == FSTStreamStateError, @"Should only perform backoff in an error case"); + self.state = FSTStreamStateBackoff; + + FSTWeakify(self); + [self.backoff backoffAndRunBlock:^{ + FSTStrongify(self); + [self resumeStartFromBackoffWithDelegate:delegate]; + }]; +} + +/** Resumes stream start after backing off. */ +- (void)resumeStartFromBackoffWithDelegate:(id)delegate { + if (self.state == FSTStreamStateStopped) { + // Streams can be stopped while waiting for backoff to complete. + return; + } + + // In order to have performed a backoff the stream must have been in an error state just prior + // to entering the backoff state. If we weren't stopped we must be in the backoff state. + FSTAssert(self.state == FSTStreamStateBackoff, @"State should still be backoff (was %ld)", + (long)self.state); + + // Momentarily set state to FSTStreamStateInitial as `start` expects it. + self.state = FSTStreamStateInitial; + [self startWithDelegate:delegate]; + FSTAssert([self isStarted], @"Stream should have started."); +} + +/** + * Can be overridden to perform additional cleanup before the stream is closed. Calling + * [super tearDown] is not required. + */ +- (void)tearDown { +} + +/** + * Closes the stream and cleans up as necessary: + * + * * closes the underlying GRPC stream; + * * calls the onClose handler with the given 'error'; + * * sets internal stream state to 'finalState'; + * * adjusts the backoff timer based on the error + * + * A new stream can be opened by calling `start` unless `finalState` is set to + * `FSTStreamStateStopped`. + * + * @param finalState the intended state of the stream after closing. + * @param error the NSError the connection was closed with. + */ +- (void)closeWithFinalState:(FSTStreamState)finalState error:(nullable NSError *)error { + FSTAssert(finalState == FSTStreamStateError || error == nil, + @"Can't provide an error when not in an error state."); + + [self.workerDispatchQueue verifyIsCurrentQueue]; + [self cancelIdleCheck]; + + if (finalState != FSTStreamStateError) { + // If this is an intentional close ensure we don't delay our next connection attempt. + [self.backoff reset]; + } else if (error != nil && error.code == FIRFirestoreErrorCodeResourceExhausted) { + FSTLog(@"%@ %p Using maximum backoff delay to prevent overloading the backend.", [self class], + (__bridge void *)self); + [self.backoff resetToMax]; + } + + [self tearDown]; + + if (self.requestsWriter) { + // Clean up the underlying RPC. If this close: is in response to an error, don't attempt to + // call half-close to avoid secondary failures. + if (finalState != FSTStreamStateError) { + FSTLog(@"%@ %p Closing stream client-side", [self class], (__bridge void *)self); + @synchronized(self.requestsWriter) { + [self.requestsWriter finishWithError:nil]; + } + } + _requestsWriter = nil; + } + + // This state must be assigned before calling `notifyStreamInterrupted` to allow the callback to + // inhibit backoff or otherwise manipulate the state in its non-started state. + self.state = finalState; + + [self.callbackFilter suppressCallbacks]; + _callbackFilter = nil; + + // Clean up remaining state. + _messageReceived = NO; + _rpc = nil; + + // If the caller explicitly requested a stream stop, don't notify them of a closing stream (it + // could trigger undesirable recovery logic, etc.). + if (finalState != FSTStreamStateStopped) { + [self notifyStreamInterruptedWithError:error]; + } + + // Clear the delegates to avoid any possible bleed through of events from GRPC. + _delegate = nil; +} + +- (void)stop { + FSTLog(@"%@ %p stop", NSStringFromClass([self class]), (__bridge void *)self); + if ([self isStarted]) { + [self closeWithFinalState:FSTStreamStateStopped error:nil]; + } +} + +- (void)inhibitBackoff { + FSTAssert(![self isStarted], @"Can only inhibit backoff after an error (was %ld)", + (long)self.state); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + // Clear the error condition. + self.state = FSTStreamStateInitial; + [self.backoff reset]; +} + +/** Called by the idle timer when the stream should close due to inactivity. */ +- (void)handleIdleCloseTimer { + [self.workerDispatchQueue verifyIsCurrentQueue]; + if (self.state == FSTStreamStateOpen && [self isIdle]) { + // When timing out an idle stream there's no reason to force the stream into backoff when + // it restarts so set the stream state to Initial instead of Error. + [self closeWithFinalState:FSTStreamStateInitial error:nil]; + } +} + +- (void)markIdle { + [self.workerDispatchQueue verifyIsCurrentQueue]; + if (self.state == FSTStreamStateOpen) { + self.idle = YES; + [self.workerDispatchQueue dispatchAfterDelay:kIdleTimeout + block:^() { + [self handleIdleCloseTimer]; + }]; + } +} + +- (void)cancelIdleCheck { + [self.workerDispatchQueue verifyIsCurrentQueue]; + self.idle = NO; +} + +/** + * Parses a protocol buffer response from the server. If the message fails to parse, generates + * an error and closes the stream. + * + * @param protoClass A protocol buffer message class object, that responds to parseFromData:error:. + * @param data The bytes in the response as returned from GRPC. + * @return An instance of the protocol buffer message, parsed from the data if parsing was + * successful, or nil otherwise. + */ +- (nullable id)parseProto:(Class)protoClass data:(NSData *)data error:(NSError **)error { + NSError *parseError; + id parsed = [protoClass parseFromData:data error:&parseError]; + if (parsed) { + *error = nil; + return parsed; + } else { + NSDictionary *info = @{ + NSLocalizedDescriptionKey : @"Unable to parse response from the server", + NSUnderlyingErrorKey : parseError, + @"Expected class" : protoClass, + @"Received value" : data, + }; + *error = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeInternal + userInfo:info]; + return nil; + } +} + +/** + * Writes a request proto into the stream. + */ +- (void)writeRequest:(GPBMessage *)request { + NSData *data = [request data]; + + [self cancelIdleCheck]; + + FSTBufferedWriter *requestsWriter = self.requestsWriter; + @synchronized(requestsWriter) { + [requestsWriter writeValue:data]; + } +} + +#pragma mark Template methods for subclasses + +/** + * Called by the stream after the stream has opened. + * + * Subclasses should relay to their stream-specific delegate. Calling [super notifyStreamOpen] is + * not required. + */ +- (void)notifyStreamOpen { +} + +/** + * Called by the stream after the stream has been unexpectedly interrupted, either due to an error + * or due to idleness. + * + * Subclasses should relay to their stream-specific delegate. Calling [super + * notifyStreamInterrupted] is not required. + */ +- (void)notifyStreamInterruptedWithError:(nullable NSError *)error { +} + +/** + * Called by the stream for each incoming protocol message coming from the server. + * + * Subclasses should implement this to deserialize the value and relay to their stream-specific + * delegate, if appropriate. Calling [super handleStreamMessage] is not required. + */ +- (void)handleStreamMessage:(id)value { +} + +/** + * Called by the stream when the underlying RPC has been closed for whatever reason. + */ +- (void)handleStreamClose:(nullable NSError *)error { + FSTLog(@"%@ %p close: %@", NSStringFromClass([self class]), (__bridge void *)self, error); + + if (![self isStarted]) { // The stream could have already been closed by the idle close timer. + FSTLog(@"%@ Ignoring server close for already closed stream.", NSStringFromClass([self class])); + return; + } + + // In theory the stream could close cleanly, however, in our current model we never expect this + // to happen because if we stop a stream ourselves, this callback will never be called. To + // prevent cases where we retry without a backoff accidentally, we set the stream to error + // in all cases. + [self closeWithFinalState:FSTStreamStateError error:error]; +} + +#pragma mark GRXWriteable implementation +// The GRXWriteable implementation defines the receive side of the RPC stream. + +/** + * Called by GRPC when it publishes a value. It is called from GRPC's own queue so we immediately + * redispatch back onto our own worker queue. + */ +- (void)writeValue:(id)value __used { + // TODO(mcg): remove the double-dispatch once GRPCCall at head is released. + // Once released we can set the responseDispatchQueue property on the GRPCCall and then this + // method can call handleStreamMessage directly. + FSTWeakify(self); + [self.workerDispatchQueue dispatchAsync:^{ + FSTStrongify(self); + if (![self isStarted]) { + FSTLog(@"%@ Ignoring stream message from inactive stream.", NSStringFromClass([self class])); + } + + if (!self.messageReceived) { + self.messageReceived = YES; + if ([FIRFirestore isLoggingEnabled]) { + FSTLog(@"%@ %p headers (whitelisted): %@", NSStringFromClass([self class]), + (__bridge void *)self, + [FSTDatastore extractWhiteListedHeaders:self.rpc.responseHeaders]); + } + } + NSError *error; + id proto = [self parseProto:self.responseMessageClass data:value error:&error]; + if (proto) { + [self handleStreamMessage:proto]; + } else { + [_rpc finishWithError:error]; + } + }]; +} + +/** + * Called by GRPC when it closed the stream with an error representing the final state of the + * stream. + * + * Do not call directly, since it dispatches via the worker queue. Call handleStreamClose to + * directly inform stream-specific logic, or call stop to tear down the stream. + */ +- (void)writesFinishedWithError:(nullable NSError *)error __used { + error = [FSTDatastore firestoreErrorForError:error]; + FSTWeakify(self); + [self.workerDispatchQueue dispatchAsync:^{ + FSTStrongify(self); + if (!self || self.state == FSTStreamStateStopped) { + return; + } + [self handleStreamClose:error]; + }]; +} + +@end + +#pragma mark - FSTWatchStream + +@interface FSTWatchStream () + +@property(nonatomic, strong, readonly) FSTSerializerBeta *serializer; + +@end + +@implementation FSTWatchStream + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + serializer:(FSTSerializerBeta *)serializer { + self = [super initWithDatabase:database + workerDispatchQueue:workerDispatchQueue + credentials:credentials + responseMessageClass:[GCFSListenResponse class]]; + if (self) { + _serializer = serializer; + } + return self; +} + +- (GRPCCall *)createRPCWithRequestsWriter:(GRXWriter *)requestsWriter { + return [[GRPCCall alloc] initWithHost:self.databaseInfo.host + path:@"/google.firestore.v1beta1.Firestore/Listen" + requestsWriter:requestsWriter]; +} + +- (void)notifyStreamOpen { + [self.delegate watchStreamDidOpen]; +} + +- (void)notifyStreamInterruptedWithError:(nullable NSError *)error { + id delegate = self.delegate; + self.delegate = nil; + [delegate watchStreamWasInterruptedWithError:error]; +} + +- (void)watchQuery:(FSTQueryData *)query { + FSTAssert([self isOpen], @"Not yet open"); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + GCFSListenRequest *request = [GCFSListenRequest message]; + request.database = [_serializer encodedDatabaseID]; + request.addTarget = [_serializer encodedTarget:query]; + request.labels = [_serializer encodedListenRequestLabelsForQueryData:query]; + + FSTLog(@"FSTWatchStream %p watch: %@", (__bridge void *)self, request); + [self writeRequest:request]; +} + +- (void)unwatchTargetID:(FSTTargetID)targetID { + FSTAssert([self isOpen], @"Not yet open"); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + GCFSListenRequest *request = [GCFSListenRequest message]; + request.database = [_serializer encodedDatabaseID]; + request.removeTarget = targetID; + + FSTLog(@"FSTWatchStream %p unwatch: %@", (__bridge void *)self, request); + [self writeRequest:request]; +} + +/** + * Receives an inbound message from GRPC, deserializes, and then passes that on to the delegate's + * watchStreamDidChange:snapshotVersion: callback. + */ +- (void)handleStreamMessage:(GCFSListenResponse *)proto { + FSTLog(@"FSTWatchStream %p response: %@", (__bridge void *)self, proto); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + // A successful response means the stream is healthy. + [self.backoff reset]; + + FSTWatchChange *change = [_serializer decodedWatchChange:proto]; + FSTSnapshotVersion *snap = [_serializer versionFromListenResponse:proto]; + [self.delegate watchStreamDidChange:change snapshotVersion:snap]; +} + +@end + +#pragma mark - FSTWriteStream + +@interface FSTWriteStream () + +@property(nonatomic, strong, readonly) FSTSerializerBeta *serializer; + +@end + +@implementation FSTWriteStream + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + serializer:(FSTSerializerBeta *)serializer { + self = [super initWithDatabase:database + workerDispatchQueue:workerDispatchQueue + credentials:credentials + responseMessageClass:[GCFSWriteResponse class]]; + if (self) { + _serializer = serializer; + } + return self; +} + +- (GRPCCall *)createRPCWithRequestsWriter:(GRXWriter *)requestsWriter { + return [[GRPCCall alloc] initWithHost:self.databaseInfo.host + path:@"/google.firestore.v1beta1.Firestore/Write" + requestsWriter:requestsWriter]; +} + +- (void)startWithDelegate:(id)delegate { + self.handshakeComplete = NO; + [super startWithDelegate:delegate]; +} + +- (void)notifyStreamOpen { + [self.delegate writeStreamDidOpen]; +} + +- (void)notifyStreamInterruptedWithError:(nullable NSError *)error { + id delegate = self.delegate; + self.delegate = nil; + [delegate writeStreamWasInterruptedWithError:error]; +} + +- (void)tearDown { + if ([self isHandshakeComplete]) { + // Send an empty write request to the backend to indicate imminent stream closure. This allows + // the backend to clean up resources. + [self writeMutations:@[]]; + } +} + +- (void)writeHandshake { + // The initial request cannot contain mutations, but must contain a projectID. + FSTAssert([self isOpen], @"Not yet open"); + FSTAssert(!self.handshakeComplete, @"Handshake sent out of turn"); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + GCFSWriteRequest *request = [GCFSWriteRequest message]; + request.database = [_serializer encodedDatabaseID]; + // TODO(dimond): Support stream resumption. We intentionally do not set the stream token on the + // handshake, ignoring any stream token we might have. + + FSTLog(@"FSTWriteStream %p initial request: %@", (__bridge void *)self, request); + [self writeRequest:request]; +} + +- (void)writeMutations:(NSArray *)mutations { + FSTAssert([self isOpen], @"Not yet open"); + FSTAssert(self.handshakeComplete, @"Mutations sent out of turn"); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + NSMutableArray *protos = [NSMutableArray arrayWithCapacity:mutations.count]; + for (FSTMutation *mutation in mutations) { + [protos addObject:[_serializer encodedMutation:mutation]]; + }; + + GCFSWriteRequest *request = [GCFSWriteRequest message]; + request.writesArray = protos; + request.streamToken = self.lastStreamToken; + + FSTLog(@"FSTWriteStream %p mutation request: %@", (__bridge void *)self, request); + [self writeRequest:request]; +} + +/** + * Implements GRXWriteable to receive an inbound message from GRPC, deserialize, and then pass + * that on to the mutationResultsHandler. + */ +- (void)handleStreamMessage:(GCFSWriteResponse *)response { + FSTLog(@"FSTWriteStream %p response: %@", (__bridge void *)self, response); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + // Always capture the last stream token. + self.lastStreamToken = response.streamToken; + + if (!self.isHandshakeComplete) { + // The first response is the handshake response + self.handshakeComplete = YES; + + [self.delegate writeStreamDidCompleteHandshake]; + } else { + // A successful first write response means the stream is healthy. + // Note that we could consider a successful handshake healthy, however, the write itself + // might be causing an error we want to back off from. + [self.backoff reset]; + + FSTSnapshotVersion *commitVersion = [_serializer decodedVersion:response.commitTime]; + NSMutableArray *protos = response.writeResultsArray; + NSMutableArray *results = [NSMutableArray arrayWithCapacity:protos.count]; + for (GCFSWriteResult *proto in protos) { + [results addObject:[_serializer decodedMutationResult:proto]]; + }; + + [self.delegate writeStreamDidReceiveResponseWithVersion:commitVersion mutationResults:results]; + } +} + +@end diff --git a/Firestore/Source/Remote/FSTWatchChange.m b/Firestore/Source/Remote/FSTWatchChange.m deleted file mode 100644 index 926d027..0000000 --- a/Firestore/Source/Remote/FSTWatchChange.m +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Remote/FSTWatchChange.h" - -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Remote/FSTExistenceFilter.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTWatchChange -@end - -@implementation FSTDocumentWatchChange - -- (instancetype)initWithUpdatedTargetIDs:(NSArray *)updatedTargetIDs - removedTargetIDs:(NSArray *)removedTargetIDs - documentKey:(FSTDocumentKey *)documentKey - document:(nullable FSTMaybeDocument *)document { - self = [super init]; - if (self) { - _updatedTargetIDs = updatedTargetIDs; - _removedTargetIDs = removedTargetIDs; - _documentKey = documentKey; - _document = document; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isMemberOfClass:[FSTDocumentWatchChange class]]) { - return NO; - } - - FSTDocumentWatchChange *otherChange = (FSTDocumentWatchChange *)other; - return [_updatedTargetIDs isEqual:otherChange.updatedTargetIDs] && - [_removedTargetIDs isEqual:otherChange.removedTargetIDs] && - [_documentKey isEqual:otherChange.documentKey] && - (_document == otherChange.document || [_document isEqual:otherChange.document]); -} - -- (NSUInteger)hash { - NSUInteger hash = self.updatedTargetIDs.hash; - hash = hash * 31 + self.removedTargetIDs.hash; - hash = hash * 31 + self.documentKey.hash; - hash = hash * 31 + self.document.hash; - return hash; -} - -@end - -@interface FSTExistenceFilterWatchChange () - -- (instancetype)initWithFilter:(FSTExistenceFilter *)filter - targetID:(FSTTargetID)targetID NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FSTExistenceFilterWatchChange - -+ (instancetype)changeWithFilter:(FSTExistenceFilter *)filter targetID:(FSTTargetID)targetID { - return [[FSTExistenceFilterWatchChange alloc] initWithFilter:filter targetID:targetID]; -} - -- (instancetype)initWithFilter:(FSTExistenceFilter *)filter targetID:(FSTTargetID)targetID { - self = [super init]; - if (self) { - _filter = filter; - _targetID = targetID; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isMemberOfClass:[FSTExistenceFilterWatchChange class]]) { - return NO; - } - - FSTExistenceFilterWatchChange *otherChange = (FSTExistenceFilterWatchChange *)other; - return [_filter isEqual:otherChange->_filter] && _targetID == otherChange->_targetID; -} - -- (NSUInteger)hash { - return self.filter.hash; -} - -@end - -@implementation FSTWatchTargetChange - -- (instancetype)initWithState:(FSTWatchTargetChangeState)state - targetIDs:(NSArray *)targetIDs - resumeToken:(NSData *)resumeToken - cause:(nullable NSError *)cause { - self = [super init]; - if (self) { - _state = state; - _targetIDs = targetIDs; - _resumeToken = resumeToken; - _cause = cause; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isMemberOfClass:[FSTWatchTargetChange class]]) { - return NO; - } - - FSTWatchTargetChange *otherChange = (FSTWatchTargetChange *)other; - return _state == otherChange->_state && [_targetIDs isEqual:otherChange->_targetIDs] && - [_resumeToken isEqual:otherChange->_resumeToken] && - (_cause == otherChange->_cause || [_cause isEqual:otherChange->_cause]); -} - -- (NSUInteger)hash { - NSUInteger hash = (NSUInteger)self.state; - - hash = hash * 31 + self.targetIDs.hash; - hash = hash * 31 + self.resumeToken.hash; - hash = hash * 31 + self.cause.hash; - return hash; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTWatchChange.mm b/Firestore/Source/Remote/FSTWatchChange.mm new file mode 100644 index 0000000..926d027 --- /dev/null +++ b/Firestore/Source/Remote/FSTWatchChange.mm @@ -0,0 +1,150 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Remote/FSTWatchChange.h" + +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Remote/FSTExistenceFilter.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTWatchChange +@end + +@implementation FSTDocumentWatchChange + +- (instancetype)initWithUpdatedTargetIDs:(NSArray *)updatedTargetIDs + removedTargetIDs:(NSArray *)removedTargetIDs + documentKey:(FSTDocumentKey *)documentKey + document:(nullable FSTMaybeDocument *)document { + self = [super init]; + if (self) { + _updatedTargetIDs = updatedTargetIDs; + _removedTargetIDs = removedTargetIDs; + _documentKey = documentKey; + _document = document; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTDocumentWatchChange class]]) { + return NO; + } + + FSTDocumentWatchChange *otherChange = (FSTDocumentWatchChange *)other; + return [_updatedTargetIDs isEqual:otherChange.updatedTargetIDs] && + [_removedTargetIDs isEqual:otherChange.removedTargetIDs] && + [_documentKey isEqual:otherChange.documentKey] && + (_document == otherChange.document || [_document isEqual:otherChange.document]); +} + +- (NSUInteger)hash { + NSUInteger hash = self.updatedTargetIDs.hash; + hash = hash * 31 + self.removedTargetIDs.hash; + hash = hash * 31 + self.documentKey.hash; + hash = hash * 31 + self.document.hash; + return hash; +} + +@end + +@interface FSTExistenceFilterWatchChange () + +- (instancetype)initWithFilter:(FSTExistenceFilter *)filter + targetID:(FSTTargetID)targetID NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTExistenceFilterWatchChange + ++ (instancetype)changeWithFilter:(FSTExistenceFilter *)filter targetID:(FSTTargetID)targetID { + return [[FSTExistenceFilterWatchChange alloc] initWithFilter:filter targetID:targetID]; +} + +- (instancetype)initWithFilter:(FSTExistenceFilter *)filter targetID:(FSTTargetID)targetID { + self = [super init]; + if (self) { + _filter = filter; + _targetID = targetID; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTExistenceFilterWatchChange class]]) { + return NO; + } + + FSTExistenceFilterWatchChange *otherChange = (FSTExistenceFilterWatchChange *)other; + return [_filter isEqual:otherChange->_filter] && _targetID == otherChange->_targetID; +} + +- (NSUInteger)hash { + return self.filter.hash; +} + +@end + +@implementation FSTWatchTargetChange + +- (instancetype)initWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray *)targetIDs + resumeToken:(NSData *)resumeToken + cause:(nullable NSError *)cause { + self = [super init]; + if (self) { + _state = state; + _targetIDs = targetIDs; + _resumeToken = resumeToken; + _cause = cause; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTWatchTargetChange class]]) { + return NO; + } + + FSTWatchTargetChange *otherChange = (FSTWatchTargetChange *)other; + return _state == otherChange->_state && [_targetIDs isEqual:otherChange->_targetIDs] && + [_resumeToken isEqual:otherChange->_resumeToken] && + (_cause == otherChange->_cause || [_cause isEqual:otherChange->_cause]); +} + +- (NSUInteger)hash { + NSUInteger hash = (NSUInteger)self.state; + + hash = hash * 31 + self.targetIDs.hash; + hash = hash * 31 + self.resumeToken.hash; + hash = hash * 31 + self.cause.hash; + return hash; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTAsyncQueryListener.m b/Firestore/Source/Util/FSTAsyncQueryListener.m deleted file mode 100644 index d98e2dd..0000000 --- a/Firestore/Source/Util/FSTAsyncQueryListener.m +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Util/FSTAsyncQueryListener.h" - -#import "Firestore/Source/Util/FSTDispatchQueue.h" - -@implementation FSTAsyncQueryListener { - volatile BOOL _muted; - FSTViewSnapshotHandler _snapshotHandler; - FSTDispatchQueue *_dispatchQueue; -} - -- (instancetype)initWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue - snapshotHandler:(FSTViewSnapshotHandler)snapshotHandler { - if (self = [super init]) { - _dispatchQueue = dispatchQueue; - _snapshotHandler = snapshotHandler; - } - return self; -} - -- (FSTViewSnapshotHandler)asyncSnapshotHandler { - return ^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error) { - [_dispatchQueue dispatchAsync:^{ - if (!_muted) { - _snapshotHandler(snapshot, error); - } - }]; - }; -} - -- (void)mute { - _muted = true; -} - -@end diff --git a/Firestore/Source/Util/FSTAsyncQueryListener.mm b/Firestore/Source/Util/FSTAsyncQueryListener.mm new file mode 100644 index 0000000..d98e2dd --- /dev/null +++ b/Firestore/Source/Util/FSTAsyncQueryListener.mm @@ -0,0 +1,50 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Util/FSTAsyncQueryListener.h" + +#import "Firestore/Source/Util/FSTDispatchQueue.h" + +@implementation FSTAsyncQueryListener { + volatile BOOL _muted; + FSTViewSnapshotHandler _snapshotHandler; + FSTDispatchQueue *_dispatchQueue; +} + +- (instancetype)initWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue + snapshotHandler:(FSTViewSnapshotHandler)snapshotHandler { + if (self = [super init]) { + _dispatchQueue = dispatchQueue; + _snapshotHandler = snapshotHandler; + } + return self; +} + +- (FSTViewSnapshotHandler)asyncSnapshotHandler { + return ^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error) { + [_dispatchQueue dispatchAsync:^{ + if (!_muted) { + _snapshotHandler(snapshot, error); + } + }]; + }; +} + +- (void)mute { + _muted = true; +} + +@end diff --git a/Firestore/Source/Util/FSTDispatchQueue.m b/Firestore/Source/Util/FSTDispatchQueue.m deleted file mode 100644 index 6ce5d74..0000000 --- a/Firestore/Source/Util/FSTDispatchQueue.m +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTDispatchQueue.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTDispatchQueue () -- (instancetype)initWithQueue:(dispatch_queue_t)queue NS_DESIGNATED_INITIALIZER; -@end - -@implementation FSTDispatchQueue - -+ (instancetype)queueWith:(dispatch_queue_t)dispatchQueue { - return [[FSTDispatchQueue alloc] initWithQueue:dispatchQueue]; -} - -- (instancetype)initWithQueue:(dispatch_queue_t)queue { - if (self = [super init]) { - _queue = queue; - } - return self; -} - -- (void)verifyIsCurrentQueue { - FSTAssert([self onTargetQueue], - @"We are running on the wrong dispatch queue. Expected '%@' Actual: '%@'", - [self targetQueueLabel], [self currentQueueLabel]); -} - -- (void)dispatchAsync:(void (^)(void))block { - FSTAssert(![self onTargetQueue], - @"dispatchAsync called when we are already running on target dispatch queue '%@'", - [self targetQueueLabel]); - - dispatch_async(self.queue, block); -} - -- (void)dispatchAsyncAllowingSameQueue:(void (^)(void))block { - dispatch_async(self.queue, block); -} - -- (void)dispatchAfterDelay:(NSTimeInterval)delay block:(void (^)(void))block { - dispatch_time_t delayNs = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)); - dispatch_after(delayNs, self.queue, block); -} - -#pragma mark - Private Methods - -- (NSString *)currentQueueLabel { - return [NSString stringWithUTF8String:dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)]; -} - -- (NSString *)targetQueueLabel { - return [NSString stringWithUTF8String:dispatch_queue_get_label(self.queue)]; -} - -- (BOOL)onTargetQueue { - return [[self currentQueueLabel] isEqualToString:[self targetQueueLabel]]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTDispatchQueue.mm b/Firestore/Source/Util/FSTDispatchQueue.mm new file mode 100644 index 0000000..6ce5d74 --- /dev/null +++ b/Firestore/Source/Util/FSTDispatchQueue.mm @@ -0,0 +1,80 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTDispatchQueue.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDispatchQueue () +- (instancetype)initWithQueue:(dispatch_queue_t)queue NS_DESIGNATED_INITIALIZER; +@end + +@implementation FSTDispatchQueue + ++ (instancetype)queueWith:(dispatch_queue_t)dispatchQueue { + return [[FSTDispatchQueue alloc] initWithQueue:dispatchQueue]; +} + +- (instancetype)initWithQueue:(dispatch_queue_t)queue { + if (self = [super init]) { + _queue = queue; + } + return self; +} + +- (void)verifyIsCurrentQueue { + FSTAssert([self onTargetQueue], + @"We are running on the wrong dispatch queue. Expected '%@' Actual: '%@'", + [self targetQueueLabel], [self currentQueueLabel]); +} + +- (void)dispatchAsync:(void (^)(void))block { + FSTAssert(![self onTargetQueue], + @"dispatchAsync called when we are already running on target dispatch queue '%@'", + [self targetQueueLabel]); + + dispatch_async(self.queue, block); +} + +- (void)dispatchAsyncAllowingSameQueue:(void (^)(void))block { + dispatch_async(self.queue, block); +} + +- (void)dispatchAfterDelay:(NSTimeInterval)delay block:(void (^)(void))block { + dispatch_time_t delayNs = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)); + dispatch_after(delayNs, self.queue, block); +} + +#pragma mark - Private Methods + +- (NSString *)currentQueueLabel { + return [NSString stringWithUTF8String:dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)]; +} + +- (NSString *)targetQueueLabel { + return [NSString stringWithUTF8String:dispatch_queue_get_label(self.queue)]; +} + +- (BOOL)onTargetQueue { + return [[self currentQueueLabel] isEqualToString:[self targetQueueLabel]]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTLogger.h b/Firestore/Source/Util/FSTLogger.h index 699570a..c4e2b85 100644 --- a/Firestore/Source/Util/FSTLogger.h +++ b/Firestore/Source/Util/FSTLogger.h @@ -18,17 +18,9 @@ NS_ASSUME_NONNULL_BEGIN -#ifdef __cplusplus -extern "C" { -#endif - /** Logs to NSLog if [FIRFirestore isLoggingEnabled] is YES. */ void FSTLog(NSString *format, ...) NS_FORMAT_FUNCTION(1, 2); void FSTWarn(NSString *format, ...) NS_FORMAT_FUNCTION(1, 2); -#ifdef __cplusplus -} // extern "C" -#endif - NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTLogger.m b/Firestore/Source/Util/FSTLogger.m deleted file mode 100644 index f0081e0..0000000 --- a/Firestore/Source/Util/FSTLogger.m +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Util/FSTLogger.h" - -#import - -#import "Firestore/Source/API/FIRFirestore+Internal.h" - -NS_ASSUME_NONNULL_BEGIN - -void FSTLog(NSString *format, ...) { - if ([FIRFirestore isLoggingEnabled]) { - va_list args; - va_start(args, format); - FIRLogBasic(FIRLoggerLevelDebug, kFIRLoggerFirestore, @"I-FST000001", format, args); - va_end(args); - } -} - -void FSTWarn(NSString *format, ...) { - va_list args; - va_start(args, format); - FIRLogBasic(FIRLoggerLevelWarning, kFIRLoggerFirestore, @"I-FST000001", format, args); - va_end(args); -} - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTLogger.mm b/Firestore/Source/Util/FSTLogger.mm new file mode 100644 index 0000000..f0081e0 --- /dev/null +++ b/Firestore/Source/Util/FSTLogger.mm @@ -0,0 +1,41 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Util/FSTLogger.h" + +#import + +#import "Firestore/Source/API/FIRFirestore+Internal.h" + +NS_ASSUME_NONNULL_BEGIN + +void FSTLog(NSString *format, ...) { + if ([FIRFirestore isLoggingEnabled]) { + va_list args; + va_start(args, format); + FIRLogBasic(FIRLoggerLevelDebug, kFIRLoggerFirestore, @"I-FST000001", format, args); + va_end(args); + } +} + +void FSTWarn(NSString *format, ...) { + va_list args; + va_start(args, format); + FIRLogBasic(FIRLoggerLevelWarning, kFIRLoggerFirestore, @"I-FST000001", format, args); + va_end(args); +} + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTUsageValidation.h b/Firestore/Source/Util/FSTUsageValidation.h index 34a3d64..a80dafa 100644 --- a/Firestore/Source/Util/FSTUsageValidation.h +++ b/Firestore/Source/Util/FSTUsageValidation.h @@ -18,10 +18,6 @@ NS_ASSUME_NONNULL_BEGIN -#if __cplusplus -extern "C" { -#endif - /** Helper for creating a general exception for invalid usage of an API. */ NSException *FSTInvalidUsage(NSString *exceptionName, NSString *format, ...); @@ -46,8 +42,4 @@ NSException *FSTInvalidUsage(NSString *exceptionName, NSString *format, ...); @throw FSTInvalidUsage(@"FIRInvalidArgumentException", format, ##__VA_ARGS__); \ } while (0) -#if __cplusplus -} // extern "C" -#endif - NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTUsageValidation.m b/Firestore/Source/Util/FSTUsageValidation.m deleted file mode 100644 index 82128f4..0000000 --- a/Firestore/Source/Util/FSTUsageValidation.m +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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. - */ - -#include - -NS_ASSUME_NONNULL_BEGIN - -NSException *FSTInvalidUsage(NSString *exceptionName, NSString *format, ...) { - va_list arg_list; - va_start(arg_list, format); - NSString *formattedString = [[NSString alloc] initWithFormat:format arguments:arg_list]; - va_end(arg_list); - - return [[NSException alloc] initWithName:exceptionName reason:formattedString userInfo:nil]; -} - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTUsageValidation.mm b/Firestore/Source/Util/FSTUsageValidation.mm new file mode 100644 index 0000000..82128f4 --- /dev/null +++ b/Firestore/Source/Util/FSTUsageValidation.mm @@ -0,0 +1,30 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#include + +NS_ASSUME_NONNULL_BEGIN + +NSException *FSTInvalidUsage(NSString *exceptionName, NSString *format, ...) { + va_list arg_list; + va_start(arg_list, format); + NSString *formattedString = [[NSString alloc] initWithFormat:format arguments:arg_list]; + va_end(arg_list); + + return [[NSException alloc] initWithName:exceptionName reason:formattedString userInfo:nil]; +} + +NS_ASSUME_NONNULL_END -- cgit v1.2.3