Pārlūkot izejas kodu

Rewrite local store tests in C++ (#4421)

* Move FSTLocalStoreTests to core/test

* Add testutils for constructing TransformMutations

* Rewrite local store tests in C++
Gil 6 gadi atpakaļ
vecāks
revīzija
c2fa88e3b0

+ 44 - 44
Firestore/Example/Firestore.xcodeproj/project.pbxproj

@@ -24,6 +24,7 @@
 		041CF73F67F6A22BF317625A /* FIRTimestampTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B65D34A7203C99090076A5E1 /* FIRTimestampTest.m */; };
 		0455FC6E2A281BD755FD933A /* precondition_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA5520A36E1F00BCEB75 /* precondition_test.cc */; };
 		047F5209AB055A884D795B8A /* field_filter_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = E8551D6C6FB0B1BACE9E5BAD /* field_filter_test.cc */; };
+		04887E378B39FB86A8A5B52B /* leveldb_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5FF903AEFA7A3284660FA4C5 /* leveldb_local_store_test.cc */; };
 		048A55EED3241ABC28752F86 /* memory_mutation_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 74FBEFA4FE4B12C435011763 /* memory_mutation_queue_test.cc */; };
 		0500A324CEC854C5B0CF364C /* FIRCollectionReferenceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E045202154AA00B64F25 /* FIRCollectionReferenceTests.mm */; };
 		051D3E20184AF195266EF678 /* no_document_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB6B908720322E8800CC290A /* no_document_test.cc */; };
@@ -60,6 +61,7 @@
 		0BC541D6457CBEDEA7BCF180 /* objc_type_traits_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2A0CF41BA5AED6049B0BEB2C /* objc_type_traits_apple_test.mm */; };
 		0BDC438E72D4DD44877BEDEE /* string_win_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 79507DF8378D3C42F5B36268 /* string_win_test.cc */; };
 		0C18678CE7E355B17C34F2EE /* grpc_stream_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6BBE42F21262CF400C6A53E /* grpc_stream_test.cc */; };
+		0C4219F37CC83614F1FD44ED /* local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 307FF03D0297024D59348EBD /* local_store_test.cc */; };
 		0CEE93636BA4852D3C5EC428 /* timestamp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ABF6506B201131F8005F2C74 /* timestamp_test.cc */; };
 		0D2D25522A94AA8195907870 /* status.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9920B89AAC00B5BCE7 /* status.pb.cc */; };
 		0D88B4CB916A4752B08E5B42 /* query_listener_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7C3F995E040E9E9C5E8514BB /* query_listener_test.cc */; };
@@ -73,6 +75,7 @@
 		0F99BB63CE5B3CFE35F9027E /* event_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6F57521E161450FAF89075ED /* event_manager_test.cc */; };
 		0FA4D5601BE9F0CB5EC2882C /* local_serializer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F8043813A5D16963EC02B182 /* local_serializer_test.cc */; };
 		0FBDD5991E8F6CD5F8542474 /* latlng.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9220B89AAC00B5BCE7 /* latlng.pb.cc */; };
+		1029F0461945A444FCB523B3 /* leveldb_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5FF903AEFA7A3284660FA4C5 /* leveldb_local_store_test.cc */; };
 		1115DB1F1DCE93B63E03BA8C /* comparison_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 548DB928200D59F600E00ABC /* comparison_test.cc */; };
 		113190791F42202FDE1ABC14 /* FIRQuerySnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04F202154AA00B64F25 /* FIRQuerySnapshotTests.mm */; };
 		1145D70555D8CDC75183A88C /* leveldb_mutation_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5C7942B6244F4C416B11B86C /* leveldb_mutation_queue_test.cc */; };
@@ -129,6 +132,7 @@
 		1C79AE3FBFC91800E30D092C /* CodableIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 124C932B22C1642C00CA8C2D /* CodableIntegrationTests.swift */; };
 		1CAA9012B25F975D445D5978 /* strerror_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 358C3B5FE573B1D60A4F7592 /* strerror_test.cc */; };
 		1CB8AEFBF3E9565FF9955B50 /* async_queue_libdispatch_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4680208EA0BE00554BA2 /* async_queue_libdispatch_test.mm */; };
+		1CC56DCA513B98CE39A6ED45 /* memory_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F6CA0C5638AB6627CB5B4CF4 /* memory_local_store_test.cc */; };
 		1CC9BABDD52B2A1E37E2698D /* mutation_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = C8522DE226C467C54E6788D8 /* mutation_test.cc */; };
 		1D618761796DE311A1707AA2 /* database_id_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB71064B201FA60300344F18 /* database_id_test.cc */; };
 		1D71CA6BBA1E3433F243188E /* common.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D221C2DDC800EFB9CC /* common.pb.cc */; };
@@ -148,7 +152,6 @@
 		20814A477D00EA11D0E76631 /* FIRDocumentSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04B202154AA00B64F25 /* FIRDocumentSnapshotTests.mm */; };
 		208491E14A9E478B9E0FABF7 /* index_free_query_engine_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 299752013F200FE5BAB1555B /* index_free_query_engine_test.cc */; };
 		20A26E9D0336F7F32A098D05 /* Pods_Firestore_IntegrationTests_tvOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2220F583583EFC28DE792ABE /* Pods_Firestore_IntegrationTests_tvOS.framework */; };
-		20BBEADB4725D48BB27548E9 /* FSTLevelDBLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E08F2021552B00B64F25 /* FSTLevelDBLocalStoreTests.mm */; };
 		21836C4D9D48F962E7A3A244 /* ordered_code_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380D03201BC6E400D97691 /* ordered_code_test.cc */; };
 		21A2A881F71CB825299DF06E /* hard_assert_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 444B7AB3F5A2929070CB1363 /* hard_assert_test.cc */; };
 		21C17F15579341289AD01051 /* persistence_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 9113B6F513D0473AEABBAF1F /* persistence_testing.cc */; };
@@ -236,10 +239,10 @@
 		37286D731E432CB873354357 /* remote_event_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 584AE2C37A55B408541A6FF3 /* remote_event_test.cc */; };
 		37C4BF11C8B2B8B54B5ED138 /* string_apple_benchmark.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4C73C0CC6F62A90D8573F383 /* string_apple_benchmark.mm */; };
 		37EC6C6EA9169BB99078CA96 /* reference_set_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 132E32997D781B896672D30A /* reference_set_test.cc */; };
+		380A137B785A5A6991BEDF4B /* leveldb_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5FF903AEFA7A3284660FA4C5 /* leveldb_local_store_test.cc */; };
 		38208AC761FF994BA69822BE /* async_queue_std_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4681208EA0BE00554BA2 /* async_queue_std_test.cc */; };
 		3887E1635B31DCD7BC0922BD /* existence_filter_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */; };
 		392F527F144BADDAC69C5485 /* string_format_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54131E9620ADE678001DF3FF /* string_format_test.cc */; };
-		3958F87E768E5CF40B87EF90 /* FSTMemoryLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0882021552A00B64F25 /* FSTMemoryLocalStoreTests.mm */; };
 		396F03881A10FD54AEB71D06 /* fake_credentials_provider.cc in Sources */ = {isa = PBXBuildFile; fileRef = B60894F62170207100EBC644 /* fake_credentials_provider.cc */; };
 		3987A3E8534BAA496D966735 /* memory_index_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = DB5A1E760451189DA36028B3 /* memory_index_manager_test.cc */; };
 		39CDC9EC5FD2E891D6D49151 /* secure_random_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54740A531FC913E500713A1A /* secure_random_test.cc */; };
@@ -248,13 +251,11 @@
 		3A8C29BF47A62B7BADCBA6F5 /* empty_credentials_provider_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB38D93620239689000A432D /* empty_credentials_provider_test.cc */; };
 		3ABF84FC618016CA6E1D3C03 /* leveldb_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 332485C4DCC6BA0DBB5E31B7 /* leveldb_util_test.cc */; };
 		3AC147E153D4A535B71C519E /* sorted_set_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA4C20A36DBB00BCEB75 /* sorted_set_test.cc */; };
-		3AFEB9ED387C59DEF6B32D5F /* FSTMemoryLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0882021552A00B64F25 /* FSTMemoryLocalStoreTests.mm */; };
 		3B1E27D951407FD237E64D07 /* FirestoreEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1235769422B86E65007DDFA9 /* FirestoreEncoderTests.swift */; };
 		3B23E21D5D7ACF54EBD8CF67 /* memory_lru_garbage_collector_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 9765D47FA12FA283F4EFAD02 /* memory_lru_garbage_collector_test.cc */; };
 		3B256CCF6AEEE12E22F16BB8 /* hashing_test_apple.mm in Sources */ = {isa = PBXBuildFile; fileRef = B69CF3F02227386500B281C8 /* hashing_test_apple.mm */; };
 		3B37BD3C13A66625EC82CF77 /* hard_assert_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 444B7AB3F5A2929070CB1363 /* hard_assert_test.cc */; };
 		3B47CC43DBA24434E215B8ED /* memory_index_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = DB5A1E760451189DA36028B3 /* memory_index_manager_test.cc */; };
-		3B47E82ED2A3C59AB5002640 /* FSTMemoryLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0882021552A00B64F25 /* FSTMemoryLocalStoreTests.mm */; };
 		3B843E4C1F3A182900548890 /* remote_store_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */; };
 		3BA4EEA6153B3833F86B8104 /* writer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BC3C788D290A935C353CEAA1 /* writer_test.cc */; };
 		3BAFCABA851AE1865D904323 /* to_string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B696858D2214B53900271095 /* to_string_test.cc */; };
@@ -273,7 +274,6 @@
 		401BBE4D4572EEBAA80E0B89 /* field_filter_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = E8551D6C6FB0B1BACE9E5BAD /* field_filter_test.cc */; };
 		40431BF2A368D0C891229F6E /* FSTMemorySpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02F20213FFC00B64F25 /* FSTMemorySpecTests.mm */; };
 		409C0F2BFC2E1BECFFAC4D32 /* testutil.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54A0352820A3B3BD003E0143 /* testutil.cc */; };
-		40A8F6C6A3670663D6520644 /* FSTMemoryLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0882021552A00B64F25 /* FSTMemoryLocalStoreTests.mm */; };
 		4173B61CB74EB4CD1D89EE68 /* latlng.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9220B89AAC00B5BCE7 /* latlng.pb.cc */; };
 		4194B7BB8B0352E1AC5D69B9 /* precondition_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA5520A36E1F00BCEB75 /* precondition_test.cc */; };
 		41EAC526C543064B8F3F7EDA /* field_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2AD2023DDB20028D6BE /* field_path_test.cc */; };
@@ -301,6 +301,7 @@
 		485CBA9F99771437BA1CB401 /* event_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6F57521E161450FAF89075ED /* event_manager_test.cc */; };
 		489D672CAA09B9BC66798E9F /* status.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9920B89AAC00B5BCE7 /* status.pb.cc */; };
 		48D1B38B93D34F1B82320577 /* view_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = A5466E7809AD2871FFDE6C76 /* view_testing.cc */; };
+		49774EBBC8496FE1E43AEE29 /* memory_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F6CA0C5638AB6627CB5B4CF4 /* memory_local_store_test.cc */; };
 		49794806F3D5052E5F61A40D /* http.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9720B89AAC00B5BCE7 /* http.pb.cc */; };
 		498A45B1EEBAC97A1C547BAC /* grpc_unary_call_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D964942163E63900EB9CFB /* grpc_unary_call_test.cc */; };
 		49C04B97AB282FFA82FD98CD /* latlng.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9220B89AAC00B5BCE7 /* latlng.pb.cc */; };
@@ -340,7 +341,6 @@
 		4FAD8823DC37B9CA24379E85 /* leveldb_mutation_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5C7942B6244F4C416B11B86C /* leveldb_mutation_queue_test.cc */; };
 		50454F81EC4584D4EB5F5ED5 /* serializer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 61F72C5520BC48FD001A68CB /* serializer_test.cc */; };
 		518BF03D57FBAD7C632D18F8 /* FIRQueryUnitTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = FF73B39D04D1760190E6B84A /* FIRQueryUnitTests.mm */; };
-		5210694AA6274DEDB3AF1177 /* FSTLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0832021552A00B64F25 /* FSTLocalStoreTests.mm */; };
 		52967C3DD7896BFA48840488 /* byte_string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5342CDDB137B4E93E2E85CCA /* byte_string_test.cc */; };
 		53AB47E44D897C81A94031F6 /* write.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D921C2DDC800EFB9CC /* write.pb.cc */; };
 		53BBB5CDED453F923ADD08D2 /* stream_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5B5414D28802BC76FDADABD6 /* stream_test.cc */; };
@@ -418,9 +418,6 @@
 		5492E07F202154EC00B64F25 /* FSTTransactionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E07B202154EB00B64F25 /* FSTTransactionTests.mm */; };
 		5492E080202154EC00B64F25 /* FSTSmokeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E07C202154EB00B64F25 /* FSTSmokeTests.mm */; };
 		5492E082202154EC00B64F25 /* FSTDatastoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E07E202154EC00B64F25 /* FSTDatastoreTests.mm */; };
-		5492E09D2021552D00B64F25 /* FSTLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0832021552A00B64F25 /* FSTLocalStoreTests.mm */; };
-		5492E0A12021552D00B64F25 /* FSTMemoryLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0882021552A00B64F25 /* FSTMemoryLocalStoreTests.mm */; };
-		5492E0A82021552D00B64F25 /* FSTLevelDBLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E08F2021552B00B64F25 /* FSTLevelDBLocalStoreTests.mm */; };
 		5493A424225F9990006DE7BA /* status_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5493A423225F9990006DE7BA /* status_apple_test.mm */; };
 		5493A425225F9990006DE7BA /* status_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5493A423225F9990006DE7BA /* status_apple_test.mm */; };
 		5493A426225F9990006DE7BA /* status_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5493A423225F9990006DE7BA /* status_apple_test.mm */; };
@@ -476,6 +473,7 @@
 		54EB764D202277B30088B8F3 /* array_sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54EB764C202277B30088B8F3 /* array_sorted_map_test.cc */; };
 		555161D6DB2DDC8B57F72A70 /* comparison_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 548DB928200D59F600E00ABC /* comparison_test.cc */; };
 		5556B648B9B1C2F79A706B4F /* common.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D221C2DDC800EFB9CC /* common.pb.cc */; };
+		55E84644D385A70E607A0F91 /* leveldb_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5FF903AEFA7A3284660FA4C5 /* leveldb_local_store_test.cc */; };
 		5686B35D611C1CFF6BFE7215 /* credentials_provider_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB38D9342023966E000A432D /* credentials_provider_test.cc */; };
 		568EC1C0F68A7B95E57C8C6C /* leveldb_key_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54995F6E205B6E12004EFFA0 /* leveldb_key_test.cc */; };
 		56D85436D3C864B804851B15 /* string_format_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9CFD366B783AE27B9E79EE7A /* string_format_apple_test.mm */; };
@@ -529,7 +527,6 @@
 		6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; };
 		60186935E36CF79E48A0B293 /* transform_operation_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 33607A3AE91548BD219EC9C6 /* transform_operation_test.cc */; };
 		60260A06871DCB1A5F3448D3 /* to_string_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B68B1E002213A764008977EF /* to_string_apple_test.mm */; };
-		602F6CF9FBF5DA712DA58B5F /* FSTMemoryLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0882021552A00B64F25 /* FSTMemoryLocalStoreTests.mm */; };
 		60985657831B8DDE2C65AC8B /* FIRFieldsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06A202154D500B64F25 /* FIRFieldsTests.mm */; };
 		60C72F86D2231B1B6592A5E6 /* filesystem_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F51859B394D01C0C507282F1 /* filesystem_test.cc */; };
 		6105A1365831B79A7DEEA4F3 /* path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 403DBF6EFB541DFD01582AA3 /* path_test.cc */; };
@@ -635,6 +632,7 @@
 		7A7EC216A0015D7620B4FF3E /* string_format_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9CFD366B783AE27B9E79EE7A /* string_format_apple_test.mm */; };
 		7A8DF35E7DB4278E67E6BDB3 /* snapshot_version_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ABA495B9202B7E79008A7851 /* snapshot_version_test.cc */; };
 		7AA8771FE1F048D012E5E317 /* query_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 731541602214AFFA0037F4DC /* query_spec_test.json */; };
+		7ACA8D967438B5CD9DA4C884 /* memory_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F6CA0C5638AB6627CB5B4CF4 /* memory_local_store_test.cc */; };
 		7AD020FC27493FF8E659436C /* existence_filter_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */; };
 		7B0EA399F899537ACCC84E53 /* string_format_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9CFD366B783AE27B9E79EE7A /* string_format_apple_test.mm */; };
 		7B0F073BDB6D0D6E542E23D4 /* query.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D621C2DDC800EFB9CC /* query.pb.cc */; };
@@ -671,6 +669,7 @@
 		8403D519C916C72B9C7F2FA1 /* FIRValidationTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06D202154D600B64F25 /* FIRValidationTests.mm */; };
 		8405FF2BFBB233031A887398 /* event_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6F57521E161450FAF89075ED /* event_manager_test.cc */; };
 		8413BD9958F6DD52C466D70F /* sorted_set_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA4C20A36DBB00BCEB75 /* sorted_set_test.cc */; };
+		843EE932AA9A8F43721F189E /* leveldb_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5FF903AEFA7A3284660FA4C5 /* leveldb_local_store_test.cc */; };
 		8460C97C9209D7DAF07090BD /* FIRFieldsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06A202154D500B64F25 /* FIRFieldsTests.mm */; };
 		851346D66DEC223E839E3AA9 /* memory_mutation_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 74FBEFA4FE4B12C435011763 /* memory_mutation_queue_test.cc */; };
 		85B8918FC8C5DC62482E39C3 /* resource_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2B02024FFD70028D6BE /* resource_path_test.cc */; };
@@ -709,7 +708,6 @@
 		9009C285F418EA80C46CF06B /* fake_target_metadata_provider.cc in Sources */ = {isa = PBXBuildFile; fileRef = 71140E5D09C6E76F7C71B2FC /* fake_target_metadata_provider.cc */; };
 		900D0E9F18CE3DB954DD0D1E /* async_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB467B208E9A8200554BA2 /* async_queue_test.cc */; };
 		9016EF298E41456060578C90 /* field_transform_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7515B47C92ABEEC66864B55C /* field_transform_test.cc */; };
-		904DA0AE915C02154AE547FC /* FSTLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0832021552A00B64F25 /* FSTLocalStoreTests.mm */; };
 		906DB5C85F57EFCBD2027E60 /* grpc_unary_call_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D964942163E63900EB9CFB /* grpc_unary_call_test.cc */; };
 		9073AFB51EA26A818C29131E /* no_document_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB6B908720322E8800CC290A /* no_document_test.cc */; };
 		90B9302B082E6252AF4E7DC7 /* leveldb_migrations_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EF83ACD5E1E9F25845A9ACED /* leveldb_migrations_test.cc */; };
@@ -719,11 +717,11 @@
 		913F6E57AF18F84C5ECFD414 /* lru_garbage_collector_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 277EAACC4DD7C21332E8496A /* lru_garbage_collector_test.cc */; };
 		918E0F94B77665AAB2F4ABFB /* leveldb_query_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BBE612A9FA0F53A9F8F02981 /* leveldb_query_cache_test.cc */; };
 		918E3D35942CE493690C45CE /* user_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB38D93220239654000A432D /* user_test.cc */; };
+		91AEFFEE35FBE15FEC42A1F4 /* memory_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F6CA0C5638AB6627CB5B4CF4 /* memory_local_store_test.cc */; };
 		920B6ABF76FDB3547F1CCD84 /* firestore.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D421C2DDC800EFB9CC /* firestore.pb.cc */; };
 		925BE64990449E93242A00A2 /* memory_mutation_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 74FBEFA4FE4B12C435011763 /* memory_mutation_queue_test.cc */; };
 		92D7081085679497DC112EDB /* persistence_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 9113B6F513D0473AEABBAF1F /* persistence_testing.cc */; };
 		92EFF0CC2993B43CBC7A61FF /* grpc_streaming_reader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D964922154AB8F00EB9CFB /* grpc_streaming_reader_test.cc */; };
-		9328C93759C78A10FDBF68E0 /* FSTLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0832021552A00B64F25 /* FSTLocalStoreTests.mm */; };
 		9382BE7190E7750EE7CCCE7C /* write_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A51F315EE100DD57A1 /* write_spec_test.json */; };
 		938F2AF6EC5CD0B839300DB0 /* query.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D621C2DDC800EFB9CC /* query.pb.cc */; };
 		939C898FE9D129F6A2EA259C /* FSTHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E03A2021401F00B64F25 /* FSTHelpers.mm */; };
@@ -770,7 +768,6 @@
 		A2346D231C8021698F0BDD13 /* fake_credentials_provider.cc in Sources */ = {isa = PBXBuildFile; fileRef = B60894F62170207100EBC644 /* fake_credentials_provider.cc */; };
 		A25FF76DEF542E01A2DF3B0E /* time_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5497CB76229DECDE000FB92F /* time_testing.cc */; };
 		A27096F764227BC73526FED3 /* leveldb_remote_document_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0840319686A223CC4AD3FAB1 /* leveldb_remote_document_cache_test.cc */; };
-		A2874B68641B7E9F17E66C50 /* FSTLevelDBLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E08F2021552B00B64F25 /* FSTLevelDBLocalStoreTests.mm */; };
 		A4237B1E55F30FE40DCB28C8 /* objc_compatibility_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B696858F221770F000271095 /* objc_compatibility_apple_test.mm */; };
 		A478FDD7C3F48FBFDDA7D8F5 /* leveldb_mutation_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5C7942B6244F4C416B11B86C /* leveldb_mutation_queue_test.cc */; };
 		A4AD189BDEF7A609953457A6 /* leveldb_key_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54995F6E205B6E12004EFFA0 /* leveldb_key_test.cc */; };
@@ -789,14 +786,13 @@
 		A6E236CE8B3A47BE32254436 /* array_sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54EB764C202277B30088B8F3 /* array_sorted_map_test.cc */; };
 		A7309DAD4A3B5334536ECA46 /* remote_event_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 584AE2C37A55B408541A6FF3 /* remote_event_test.cc */; };
 		A7399FB3BEC50BBFF08EC9BA /* mutation_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 3068AA9DFBBA86C1FE2A946E /* mutation_queue_test.cc */; };
-		A7470B7B2433264FFDCC7AC3 /* FSTLevelDBLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E08F2021552B00B64F25 /* FSTLevelDBLocalStoreTests.mm */; };
 		A78B38A9B29579342D48F6D5 /* grpc_stream_tester.cc in Sources */ = {isa = PBXBuildFile; fileRef = B1A7E1959AF8141FA7E6B888 /* grpc_stream_tester.cc */; };
 		A8AF92A35DFA30EEF9C27FB7 /* database_info_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB38D92E20235D22000A432D /* database_info_test.cc */; };
 		A8C9FF6D13E6C83D4AB54EA7 /* secure_random_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54740A531FC913E500713A1A /* secure_random_test.cc */; };
 		A907244EE37BC32C8D82948E /* FSTSpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E03020213FFC00B64F25 /* FSTSpecTests.mm */; };
 		A94884460990CD48CC0AD070 /* xcgmock_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4425A513895DEC60325A139E /* xcgmock_test.mm */; };
+		A97ED2BAAEDB0F765BBD5F98 /* local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 307FF03D0297024D59348EBD /* local_store_test.cc */; };
 		A9A9994FB8042838671E8506 /* view_snapshot_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CC572A9168BBEF7B83E4BBC5 /* view_snapshot_test.cc */; };
-		AA616D15FE0E7952787D6A59 /* FSTLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0832021552A00B64F25 /* FSTLocalStoreTests.mm */; };
 		AAA50E56B9A7EF3EFDA62172 /* create_noop_connectivity_monitor.cc in Sources */ = {isa = PBXBuildFile; fileRef = B67BF448216EB43000CA9097 /* create_noop_connectivity_monitor.cc */; };
 		AAC15E7CCAE79619B2ABB972 /* XCTestCase+Await.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0372021401E00B64F25 /* XCTestCase+Await.mm */; };
 		AAE47EEF4A19F0DC6E1847CE /* create_noop_connectivity_monitor.cc in Sources */ = {isa = PBXBuildFile; fileRef = B67BF448216EB43000CA9097 /* create_noop_connectivity_monitor.cc */; };
@@ -837,13 +833,12 @@
 		AF81B6A91987826426F18647 /* remote_store_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */; };
 		AFAC87E03815769ABB11746F /* append_only_list_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5477CDE922EE71C8000FCC1E /* append_only_list_test.cc */; };
 		AFB0ACCF130713DF6495E110 /* writer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BC3C788D290A935C353CEAA1 /* writer_test.cc */; };
-		AFE6FCE804A3B0217D3E2B54 /* FSTLevelDBLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E08F2021552B00B64F25 /* FSTLevelDBLocalStoreTests.mm */; };
 		B03F286F3AEC3781C386C646 /* FIRNumericTransformTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = D5B25E7E7D6873CBA4571841 /* FIRNumericTransformTests.mm */; };
 		B0B779769926304268200015 /* query_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 731541602214AFFA0037F4DC /* query_spec_test.json */; };
 		B0D10C3451EDFB016A6EAF03 /* writer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BC3C788D290A935C353CEAA1 /* writer_test.cc */; };
+		B15D17049414E2F5AE72C9C6 /* memory_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F6CA0C5638AB6627CB5B4CF4 /* memory_local_store_test.cc */; };
 		B192F30DECA8C28007F9B1D0 /* array_sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54EB764C202277B30088B8F3 /* array_sorted_map_test.cc */; };
 		B1A4D8A731EC0A0B16CC411A /* append_only_list_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5477CDE922EE71C8000FCC1E /* append_only_list_test.cc */; };
-		B1BD0A7EC48C7B7AF09437D5 /* FSTLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0832021552A00B64F25 /* FSTLocalStoreTests.mm */; };
 		B220E091D8F4E6DE1EA44F57 /* executor_libdispatch_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4689208F9B9100554BA2 /* executor_libdispatch_test.mm */; };
 		B235E260EA0DCB7BAC04F69B /* field_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2AD2023DDB20028D6BE /* field_path_test.cc */; };
 		B28ACC69EB1F232AE612E77B /* async_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 872C92ABD71B12784A1C5520 /* async_testing.cc */; };
@@ -928,6 +923,7 @@
 		C1E35BCE2CFF9B56C28545A2 /* Pods_Firestore_Example_tvOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62E103B28B48A81D682A0DE9 /* Pods_Firestore_Example_tvOS.framework */; };
 		C1F196EC5A7C112D2F7C7724 /* view_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = C7429071B33BDF80A7FA2F8A /* view_test.cc */; };
 		C21B3A1CCB3AD42E57EA14FC /* Pods_Firestore_Tests_macOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 759E964B6A03E6775C992710 /* Pods_Firestore_Tests_macOS.framework */; };
+		C23552A6D9FB0557962870C2 /* local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 307FF03D0297024D59348EBD /* local_store_test.cc */; };
 		C25F321AC9BF8D1CFC8543AF /* reference_set_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 132E32997D781B896672D30A /* reference_set_test.cc */; };
 		C2B25B816170505AC12E4D65 /* memory_query_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BB644F9A3FB7EE4EA704E56A /* memory_query_cache_test.cc */; };
 		C393D6984614D8E4D8C336A2 /* mutation.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE8220B89AAC00B5BCE7 /* mutation.pb.cc */; };
@@ -943,6 +939,7 @@
 		C5F1E2220E30ED5EAC9ABD9E /* mutation.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE8220B89AAC00B5BCE7 /* mutation.pb.cc */; };
 		C5FF700C4E992B9BCB1A630F /* memory_query_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BB644F9A3FB7EE4EA704E56A /* memory_query_cache_test.cc */; };
 		C663A8B74B57FD84717DEA21 /* delayed_constructor_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = D0A6E9136804A41CEC9D55D4 /* delayed_constructor_test.cc */; };
+		C6BF529243414C53DF5F1012 /* memory_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F6CA0C5638AB6627CB5B4CF4 /* memory_local_store_test.cc */; };
 		C71AD99EE8D176614E742FD7 /* string_apple_benchmark.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4C73C0CC6F62A90D8573F383 /* string_apple_benchmark.mm */; };
 		C7F174164D7C55E35A526009 /* resource_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2B02024FFD70028D6BE /* resource_path_test.cc */; };
 		C80B10E79CDD7EF7843C321E /* objc_type_traits_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2A0CF41BA5AED6049B0BEB2C /* objc_type_traits_apple_test.mm */; };
@@ -973,6 +970,7 @@
 		D148475D7F26BFEE6E05CCDA /* firebase_credentials_provider_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = ABC1D7E22023CDC500BA84F0 /* firebase_credentials_provider_test.mm */; };
 		D1690214781198276492442D /* event_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6F57521E161450FAF89075ED /* event_manager_test.cc */; };
 		D18DBCE3FE34BF5F14CF8ABD /* mutation_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = C8522DE226C467C54E6788D8 /* mutation_test.cc */; };
+		D21060F8115A5F48FC3BF335 /* local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 307FF03D0297024D59348EBD /* local_store_test.cc */; };
 		D22B96C19A0F3DE998D4320C /* delayed_constructor_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = D0A6E9136804A41CEC9D55D4 /* delayed_constructor_test.cc */; };
 		D377FA653FB976FB474D748C /* remote_event_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 584AE2C37A55B408541A6FF3 /* remote_event_test.cc */; };
 		D39F0216BF1EA8CD54C76CF8 /* FIRQueryUnitTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = FF73B39D04D1760190E6B84A /* FIRQueryUnitTests.mm */; };
@@ -1034,6 +1032,7 @@
 		DE435F33CE563E238868D318 /* query_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B9C261C26C5D311E1E3C0CB9 /* query_test.cc */; };
 		DE8C47B973526A20D88F785D /* token_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ABC1D7DF2023A3EF00BA84F0 /* token_test.cc */; };
 		DF27137C8EA7D095D68851B4 /* field_filter_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = E8551D6C6FB0B1BACE9E5BAD /* field_filter_test.cc */; };
+		DF4B3835C5AA4835C01CD255 /* local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 307FF03D0297024D59348EBD /* local_store_test.cc */; };
 		DF84C1193BA3A7761A3E8BDA /* leveldb_persistence_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5C2989FEC97E94ADD2A0B7E5 /* leveldb_persistence_test.cc */; };
 		E08297B35E12106105F448EB /* ordered_code_benchmark.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0473AFFF5567E667A125347B /* ordered_code_benchmark.cc */; };
 		E084921EFB7CF8CB1E950D6C /* iterator_adaptors_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54A0353420A3D8CB003E0143 /* iterator_adaptors_test.cc */; };
@@ -1057,6 +1056,7 @@
 		E500AB82DF2E7F3AFDB1AB3F /* to_string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B696858D2214B53900271095 /* to_string_test.cc */; };
 		E50187548B537DBCDBF7F9F0 /* string_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380CFC201A2EE200D97691 /* string_util_test.cc */; };
 		E51957EDECF741E1D3C3968A /* writer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BC3C788D290A935C353CEAA1 /* writer_test.cc */; };
+		E63342115B1DA65DB6F2C59A /* leveldb_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5FF903AEFA7A3284660FA4C5 /* leveldb_local_store_test.cc */; };
 		E6357221227031DD77EE5265 /* index_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AE4A9E38D65688EE000EE2A1 /* index_manager_test.cc */; };
 		E6688C8E524770A3C6EBB33A /* write_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A51F315EE100DD57A1 /* write_spec_test.json */; };
 		E6821243C510797EFFC7BCE2 /* grpc_streaming_reader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D964922154AB8F00EB9CFB /* grpc_streaming_reader_test.cc */; };
@@ -1089,6 +1089,7 @@
 		ED420D8F49DA5C41EEF93913 /* FIRSnapshotMetadataTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04D202154AA00B64F25 /* FIRSnapshotMetadataTests.mm */; };
 		ED4E2AC80CAF2A8FDDAC3DEE /* field_mask_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA5320A36E1F00BCEB75 /* field_mask_test.cc */; };
 		ED9DF1EB20025227B38736EC /* message_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CE37875365497FFA8687B745 /* message_test.cc */; };
+		EE470CC3C8FBCDA5F70A8466 /* local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 307FF03D0297024D59348EBD /* local_store_test.cc */; };
 		EE6DBFB0874A50578CE97A7F /* leveldb_remote_document_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0840319686A223CC4AD3FAB1 /* leveldb_remote_document_cache_test.cc */; };
 		EECC1EC64CA963A8376FA55C /* persistence_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 9113B6F513D0473AEABBAF1F /* persistence_testing.cc */; };
 		EF3518F84255BAF3EBD317F6 /* exponential_backoff_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D1B68420E2AB1A00B35856 /* exponential_backoff_test.cc */; };
@@ -1104,7 +1105,6 @@
 		F3261CBFC169DB375A0D9492 /* FSTMockDatastore.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02D20213FFC00B64F25 /* FSTMockDatastore.mm */; };
 		F386012CAB7F0C0A5564016A /* credentials_provider_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB38D9342023966E000A432D /* credentials_provider_test.cc */; };
 		F3F09BC931A717CEFF4E14B9 /* FIRFieldValueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04A202154AA00B64F25 /* FIRFieldValueTests.mm */; };
-		F46394FAA186BC6D19213B59 /* FSTLevelDBLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E08F2021552B00B64F25 /* FSTLevelDBLocalStoreTests.mm */; };
 		F481368DB694B3B4D0C8E4A2 /* query_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B9C261C26C5D311E1E3C0CB9 /* query_test.cc */; };
 		F4F00BF4E87D7F0F0F8831DB /* FSTEventAccumulator.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0392021401F00B64F25 /* FSTEventAccumulator.mm */; };
 		F4FAC5A7D40A0A9A3EA77998 /* FSTLevelDBSpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02C20213FFB00B64F25 /* FSTLevelDBSpecTests.mm */; };
@@ -1236,6 +1236,7 @@
 		2E48431B0EDA400BEA91D4AB /* Pods-Firestore_Tests_tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests_tvOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests_tvOS/Pods-Firestore_Tests_tvOS.debug.xcconfig"; sourceTree = "<group>"; };
 		2F901F31BC62444A476B779F /* Pods-Firestore_IntegrationTests_macOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests_macOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests_macOS/Pods-Firestore_IntegrationTests_macOS.debug.xcconfig"; sourceTree = "<group>"; };
 		3068AA9DFBBA86C1FE2A946E /* mutation_queue_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = mutation_queue_test.cc; sourceTree = "<group>"; };
+		307FF03D0297024D59348EBD /* local_store_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = local_store_test.cc; sourceTree = "<group>"; };
 		3167BD972EFF8EC636530E59 /* datastore_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = datastore_test.cc; sourceTree = "<group>"; };
 		332485C4DCC6BA0DBB5E31B7 /* leveldb_util_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_util_test.cc; sourceTree = "<group>"; };
 		33607A3AE91548BD219EC9C6 /* transform_operation_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = transform_operation_test.cc; sourceTree = "<group>"; };
@@ -1306,10 +1307,6 @@
 		5492E07B202154EB00B64F25 /* FSTTransactionTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTTransactionTests.mm; sourceTree = "<group>"; };
 		5492E07C202154EB00B64F25 /* FSTSmokeTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTSmokeTests.mm; sourceTree = "<group>"; };
 		5492E07E202154EC00B64F25 /* FSTDatastoreTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTDatastoreTests.mm; sourceTree = "<group>"; };
-		5492E0832021552A00B64F25 /* FSTLocalStoreTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLocalStoreTests.mm; sourceTree = "<group>"; };
-		5492E0882021552A00B64F25 /* FSTMemoryLocalStoreTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTMemoryLocalStoreTests.mm; sourceTree = "<group>"; };
-		5492E08F2021552B00B64F25 /* FSTLevelDBLocalStoreTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBLocalStoreTests.mm; sourceTree = "<group>"; };
-		5492E0912021552B00B64F25 /* FSTLocalStoreTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTLocalStoreTests.h; sourceTree = "<group>"; };
 		5493A423225F9990006DE7BA /* status_apple_test.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = status_apple_test.mm; sourceTree = "<group>"; };
 		5495EB022040E90200EBA509 /* CodableGeoPointTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodableGeoPointTests.swift; sourceTree = "<group>"; };
 		5497CB75229DECDE000FB92F /* time_testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = time_testing.h; sourceTree = "<group>"; };
@@ -1364,6 +1361,7 @@
 		5C7942B6244F4C416B11B86C /* leveldb_mutation_queue_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = leveldb_mutation_queue_test.cc; sourceTree = "<group>"; };
 		5CAE131920FFFED600BE9A4A /* Firestore_Benchmarks_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firestore_Benchmarks_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		5CAE131D20FFFED600BE9A4A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		5FF903AEFA7A3284660FA4C5 /* leveldb_local_store_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = leveldb_local_store_test.cc; sourceTree = "<group>"; };
 		6003F58A195388D20070C39A /* Firestore_Example_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Firestore_Example_iOS.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; };
@@ -1498,6 +1496,7 @@
 		BBE612A9FA0F53A9F8F02981 /* leveldb_query_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = leveldb_query_cache_test.cc; sourceTree = "<group>"; };
 		BC3C788D290A935C353CEAA1 /* writer_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; name = writer_test.cc; path = nanopb/writer_test.cc; sourceTree = "<group>"; };
 		BD01F0E43E4E2A07B8B05099 /* Pods-Firestore_Tests_macOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests_macOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests_macOS/Pods-Firestore_Tests_macOS.debug.xcconfig"; sourceTree = "<group>"; };
+		C0C7C8977C94F9F9AFA4DB00 /* local_store_test.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = local_store_test.h; sourceTree = "<group>"; };
 		C7429071B33BDF80A7FA2F8A /* view_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = view_test.cc; sourceTree = "<group>"; };
 		C8522DE226C467C54E6788D8 /* mutation_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = mutation_test.cc; sourceTree = "<group>"; };
 		CB7B2D4691C380DE3EB59038 /* lru_garbage_collector_test.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = lru_garbage_collector_test.h; sourceTree = "<group>"; };
@@ -1537,6 +1536,7 @@
 		F354C0FE92645B56A6C6FD44 /* Pods-Firestore_IntegrationTests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests_iOS/Pods-Firestore_IntegrationTests_iOS.release.xcconfig"; sourceTree = "<group>"; };
 		F51859B394D01C0C507282F1 /* filesystem_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = filesystem_test.cc; sourceTree = "<group>"; };
 		F694C3CE4B77B3C0FA4BBA53 /* Pods_Firestore_Benchmarks_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Benchmarks_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		F6CA0C5638AB6627CB5B4CF4 /* memory_local_store_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = memory_local_store_test.cc; sourceTree = "<group>"; };
 		F8043813A5D16963EC02B182 /* local_serializer_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = local_serializer_test.cc; sourceTree = "<group>"; };
 		FA2E9952BA2B299C1156C43C /* Pods-Firestore_Benchmarks_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Benchmarks_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Benchmarks_iOS/Pods-Firestore_Benchmarks_iOS.debug.xcconfig"; sourceTree = "<group>"; };
 		FC738525340E594EBFAB121E /* Pods-Firestore_Example_tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_tvOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_tvOS/Pods-Firestore_Example_tvOS.release.xcconfig"; sourceTree = "<group>"; };
@@ -1836,6 +1836,7 @@
 				73F1F73A2210F3D800E1F692 /* index_manager_test.h */,
 				166CE73C03AB4366AAC5201C /* leveldb_index_manager_test.cc */,
 				54995F6E205B6E12004EFFA0 /* leveldb_key_test.cc */,
+				5FF903AEFA7A3284660FA4C5 /* leveldb_local_store_test.cc */,
 				B629525F7A1AAC1AB765C74F /* leveldb_lru_garbage_collector_test.cc */,
 				EF83ACD5E1E9F25845A9ACED /* leveldb_migrations_test.cc */,
 				5C7942B6244F4C416B11B86C /* leveldb_mutation_queue_test.cc */,
@@ -1845,9 +1846,12 @@
 				88CF09277CFA45EE1273E3BA /* leveldb_transaction_test.cc */,
 				332485C4DCC6BA0DBB5E31B7 /* leveldb_util_test.cc */,
 				F8043813A5D16963EC02B182 /* local_serializer_test.cc */,
+				307FF03D0297024D59348EBD /* local_store_test.cc */,
+				C0C7C8977C94F9F9AFA4DB00 /* local_store_test.h */,
 				277EAACC4DD7C21332E8496A /* lru_garbage_collector_test.cc */,
 				CB7B2D4691C380DE3EB59038 /* lru_garbage_collector_test.h */,
 				DB5A1E760451189DA36028B3 /* memory_index_manager_test.cc */,
+				F6CA0C5638AB6627CB5B4CF4 /* memory_local_store_test.cc */,
 				9765D47FA12FA283F4EFAD02 /* memory_lru_garbage_collector_test.cc */,
 				74FBEFA4FE4B12C435011763 /* memory_mutation_queue_test.cc */,
 				BB644F9A3FB7EE4EA704E56A /* memory_query_cache_test.cc */,
@@ -2252,10 +2256,6 @@
 		DE51B1621F0D48AC0013853F /* Local */ = {
 			isa = PBXGroup;
 			children = (
-				5492E08F2021552B00B64F25 /* FSTLevelDBLocalStoreTests.mm */,
-				5492E0912021552B00B64F25 /* FSTLocalStoreTests.h */,
-				5492E0832021552A00B64F25 /* FSTLocalStoreTests.mm */,
-				5492E0882021552A00B64F25 /* FSTMemoryLocalStoreTests.mm */,
 			);
 			path = Local;
 			sourceTree = "<group>";
@@ -3364,10 +3364,7 @@
 				0A6FBE65A7FE048BAD562A15 /* FSTGoogleTestTests.mm in Sources */,
 				939C898FE9D129F6A2EA259C /* FSTHelpers.mm in Sources */,
 				C4055D868A38221B332CD03D /* FSTIntegrationTestCase.mm in Sources */,
-				A7470B7B2433264FFDCC7AC3 /* FSTLevelDBLocalStoreTests.mm in Sources */,
 				EC80A217F3D66EB0272B36B0 /* FSTLevelDBSpecTests.mm in Sources */,
-				904DA0AE915C02154AE547FC /* FSTLocalStoreTests.mm in Sources */,
-				3958F87E768E5CF40B87EF90 /* FSTMemoryLocalStoreTests.mm in Sources */,
 				6FF2B680CC8631B06C7BD7AB /* FSTMemorySpecTests.mm in Sources */,
 				F3261CBFC169DB375A0D9492 /* FSTMockDatastore.mm in Sources */,
 				A907244EE37BC32C8D82948E /* FSTSpecTests.mm in Sources */,
@@ -3431,6 +3428,7 @@
 				49C04B97AB282FFA82FD98CD /* latlng.pb.cc in Sources */,
 				8B3EB33933D11CF897EAF4C3 /* leveldb_index_manager_test.cc in Sources */,
 				568EC1C0F68A7B95E57C8C6C /* leveldb_key_test.cc in Sources */,
+				843EE932AA9A8F43721F189E /* leveldb_local_store_test.cc in Sources */,
 				80AB93C807F35539EEC510B2 /* leveldb_lru_garbage_collector_test.cc in Sources */,
 				7EAB3129A58368EE4BD449ED /* leveldb_migrations_test.cc in Sources */,
 				1D7919CD2A05C15803F5FE05 /* leveldb_mutation_queue_test.cc in Sources */,
@@ -3440,10 +3438,12 @@
 				B46E778F9E40864B5D2B2F1C /* leveldb_transaction_test.cc in Sources */,
 				66FAB8EAC012A3822BD4D0C9 /* leveldb_util_test.cc in Sources */,
 				974FF09E6AFD24D5A39B898B /* local_serializer_test.cc in Sources */,
+				C23552A6D9FB0557962870C2 /* local_store_test.cc in Sources */,
 				DBDC8E997E909804F1B43E92 /* log_test.cc in Sources */,
 				3F6C9F8A993CF4B0CD51E7F0 /* lru_garbage_collector_test.cc in Sources */,
 				12158DFCEE09D24B7988A340 /* maybe_document.pb.cc in Sources */,
 				CFF1EBC60A00BA5109893C6E /* memory_index_manager_test.cc in Sources */,
+				49774EBBC8496FE1E43AEE29 /* memory_local_store_test.cc in Sources */,
 				66D9F8E8A65F97F436B1EE5E /* memory_lru_garbage_collector_test.cc in Sources */,
 				E3319DC1804B69F0ED1FFE02 /* memory_mutation_queue_test.cc in Sources */,
 				4604EDB720102F208925CDBC /* memory_query_cache_test.cc in Sources */,
@@ -3537,10 +3537,7 @@
 				E375FBA0632EFB4D14C4E5A9 /* FSTGoogleTestTests.mm in Sources */,
 				F72DF72447EA7AB9D100816A /* FSTHelpers.mm in Sources */,
 				AEBF3F80ACC01AA8A27091CD /* FSTIntegrationTestCase.mm in Sources */,
-				F46394FAA186BC6D19213B59 /* FSTLevelDBLocalStoreTests.mm in Sources */,
 				7495E3BAE536CD839EE20F31 /* FSTLevelDBSpecTests.mm in Sources */,
-				9328C93759C78A10FDBF68E0 /* FSTLocalStoreTests.mm in Sources */,
-				3B47E82ED2A3C59AB5002640 /* FSTMemoryLocalStoreTests.mm in Sources */,
 				EB04FE18E5794FEC187A09E3 /* FSTMemorySpecTests.mm in Sources */,
 				31D8E3D925FA3F70AA20ACCE /* FSTMockDatastore.mm in Sources */,
 				D5E9954FC1C5ABBC7A180B33 /* FSTSpecTests.mm in Sources */,
@@ -3604,6 +3601,7 @@
 				0FBDD5991E8F6CD5F8542474 /* latlng.pb.cc in Sources */,
 				A215078DBFBB5A4F4DADE8A9 /* leveldb_index_manager_test.cc in Sources */,
 				B513F723728E923DFF34F60F /* leveldb_key_test.cc in Sources */,
+				E63342115B1DA65DB6F2C59A /* leveldb_local_store_test.cc in Sources */,
 				4F65FD71B7960944C708A962 /* leveldb_lru_garbage_collector_test.cc in Sources */,
 				90B9302B082E6252AF4E7DC7 /* leveldb_migrations_test.cc in Sources */,
 				1145D70555D8CDC75183A88C /* leveldb_mutation_queue_test.cc in Sources */,
@@ -3613,10 +3611,12 @@
 				EC62F9E29CE3598881908FB8 /* leveldb_transaction_test.cc in Sources */,
 				7A3BE0ED54933C234FDE23D1 /* leveldb_util_test.cc in Sources */,
 				0FA4D5601BE9F0CB5EC2882C /* local_serializer_test.cc in Sources */,
+				0C4219F37CC83614F1FD44ED /* local_store_test.cc in Sources */,
 				12BB9ED1CA98AA52B92F497B /* log_test.cc in Sources */,
 				1F56F51EB6DF0951B1F4F85B /* lru_garbage_collector_test.cc in Sources */,
 				88FD82A1FC5FEC5D56B481D8 /* maybe_document.pb.cc in Sources */,
 				3987A3E8534BAA496D966735 /* memory_index_manager_test.cc in Sources */,
+				B15D17049414E2F5AE72C9C6 /* memory_local_store_test.cc in Sources */,
 				D4D8BA32ACC5C2B1B29711C0 /* memory_lru_garbage_collector_test.cc in Sources */,
 				26C577D159CFFD73E24D543C /* memory_mutation_queue_test.cc in Sources */,
 				BC4246BD7DD4CD92198A7993 /* memory_query_cache_test.cc in Sources */,
@@ -3718,10 +3718,7 @@
 				1817DEF8FF479D218381C541 /* FSTGoogleTestTests.mm in Sources */,
 				086E10B1B37666FB746D56BC /* FSTHelpers.mm in Sources */,
 				02C953A7B0FA5EF87DB0361A /* FSTIntegrationTestCase.mm in Sources */,
-				A2874B68641B7E9F17E66C50 /* FSTLevelDBLocalStoreTests.mm in Sources */,
 				29954A3172DDFE5133D91E24 /* FSTLevelDBSpecTests.mm in Sources */,
-				B1BD0A7EC48C7B7AF09437D5 /* FSTLocalStoreTests.mm in Sources */,
-				40A8F6C6A3670663D6520644 /* FSTMemoryLocalStoreTests.mm in Sources */,
 				2F8FDF35BBB549A6F4D2118E /* FSTMemorySpecTests.mm in Sources */,
 				26B52236C9D049847042E1BD /* FSTMockDatastore.mm in Sources */,
 				3D9619906F09108E34FF0C95 /* FSTSmokeTests.mm in Sources */,
@@ -3788,6 +3785,7 @@
 				CBC891BEEC525F4D8F40A319 /* latlng.pb.cc in Sources */,
 				A602E6C7C8B243BB767D251C /* leveldb_index_manager_test.cc in Sources */,
 				8AA7A1FCEE6EC309399978AD /* leveldb_key_test.cc in Sources */,
+				55E84644D385A70E607A0F91 /* leveldb_local_store_test.cc in Sources */,
 				AF4CD9DB5A7D4516FC54892B /* leveldb_lru_garbage_collector_test.cc in Sources */,
 				AD89E95440264713557FB38E /* leveldb_migrations_test.cc in Sources */,
 				FE701C2D739A5371BCBD62B9 /* leveldb_mutation_queue_test.cc in Sources */,
@@ -3797,10 +3795,12 @@
 				D4572060A0FD4D448470D329 /* leveldb_transaction_test.cc in Sources */,
 				3ABF84FC618016CA6E1D3C03 /* leveldb_util_test.cc in Sources */,
 				F05B277F16BDE6A47FE0F943 /* local_serializer_test.cc in Sources */,
+				EE470CC3C8FBCDA5F70A8466 /* local_store_test.cc in Sources */,
 				CAFB1E0ED514FEF4641E3605 /* log_test.cc in Sources */,
 				913F6E57AF18F84C5ECFD414 /* lru_garbage_collector_test.cc in Sources */,
 				6F511ABFD023AEB81F92DB12 /* maybe_document.pb.cc in Sources */,
 				E6B825EE85BF20B88AF3E3CD /* memory_index_manager_test.cc in Sources */,
+				7ACA8D967438B5CD9DA4C884 /* memory_local_store_test.cc in Sources */,
 				444298A613D027AC67F7E977 /* memory_lru_garbage_collector_test.cc in Sources */,
 				048A55EED3241ABC28752F86 /* memory_mutation_queue_test.cc in Sources */,
 				1BD1E22ECED65BD98A9138A3 /* memory_query_cache_test.cc in Sources */,
@@ -3902,10 +3902,7 @@
 				8D0EF43F1B7B156550E65C20 /* FSTGoogleTestTests.mm in Sources */,
 				198F193BD9484E49375A7BE7 /* FSTHelpers.mm in Sources */,
 				0F54634745BA07B09BDC14D7 /* FSTIntegrationTestCase.mm in Sources */,
-				20BBEADB4725D48BB27548E9 /* FSTLevelDBLocalStoreTests.mm in Sources */,
 				7DED491019248CE9B9E9EB50 /* FSTLevelDBSpecTests.mm in Sources */,
-				5210694AA6274DEDB3AF1177 /* FSTLocalStoreTests.mm in Sources */,
-				3AFEB9ED387C59DEF6B32D5F /* FSTMemoryLocalStoreTests.mm in Sources */,
 				B3B8608727430210C4405AC0 /* FSTMemorySpecTests.mm in Sources */,
 				07ADEF17BFBC07C0C2E306F6 /* FSTMockDatastore.mm in Sources */,
 				42063E6AE9ADF659AA6D4E18 /* FSTSmokeTests.mm in Sources */,
@@ -3972,6 +3969,7 @@
 				4173B61CB74EB4CD1D89EE68 /* latlng.pb.cc in Sources */,
 				839D8B502026706419FE09D6 /* leveldb_index_manager_test.cc in Sources */,
 				A4AD189BDEF7A609953457A6 /* leveldb_key_test.cc in Sources */,
+				1029F0461945A444FCB523B3 /* leveldb_local_store_test.cc in Sources */,
 				000212BFBE7A17712FC9754A /* leveldb_lru_garbage_collector_test.cc in Sources */,
 				61ECC7CE18700CBD73D0D810 /* leveldb_migrations_test.cc in Sources */,
 				A478FDD7C3F48FBFDDA7D8F5 /* leveldb_mutation_queue_test.cc in Sources */,
@@ -3981,10 +3979,12 @@
 				29243A4BBB2E2B1530A62C59 /* leveldb_transaction_test.cc in Sources */,
 				08FA4102AD14452E9587A1F2 /* leveldb_util_test.cc in Sources */,
 				009CDC5D8C96F54A229F462F /* local_serializer_test.cc in Sources */,
+				DF4B3835C5AA4835C01CD255 /* local_store_test.cc in Sources */,
 				6B94E0AE1002C5C9EA0F5582 /* log_test.cc in Sources */,
 				95CE3F5265B9BB7297EE5A6B /* lru_garbage_collector_test.cc in Sources */,
 				C19214F5B43AA745A7FC2FC1 /* maybe_document.pb.cc in Sources */,
 				4D8367018652104A8803E8DB /* memory_index_manager_test.cc in Sources */,
+				91AEFFEE35FBE15FEC42A1F4 /* memory_local_store_test.cc in Sources */,
 				3B23E21D5D7ACF54EBD8CF67 /* memory_lru_garbage_collector_test.cc in Sources */,
 				1F3DD2971C13CBBFA0D84866 /* memory_mutation_queue_test.cc in Sources */,
 				C5FF700C4E992B9BCB1A630F /* memory_query_cache_test.cc in Sources */,
@@ -4100,10 +4100,7 @@
 				54764FAF1FAA21B90085E60A /* FSTGoogleTestTests.mm in Sources */,
 				5492E03F2021401F00B64F25 /* FSTHelpers.mm in Sources */,
 				5491BC721FB44593008B3588 /* FSTIntegrationTestCase.mm in Sources */,
-				5492E0A82021552D00B64F25 /* FSTLevelDBLocalStoreTests.mm in Sources */,
 				5492E03120213FFC00B64F25 /* FSTLevelDBSpecTests.mm in Sources */,
-				5492E09D2021552D00B64F25 /* FSTLocalStoreTests.mm in Sources */,
-				5492E0A12021552D00B64F25 /* FSTMemoryLocalStoreTests.mm in Sources */,
 				5492E03420213FFC00B64F25 /* FSTMemorySpecTests.mm in Sources */,
 				5492E03220213FFC00B64F25 /* FSTMockDatastore.mm in Sources */,
 				5492E03520213FFC00B64F25 /* FSTSpecTests.mm in Sources */,
@@ -4167,6 +4164,7 @@
 				618BBEAE20B89AAC00B5BCE7 /* latlng.pb.cc in Sources */,
 				B743F4E121E879EF34536A51 /* leveldb_index_manager_test.cc in Sources */,
 				54995F6F205B6E12004EFFA0 /* leveldb_key_test.cc in Sources */,
+				04887E378B39FB86A8A5B52B /* leveldb_local_store_test.cc in Sources */,
 				CE2962775B42BDEEE8108567 /* leveldb_lru_garbage_collector_test.cc in Sources */,
 				BACBBF4AF2F5455673AEAB35 /* leveldb_migrations_test.cc in Sources */,
 				98708140787A9465D883EEC9 /* leveldb_mutation_queue_test.cc in Sources */,
@@ -4176,10 +4174,12 @@
 				35DB74DFB2F174865BCCC264 /* leveldb_transaction_test.cc in Sources */,
 				BEE0294A23AB993E5DE0E946 /* leveldb_util_test.cc in Sources */,
 				020AFD89BB40E5175838BB76 /* local_serializer_test.cc in Sources */,
+				D21060F8115A5F48FC3BF335 /* local_store_test.cc in Sources */,
 				54C2294F1FECABAE007D065B /* log_test.cc in Sources */,
 				1290FA77A922B76503AE407C /* lru_garbage_collector_test.cc in Sources */,
 				618BBEA720B89AAC00B5BCE7 /* maybe_document.pb.cc in Sources */,
 				3B47CC43DBA24434E215B8ED /* memory_index_manager_test.cc in Sources */,
+				C6BF529243414C53DF5F1012 /* memory_local_store_test.cc in Sources */,
 				72B25B2D698E4746143D5B74 /* memory_lru_garbage_collector_test.cc in Sources */,
 				851346D66DEC223E839E3AA9 /* memory_mutation_queue_test.cc in Sources */,
 				BE20AA2B4081C95A8FBA4117 /* memory_query_cache_test.cc in Sources */,
@@ -4300,10 +4300,7 @@
 				1E6E2AE74B7C9DEDFC07E76B /* FSTGoogleTestTests.mm in Sources */,
 				5492E0422021440500B64F25 /* FSTHelpers.mm in Sources */,
 				5491BC731FB44593008B3588 /* FSTIntegrationTestCase.mm in Sources */,
-				AFE6FCE804A3B0217D3E2B54 /* FSTLevelDBLocalStoreTests.mm in Sources */,
 				F4FAC5A7D40A0A9A3EA77998 /* FSTLevelDBSpecTests.mm in Sources */,
-				AA616D15FE0E7952787D6A59 /* FSTLocalStoreTests.mm in Sources */,
-				602F6CF9FBF5DA712DA58B5F /* FSTMemoryLocalStoreTests.mm in Sources */,
 				40431BF2A368D0C891229F6E /* FSTMemorySpecTests.mm in Sources */,
 				2BBFAD893295881057E6C1FD /* FSTMockDatastore.mm in Sources */,
 				5492E080202154EC00B64F25 /* FSTSmokeTests.mm in Sources */,
@@ -4370,6 +4367,7 @@
 				23C04A637090E438461E4E70 /* latlng.pb.cc in Sources */,
 				2C5C612B26168BA9286290AE /* leveldb_index_manager_test.cc in Sources */,
 				7731E564468645A4A62E2A3C /* leveldb_key_test.cc in Sources */,
+				380A137B785A5A6991BEDF4B /* leveldb_local_store_test.cc in Sources */,
 				4616CB6342775972F49EDB9B /* leveldb_lru_garbage_collector_test.cc in Sources */,
 				B576823475FBCA5EFA583F9C /* leveldb_migrations_test.cc in Sources */,
 				4FAD8823DC37B9CA24379E85 /* leveldb_mutation_queue_test.cc in Sources */,
@@ -4379,10 +4377,12 @@
 				DDD219222EEE13E3F9F2C703 /* leveldb_transaction_test.cc in Sources */,
 				BC549E3F3F119D80741D8612 /* leveldb_util_test.cc in Sources */,
 				A585BD0F31E90980B5F5FBCA /* local_serializer_test.cc in Sources */,
+				A97ED2BAAEDB0F765BBD5F98 /* local_store_test.cc in Sources */,
 				677C833244550767B71DB1BA /* log_test.cc in Sources */,
 				4DF18D15AC926FB7A4888313 /* lru_garbage_collector_test.cc in Sources */,
 				12E04A12ABD5533B616D552A /* maybe_document.pb.cc in Sources */,
 				90FE088B8FD9EC06EEED1F39 /* memory_index_manager_test.cc in Sources */,
+				1CC56DCA513B98CE39A6ED45 /* memory_local_store_test.cc in Sources */,
 				264AAB492E24318C5EEB0649 /* memory_lru_garbage_collector_test.cc in Sources */,
 				925BE64990449E93242A00A2 /* memory_mutation_queue_test.cc in Sources */,
 				C2B25B816170505AC12E4D65 /* memory_query_cache_test.cc in Sources */,

+ 0 - 47
Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.mm

@@ -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/Example/Tests/Local/FSTLocalStoreTests.h"
-
-#include "Firestore/core/src/firebase/firestore/local/leveldb_persistence.h"
-#include "Firestore/core/test/firebase/firestore/local/persistence_testing.h"
-
-NS_ASSUME_NONNULL_BEGIN
-
-using firebase::firestore::local::LevelDbPersistenceForTesting;
-using firebase::firestore::local::Persistence;
-
-/**
- * The tests for FSTLevelDBLocalStore are performed on the FSTLocalStore protocol in
- * FSTLocalStoreTests. This class is merely responsible for creating a new Persistence
- * implementation on demand.
- */
-@interface FSTLevelDBLocalStoreTests : FSTLocalStoreTests
-@end
-
-@implementation FSTLevelDBLocalStoreTests
-
-- (std::unique_ptr<Persistence>)persistence {
-  return LevelDbPersistenceForTesting();
-}
-
-- (BOOL)gcIsEager {
-  return NO;
-}
-
-@end
-
-NS_ASSUME_NONNULL_END

+ 0 - 42
Firestore/Example/Tests/Local/FSTLocalStoreTests.h

@@ -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 <XCTest/XCTest.h>
-
-#include <memory>
-
-#include "Firestore/core/src/firebase/firestore/local/persistence.h"
-
-namespace local = firebase::firestore::local;
-
-NS_ASSUME_NONNULL_BEGIN
-
-/**
- * These are tests for any implementation of the FSTLocalStore protocol.
- *
- * To test a specific implementation of FSTLocalStore:
- *
- * + Subclass FSTLocalStoreTests
- * + override -persistence, creating a new instance of Persistence.
- */
-@interface FSTLocalStoreTests : XCTestCase
-
-/** Creates and returns an appropriate Persistence implementation. */
-- (std::unique_ptr<local::Persistence>)persistence;
-
-@end
-
-NS_ASSUME_NONNULL_END

+ 0 - 1149
Firestore/Example/Tests/Local/FSTLocalStoreTests.mm

@@ -1,1149 +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.
- */
-// TODO(wuandy): Move `local_store.h` here once this test is ported to C++.
-
-#import <FirebaseFirestore/FIRTimestamp.h>
-#import <XCTest/XCTest.h>
-
-#include <string>
-#include <utility>
-#include <vector>
-
-#import "Firestore/Source/API/FIRFieldValue+Internal.h"
-#import "Firestore/Source/Util/FSTClasses.h"
-
-#import "Firestore/Example/Tests/Local/FSTLocalStoreTests.h"
-#import "Firestore/Example/Tests/Util/FSTHelpers.h"
-
-#include "Firestore/core/include/firebase/firestore/timestamp.h"
-#include "Firestore/core/src/firebase/firestore/auth/user.h"
-#include "Firestore/core/src/firebase/firestore/local/local_store.h"
-#include "Firestore/core/src/firebase/firestore/local/local_view_changes.h"
-#include "Firestore/core/src/firebase/firestore/local/local_write_result.h"
-#include "Firestore/core/src/firebase/firestore/local/persistence.h"
-#include "Firestore/core/src/firebase/firestore/local/query_data.h"
-#include "Firestore/core/src/firebase/firestore/model/document_map.h"
-#include "Firestore/core/src/firebase/firestore/model/document_set.h"
-#include "Firestore/core/src/firebase/firestore/model/mutation_batch_result.h"
-#include "Firestore/core/src/firebase/firestore/remote/remote_event.h"
-#include "Firestore/core/src/firebase/firestore/remote/watch_change.h"
-#include "Firestore/core/src/firebase/firestore/util/status.h"
-#include "Firestore/core/test/firebase/firestore/remote/fake_target_metadata_provider.h"
-#include "Firestore/core/test/firebase/firestore/testutil/testutil.h"
-
-namespace testutil = firebase::firestore::testutil;
-using firebase::Timestamp;
-using firebase::firestore::auth::User;
-using firebase::firestore::local::LocalStore;
-using firebase::firestore::local::LocalViewChanges;
-using firebase::firestore::local::LocalWriteResult;
-using firebase::firestore::local::Persistence;
-using firebase::firestore::local::QueryData;
-using firebase::firestore::model::Document;
-using firebase::firestore::model::DocumentKey;
-using firebase::firestore::model::DocumentKeySet;
-using firebase::firestore::model::DocumentState;
-using firebase::firestore::model::FieldValue;
-using firebase::firestore::model::ListenSequenceNumber;
-using firebase::firestore::model::MaybeDocument;
-using firebase::firestore::model::Mutation;
-using firebase::firestore::model::MutationBatch;
-using firebase::firestore::model::MutationBatchResult;
-using firebase::firestore::model::MutationResult;
-using firebase::firestore::model::DocumentMap;
-using firebase::firestore::model::MaybeDocumentMap;
-using firebase::firestore::model::SnapshotVersion;
-using firebase::firestore::model::TargetId;
-using firebase::firestore::nanopb::ByteString;
-using firebase::firestore::remote::FakeTargetMetadataProvider;
-using firebase::firestore::remote::RemoteEvent;
-using firebase::firestore::remote::WatchChangeAggregator;
-using firebase::firestore::remote::WatchTargetChange;
-using firebase::firestore::remote::WatchTargetChangeState;
-using firebase::firestore::util::Status;
-
-using testutil::Array;
-using testutil::DeletedDoc;
-using testutil::Doc;
-using testutil::Key;
-using testutil::Map;
-using testutil::Query;
-using testutil::UnknownDoc;
-using testutil::Vector;
-
-namespace {
-
-std::vector<MaybeDocument> DocMapToArray(const MaybeDocumentMap &docs) {
-  std::vector<MaybeDocument> result;
-  for (const auto &kv : docs) {
-    result.push_back(kv.second);
-  }
-  return result;
-}
-
-std::vector<Document> DocMapToArray(const DocumentMap &docs) {
-  std::vector<Document> result;
-  for (const auto &kv : docs.underlying_map()) {
-    result.push_back(Document(kv.second));
-  }
-  return result;
-}
-
-}  // namespace
-
-NS_ASSUME_NONNULL_BEGIN
-
-@interface FSTLocalStoreTests ()
-
-@property(nonatomic, assign, readwrite) TargetId lastTargetID;
-
-@end
-
-@implementation FSTLocalStoreTests {
-  std::unique_ptr<LocalStore> _localStore;
-  std::unique_ptr<Persistence> _localStorePersistence;
-  std::vector<MutationBatch> _batches;
-  MaybeDocumentMap _lastChanges;
-}
-
-- (void)setUp {
-  [super setUp];
-
-  if ([self isTestBaseClass]) {
-    return;
-  }
-
-  std::unique_ptr<Persistence> persistence = [self persistence];
-  _localStorePersistence = std::move(persistence);
-  _localStore =
-      absl::make_unique<LocalStore>(_localStorePersistence.get(), User::Unauthenticated());
-  _localStore->Start();
-
-  _lastTargetID = 0;
-}
-
-- (void)tearDown {
-  if (_localStorePersistence) {
-    _localStorePersistence->Shutdown();
-  }
-
-  [super tearDown];
-}
-
-- (std::unique_ptr<Persistence>)persistence {
-  @throw FSTAbstractMethodException();  // NOLINT
-}
-
-- (BOOL)gcIsEager {
-  @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];
-}
-
-- (void)writeMutation:(Mutation)mutation {
-  [self writeMutations:{std::move(mutation)}];
-}
-
-- (void)writeMutations:(std::vector<Mutation> &&)mutations {
-  auto mutationsCopy = mutations;
-  LocalWriteResult result = _localStore->WriteLocally(std::move(mutationsCopy));
-  _batches.emplace_back(result.batch_id(), Timestamp::Now(), std::vector<Mutation>{},
-                        std::move(mutations));
-  _lastChanges = result.changes();
-}
-
-- (void)applyRemoteEvent:(const RemoteEvent &)event {
-  _lastChanges = _localStore->ApplyRemoteEvent(event);
-}
-
-- (void)notifyLocalViewChanges:(LocalViewChanges)changes {
-  _localStore->NotifyLocalViewChanges(std::vector<LocalViewChanges>{std::move(changes)});
-}
-
-- (void)acknowledgeMutationWithVersion:(FSTTestSnapshotVersion)documentVersion
-                       transformResult:(id _Nullable)transformResult {
-  XCTAssertGreaterThan(_batches.size(), 0, @"Missing batch to acknowledge.");
-  MutationBatch batch = _batches.front();
-  _batches.erase(_batches.begin());
-
-  XCTAssertEqual(batch.mutations().size(), 1,
-                 @"Acknowledging more than one mutation not supported.");
-  SnapshotVersion version = testutil::Version(documentVersion);
-
-  absl::optional<std::vector<FieldValue>> mutationTransformResult;
-  if (transformResult) {
-    mutationTransformResult = std::vector<FieldValue>{FSTTestFieldValue(transformResult)};
-  }
-
-  MutationResult mutationResult(version, mutationTransformResult);
-  MutationBatchResult result(batch, version, {mutationResult}, {});
-  _lastChanges = _localStore->AcknowledgeBatch(result);
-}
-
-- (void)acknowledgeMutationWithVersion:(FSTTestSnapshotVersion)documentVersion {
-  [self acknowledgeMutationWithVersion:documentVersion transformResult:nil];
-}
-
-- (void)rejectMutation {
-  MutationBatch batch = _batches.front();
-  _batches.erase(_batches.begin());
-  _lastChanges = _localStore->RejectBatch(batch.batch_id());
-}
-
-- (TargetId)allocateQuery:(core::Query)query {
-  QueryData queryData = _localStore->AllocateQuery(std::move(query));
-  self.lastTargetID = queryData.target_id();
-  return queryData.target_id();
-}
-
-/** 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(...)                             \
-  do {                                                    \
-    std::vector<MaybeDocument> expected = {__VA_ARGS__};  \
-    XCTAssertEqual(_lastChanges.size(), expected.size()); \
-    auto lastChangesList = DocMapToArray(_lastChanges);   \
-    XCTAssertEqual(lastChangesList, expected);            \
-    _lastChanges = MaybeDocumentMap{};                    \
-  } while (0)
-
-/** Asserts that the given keys were removed. */
-#define FSTAssertRemoved(...)                             \
-  do {                                                    \
-    std::vector<std::string> keyPaths = {__VA_ARGS__};    \
-    XCTAssertEqual(_lastChanges.size(), keyPaths.size()); \
-    auto keyPathIterator = keyPaths.begin();              \
-    for (const auto &kv : _lastChanges) {                 \
-      const DocumentKey &actualKey = kv.first;            \
-      const MaybeDocument &value = kv.second;             \
-      DocumentKey expectedKey = Key(*keyPathIterator);    \
-      XCTAssertEqual(actualKey, expectedKey);             \
-      XCTAssertTrue(value.is_no_document());              \
-      ++keyPathIterator;                                  \
-    }                                                     \
-    _lastChanges = MaybeDocumentMap{};                    \
-  } while (0)
-
-/** Asserts that the given local store contains the given document. */
-#define FSTAssertContains(document)                                                   \
-  do {                                                                                \
-    MaybeDocument expected = (document);                                              \
-    absl::optional<MaybeDocument> actual = _localStore->ReadDocument(expected.key()); \
-    XCTAssertEqual(actual, expected);                                                 \
-  } while (0)
-
-/** Asserts that the given local store does not contain the given document. */
-#define FSTAssertNotContains(keyPathString)                                \
-  do {                                                                     \
-    DocumentKey key = Key(keyPathString);                                  \
-    absl::optional<MaybeDocument> actual = _localStore->ReadDocument(key); \
-    XCTAssertEqual(actual, absl::nullopt);                                 \
-  } while (0)
-
-- (void)testMutationBatchKeys {
-  if ([self isTestBaseClass]) return;
-
-  Mutation base = FSTTestSetMutation(@"foo/ignore", @{@"foo" : @"bar"});
-  Mutation set1 = FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"});
-  Mutation set2 = FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"});
-  MutationBatch batch = MutationBatch(1, Timestamp::Now(), {base}, {set1, set2});
-  DocumentKeySet keys = batch.keys();
-  XCTAssertEqual(keys.size(), 2u);
-}
-
-- (void)testHandlesSetMutation {
-  if ([self isTestBaseClass]) return;
-
-  [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
-  FSTAssertChanged(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
-  FSTAssertContains(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
-
-  [self acknowledgeMutationWithVersion:0];
-  FSTAssertChanged(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kCommittedMutations));
-  if ([self gcIsEager]) {
-    // Nothing is pinning this anymore, as it has been acknowledged and there are no targets active.
-    FSTAssertNotContains("foo/bar");
-  } else {
-    FSTAssertContains(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kCommittedMutations));
-  }
-}
-
-- (void)testHandlesSetMutationThenDocument {
-  if ([self isTestBaseClass]) return;
-
-  [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
-  FSTAssertChanged(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
-  FSTAssertContains(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
-
-  TargetId targetID = [self allocateQuery:Query("foo")];
-
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(Doc("foo/bar", 2, Map("it", "changed")),
-                                                  {targetID}, {})];
-  FSTAssertChanged(Doc("foo/bar", 2, Map("foo", "bar"), DocumentState::kLocalMutations));
-  FSTAssertContains(Doc("foo/bar", 2, Map("foo", "bar"), DocumentState::kLocalMutations));
-}
-
-- (void)testHandlesAckThenRejectThenRemoteEvent {
-  if ([self isTestBaseClass]) return;
-
-  // Start a query that requires acks to be held.
-  core::Query query = Query("foo");
-  TargetId targetID = [self allocateQuery:query];
-
-  [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
-  FSTAssertChanged(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
-  FSTAssertContains(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
-
-  // The last seen version is zero, so this ack must be held.
-  [self acknowledgeMutationWithVersion:1];
-  FSTAssertChanged(Doc("foo/bar", 1, Map("foo", "bar"), DocumentState::kCommittedMutations));
-
-  // Under eager GC, there is no longer a reference for the document, and it should be
-  // deleted.
-  if ([self gcIsEager]) {
-    FSTAssertNotContains("foo/bar");
-  } else {
-    FSTAssertContains(Doc("foo/bar", 1, Map("foo", "bar"), DocumentState::kCommittedMutations));
-  }
-
-  [self writeMutation:FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"})];
-  FSTAssertChanged(Doc("bar/baz", 0, Map("bar", "baz"), DocumentState::kLocalMutations));
-  FSTAssertContains(Doc("bar/baz", 0, Map("bar", "baz"), DocumentState::kLocalMutations));
-
-  [self rejectMutation];
-  FSTAssertRemoved("bar/baz");
-  FSTAssertNotContains("bar/baz");
-
-  [self applyRemoteEvent:FSTTestAddedRemoteEvent(Doc("foo/bar", 2, Map("it", "changed")),
-                                                 {targetID})];
-  FSTAssertChanged(Doc("foo/bar", 2, Map("it", "changed")));
-  FSTAssertContains(Doc("foo/bar", 2, Map("it", "changed")));
-  FSTAssertNotContains("bar/baz");
-}
-
-- (void)testHandlesDeletedDocumentThenSetMutationThenAck {
-  if ([self isTestBaseClass]) return;
-
-  core::Query query = Query("foo");
-  TargetId targetID = [self allocateQuery:query];
-
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(DeletedDoc("foo/bar", 2), {targetID}, {})];
-  FSTAssertRemoved("foo/bar");
-  // Under eager GC, there is no longer a reference for the document, and it should be
-  // deleted.
-  if (![self gcIsEager]) {
-    FSTAssertContains(DeletedDoc("foo/bar", 2, NO));
-  } else {
-    FSTAssertNotContains("foo/bar");
-  }
-
-  [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
-  FSTAssertChanged(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
-  FSTAssertContains(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
-  // Can now remove the target, since we have a mutation pinning the document
-  _localStore->ReleaseQuery(query);
-  // Verify we didn't lose anything
-  FSTAssertContains(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
-
-  [self acknowledgeMutationWithVersion:3];
-  FSTAssertChanged(Doc("foo/bar", 3, Map("foo", "bar"), DocumentState::kCommittedMutations));
-  // It has been acknowledged, and should no longer be retained as there is no target and mutation
-  if ([self gcIsEager]) {
-    FSTAssertNotContains("foo/bar");
-  }
-}
-
-- (void)testHandlesSetMutationThenDeletedDocument {
-  if ([self isTestBaseClass]) return;
-
-  core::Query query = Query("foo");
-  TargetId targetID = [self allocateQuery:query];
-
-  [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
-  FSTAssertChanged(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
-
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(DeletedDoc("foo/bar", 2), {targetID}, {})];
-  FSTAssertChanged(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
-  FSTAssertContains(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
-}
-
-- (void)testHandlesDocumentThenSetMutationThenAckThenDocument {
-  if ([self isTestBaseClass]) return;
-
-  // Start a query that requires acks to be held.
-  core::Query query = Query("foo");
-  TargetId targetID = [self allocateQuery:query];
-
-  [self applyRemoteEvent:FSTTestAddedRemoteEvent(Doc("foo/bar", 2, Map("it", "base")), {targetID})];
-  FSTAssertChanged(Doc("foo/bar", 2, Map("it", "base")));
-  FSTAssertContains(Doc("foo/bar", 2, Map("it", "base")));
-
-  [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
-  FSTAssertChanged(Doc("foo/bar", 2, Map("foo", "bar"), DocumentState::kLocalMutations));
-  FSTAssertContains(Doc("foo/bar", 2, Map("foo", "bar"), DocumentState::kLocalMutations));
-
-  [self acknowledgeMutationWithVersion:3];
-  // we haven't seen the remote event yet, so the write is still held.
-  FSTAssertChanged(Doc("foo/bar", 3, Map("foo", "bar"), DocumentState::kCommittedMutations));
-  FSTAssertContains(Doc("foo/bar", 3, Map("foo", "bar"), DocumentState::kCommittedMutations));
-
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(Doc("foo/bar", 3, Map("it", "changed")),
-                                                  {targetID}, {})];
-  FSTAssertChanged(Doc("foo/bar", 3, Map("it", "changed")));
-  FSTAssertContains(Doc("foo/bar", 3, Map("it", "changed")));
-}
-
-- (void)testHandlesPatchWithoutPriorDocument {
-  if ([self isTestBaseClass]) return;
-
-  [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})];
-  FSTAssertRemoved("foo/bar");
-  FSTAssertNotContains("foo/bar");
-
-  [self acknowledgeMutationWithVersion:1];
-  FSTAssertChanged(UnknownDoc("foo/bar", 1));
-  if ([self gcIsEager]) {
-    FSTAssertNotContains("foo/bar");
-  } else {
-    FSTAssertContains(UnknownDoc("foo/bar", 1));
-  }
-}
-
-- (void)testHandlesPatchMutationThenDocumentThenAck {
-  if ([self isTestBaseClass]) return;
-
-  [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})];
-  FSTAssertRemoved("foo/bar");
-  FSTAssertNotContains("foo/bar");
-
-  core::Query query = Query("foo");
-  TargetId targetID = [self allocateQuery:query];
-
-  [self applyRemoteEvent:FSTTestAddedRemoteEvent(Doc("foo/bar", 1, Map("it", "base")), {targetID})];
-  FSTAssertChanged(
-      Doc("foo/bar", 1, Map("foo", "bar", "it", "base"), DocumentState::kLocalMutations));
-  FSTAssertContains(
-      Doc("foo/bar", 1, Map("foo", "bar", "it", "base"), DocumentState::kLocalMutations));
-
-  [self acknowledgeMutationWithVersion:2];
-  // We still haven't seen the remote events for the patch, so the local changes remain, and there
-  // are no changes
-  FSTAssertChanged(
-      Doc("foo/bar", 2, Map("foo", "bar", "it", "base"), DocumentState::kCommittedMutations));
-  FSTAssertContains(
-      Doc("foo/bar", 2, Map("foo", "bar", "it", "base"), DocumentState::kCommittedMutations));
-
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(
-                             Doc("foo/bar", 2, Map("foo", "bar", "it", "base")), {targetID}, {})];
-
-  FSTAssertChanged(Doc("foo/bar", 2, Map("foo", "bar", "it", "base")));
-  FSTAssertContains(Doc("foo/bar", 2, Map("foo", "bar", "it", "base")));
-}
-
-- (void)testHandlesPatchMutationThenAckThenDocument {
-  if ([self isTestBaseClass]) return;
-
-  [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})];
-  FSTAssertRemoved("foo/bar");
-  FSTAssertNotContains("foo/bar");
-
-  [self acknowledgeMutationWithVersion:1];
-  FSTAssertChanged(UnknownDoc("foo/bar", 1));
-
-  // There's no target pinning the doc, and we've ack'd the mutation.
-  if ([self gcIsEager]) {
-    FSTAssertNotContains("foo/bar");
-  } else {
-    FSTAssertContains(UnknownDoc("foo/bar", 1));
-  }
-
-  core::Query query = Query("foo");
-  TargetId targetID = [self allocateQuery:query];
-
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(Doc("foo/bar", 1, Map("it", "base")), {targetID},
-                                                  {})];
-  FSTAssertChanged(Doc("foo/bar", 1, Map("it", "base")));
-  FSTAssertContains(Doc("foo/bar", 1, Map("it", "base")));
-}
-
-- (void)testHandlesDeleteMutationThenAck {
-  if ([self isTestBaseClass]) return;
-
-  [self writeMutation:FSTTestDeleteMutation(@"foo/bar")];
-  FSTAssertRemoved("foo/bar");
-  FSTAssertContains(DeletedDoc("foo/bar"));
-
-  [self acknowledgeMutationWithVersion:1];
-  FSTAssertRemoved("foo/bar");
-  // There's no target pinning the doc, and we've ack'd the mutation.
-  if ([self gcIsEager]) {
-    FSTAssertNotContains("foo/bar");
-  }
-}
-
-- (void)testHandlesDocumentThenDeleteMutationThenAck {
-  if ([self isTestBaseClass]) return;
-
-  core::Query query = Query("foo");
-  TargetId targetID = [self allocateQuery:query];
-
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(Doc("foo/bar", 1, Map("it", "base")), {targetID},
-                                                  {})];
-  FSTAssertChanged(Doc("foo/bar", 1, Map("it", "base")));
-  FSTAssertContains(Doc("foo/bar", 1, Map("it", "base")));
-
-  [self writeMutation:FSTTestDeleteMutation(@"foo/bar")];
-  FSTAssertRemoved("foo/bar");
-  FSTAssertContains(DeletedDoc("foo/bar"));
-
-  // Remove the target so only the mutation is pinning the document
-  _localStore->ReleaseQuery(query);
-
-  [self acknowledgeMutationWithVersion:2];
-  FSTAssertRemoved("foo/bar");
-  if ([self gcIsEager]) {
-    // Neither the target nor the mutation pin the document, it should be gone.
-    FSTAssertNotContains("foo/bar");
-  }
-}
-
-- (void)testHandlesDeleteMutationThenDocumentThenAck {
-  if ([self isTestBaseClass]) return;
-
-  core::Query query = Query("foo");
-  TargetId targetID = [self allocateQuery:query];
-
-  [self writeMutation:FSTTestDeleteMutation(@"foo/bar")];
-  FSTAssertRemoved("foo/bar");
-  FSTAssertContains(DeletedDoc("foo/bar"));
-
-  // Add the document to a target so it will remain in persistence even when ack'd
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(Doc("foo/bar", 1, Map("it", "base")), {targetID},
-                                                  {})];
-  FSTAssertRemoved("foo/bar");
-  FSTAssertContains(DeletedDoc("foo/bar"));
-
-  // Don't need to keep it pinned anymore
-  _localStore->ReleaseQuery(query);
-
-  [self acknowledgeMutationWithVersion:2];
-  FSTAssertRemoved("foo/bar");
-  if ([self gcIsEager]) {
-    // The doc is not pinned in a target and we've acknowledged the mutation. It shouldn't exist
-    // anymore.
-    FSTAssertNotContains("foo/bar");
-  }
-}
-
-- (void)testHandlesDocumentThenDeletedDocumentThenDocument {
-  if ([self isTestBaseClass]) return;
-
-  core::Query query = Query("foo");
-  TargetId targetID = [self allocateQuery:query];
-
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(Doc("foo/bar", 1, Map("it", "base")), {targetID},
-                                                  {})];
-  FSTAssertChanged(Doc("foo/bar", 1, Map("it", "base")));
-  FSTAssertContains(Doc("foo/bar", 1, Map("it", "base")));
-
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(DeletedDoc("foo/bar", 2), {targetID}, {})];
-  FSTAssertRemoved("foo/bar");
-  if (![self gcIsEager]) {
-    FSTAssertContains(DeletedDoc("foo/bar", 2));
-  }
-
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(Doc("foo/bar", 3, Map("it", "changed")),
-                                                  {targetID}, {})];
-  FSTAssertChanged(Doc("foo/bar", 3, Map("it", "changed")));
-  FSTAssertContains(Doc("foo/bar", 3, Map("it", "changed")));
-}
-
-- (void)testHandlesSetMutationThenPatchMutationThenDocumentThenAckThenAck {
-  if ([self isTestBaseClass]) return;
-
-  [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"})];
-  FSTAssertChanged(Doc("foo/bar", 0, Map("foo", "old"), DocumentState::kLocalMutations));
-  FSTAssertContains(Doc("foo/bar", 0, Map("foo", "old"), DocumentState::kLocalMutations));
-
-  [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})];
-  FSTAssertChanged(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
-  FSTAssertContains(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
-
-  core::Query query = Query("foo");
-  TargetId targetID = [self allocateQuery:query];
-
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(Doc("foo/bar", 1, Map("it", "base")), {targetID},
-                                                  {})];
-  FSTAssertChanged(Doc("foo/bar", 1, Map("foo", "bar"), DocumentState::kLocalMutations));
-  FSTAssertContains(Doc("foo/bar", 1, Map("foo", "bar"), DocumentState::kLocalMutations));
-
-  _localStore->ReleaseQuery(query);
-  [self acknowledgeMutationWithVersion:2];  // delete mutation
-  FSTAssertChanged(Doc("foo/bar", 2, Map("foo", "bar"), DocumentState::kLocalMutations));
-  FSTAssertContains(Doc("foo/bar", 2, Map("foo", "bar"), DocumentState::kLocalMutations));
-
-  [self acknowledgeMutationWithVersion:3];  // patch mutation
-  FSTAssertChanged(Doc("foo/bar", 3, Map("foo", "bar"), DocumentState::kCommittedMutations));
-  if ([self gcIsEager]) {
-    // we've ack'd all of the mutations, nothing is keeping this pinned anymore
-    FSTAssertNotContains("foo/bar");
-  } else {
-    FSTAssertContains(Doc("foo/bar", 3, Map("foo", "bar"), DocumentState::kCommittedMutations));
-  }
-}
-
-- (void)testHandlesSetMutationAndPatchMutationTogether {
-  if ([self isTestBaseClass]) return;
-
-  [self writeMutations:{
-    FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}),
-        FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})
-  }];
-
-  FSTAssertChanged(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
-  FSTAssertContains(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
-}
-
-- (void)testHandlesSetMutationThenPatchMutationThenReject {
-  if ([self isTestBaseClass]) return;
-  if (![self gcIsEager]) return;
-
-  [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"})];
-  FSTAssertContains(Doc("foo/bar", 0, Map("foo", "old"), DocumentState::kLocalMutations));
-  [self acknowledgeMutationWithVersion:1];
-  FSTAssertNotContains("foo/bar");
-
-  [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})];
-  // A blind patch is not visible in the cache
-  FSTAssertNotContains("foo/bar");
-
-  [self rejectMutation];
-  FSTAssertNotContains("foo/bar");
-}
-
-- (void)testHandlesSetMutationsAndPatchMutationOfJustOneTogether {
-  if ([self isTestBaseClass]) return;
-
-  [self writeMutations:{
-    FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}),
-        FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"}),
-        FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})
-  }];
-
-  FSTAssertChanged(Doc("bar/baz", 0, Map("bar", "baz"), DocumentState::kLocalMutations),
-                   Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
-  FSTAssertContains(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
-  FSTAssertContains(Doc("bar/baz", 0, Map("bar", "baz"), DocumentState::kLocalMutations));
-}
-
-- (void)testHandlesDeleteMutationThenPatchMutationThenAckThenAck {
-  if ([self isTestBaseClass]) return;
-
-  [self writeMutation:FSTTestDeleteMutation(@"foo/bar")];
-  FSTAssertRemoved("foo/bar");
-  FSTAssertContains(DeletedDoc("foo/bar"));
-
-  [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})];
-  FSTAssertRemoved("foo/bar");
-  FSTAssertContains(DeletedDoc("foo/bar"));
-
-  [self acknowledgeMutationWithVersion:2];  // delete mutation
-  FSTAssertRemoved("foo/bar");
-  FSTAssertContains(DeletedDoc("foo/bar", 2, /* has_committed_mutations= */ true));
-
-  [self acknowledgeMutationWithVersion:3];  // patch mutation
-  FSTAssertChanged(UnknownDoc("foo/bar", 3));
-  if ([self gcIsEager]) {
-    // There are no more pending mutations, the doc has been dropped
-    FSTAssertNotContains("foo/bar");
-  } else {
-    FSTAssertContains(UnknownDoc("foo/bar", 3));
-  }
-}
-
-- (void)testCollectsGarbageAfterChangeBatchWithNoTargetIDs {
-  if ([self isTestBaseClass]) return;
-  if (![self gcIsEager]) return;
-
-  [self applyRemoteEvent:FSTTestUpdateRemoteEventWithLimboTargets(DeletedDoc("foo/bar", 2), {}, {},
-                                                                  {1})];
-  FSTAssertNotContains("foo/bar");
-
-  [self applyRemoteEvent:FSTTestUpdateRemoteEventWithLimboTargets(
-                             Doc("foo/bar", 2, Map("foo", "bar")), {}, {}, {1})];
-  FSTAssertNotContains("foo/bar");
-}
-
-- (void)testCollectsGarbageAfterChangeBatch {
-  if ([self isTestBaseClass]) return;
-  if (![self gcIsEager]) return;
-
-  core::Query query = Query("foo");
-  TargetId targetID = [self allocateQuery:query];
-
-  [self applyRemoteEvent:FSTTestAddedRemoteEvent(Doc("foo/bar", 2, Map("foo", "bar")), {targetID})];
-  FSTAssertContains(Doc("foo/bar", 2, Map("foo", "bar")));
-
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(Doc("foo/bar", 2, Map("foo", "baz")), {},
-                                                  {targetID})];
-
-  FSTAssertNotContains("foo/bar");
-}
-
-- (void)testCollectsGarbageAfterAcknowledgedMutation {
-  if ([self isTestBaseClass]) return;
-  if (![self gcIsEager]) return;
-
-  core::Query query = Query("foo");
-  TargetId targetID = [self allocateQuery:query];
-
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(Doc("foo/bar", 1, Map("foo", "old")), {targetID},
-                                                  {})];
-  [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})];
-  // Release the query so that our target count goes back to 0 and we are considered up-to-date.
-  _localStore->ReleaseQuery(query);
-
-  [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})];
-  [self writeMutation:FSTTestDeleteMutation(@"foo/baz")];
-  FSTAssertContains(Doc("foo/bar", 1, Map("foo", "bar"), DocumentState::kLocalMutations));
-  FSTAssertContains(Doc("foo/bah", 0, Map("foo", "bah"), DocumentState::kLocalMutations));
-  FSTAssertContains(DeletedDoc("foo/baz"));
-
-  [self acknowledgeMutationWithVersion:3];
-  FSTAssertNotContains("foo/bar");
-  FSTAssertContains(Doc("foo/bah", 0, Map("foo", "bah"), DocumentState::kLocalMutations));
-  FSTAssertContains(DeletedDoc("foo/baz"));
-
-  [self acknowledgeMutationWithVersion:4];
-  FSTAssertNotContains("foo/bar");
-  FSTAssertNotContains("foo/bah");
-  FSTAssertContains(DeletedDoc("foo/baz"));
-
-  [self acknowledgeMutationWithVersion:5];
-  FSTAssertNotContains("foo/bar");
-  FSTAssertNotContains("foo/bah");
-  FSTAssertNotContains("foo/baz");
-}
-
-- (void)testCollectsGarbageAfterRejectedMutation {
-  if ([self isTestBaseClass]) return;
-  if (![self gcIsEager]) return;
-
-  core::Query query = Query("foo");
-  TargetId targetID = [self allocateQuery:query];
-
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(Doc("foo/bar", 1, Map("foo", "old")), {targetID},
-                                                  {})];
-  [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})];
-  // Release the query so that our target count goes back to 0 and we are considered up-to-date.
-  _localStore->ReleaseQuery(query);
-
-  [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})];
-  [self writeMutation:FSTTestDeleteMutation(@"foo/baz")];
-  FSTAssertContains(Doc("foo/bar", 1, Map("foo", "bar"), DocumentState::kLocalMutations));
-  FSTAssertContains(Doc("foo/bah", 0, Map("foo", "bah"), DocumentState::kLocalMutations));
-  FSTAssertContains(DeletedDoc("foo/baz"));
-
-  [self rejectMutation];  // patch mutation
-  FSTAssertNotContains("foo/bar");
-  FSTAssertContains(Doc("foo/bah", 0, Map("foo", "bah"), DocumentState::kLocalMutations));
-  FSTAssertContains(DeletedDoc("foo/baz"));
-
-  [self rejectMutation];  // set mutation
-  FSTAssertNotContains("foo/bar");
-  FSTAssertNotContains("foo/bah");
-  FSTAssertContains(DeletedDoc("foo/baz"));
-
-  [self rejectMutation];  // delete mutation
-  FSTAssertNotContains("foo/bar");
-  FSTAssertNotContains("foo/bah");
-  FSTAssertNotContains("foo/baz");
-}
-
-- (void)testPinsDocumentsInTheLocalView {
-  if ([self isTestBaseClass]) return;
-  if (![self gcIsEager]) return;
-
-  core::Query query = Query("foo");
-  TargetId targetID = [self allocateQuery:query];
-
-  [self applyRemoteEvent:FSTTestAddedRemoteEvent(Doc("foo/bar", 1, Map("foo", "bar")), {targetID})];
-  [self writeMutation:FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"})];
-  FSTAssertContains(Doc("foo/bar", 1, Map("foo", "bar")));
-  FSTAssertContains(Doc("foo/baz", 0, Map("foo", "baz"), DocumentState::kLocalMutations));
-
-  [self notifyLocalViewChanges:TestViewChanges(targetID, @[ @"foo/bar", @"foo/baz" ], @[])];
-  FSTAssertContains(Doc("foo/bar", 1, Map("foo", "bar")));
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(Doc("foo/bar", 1, Map("foo", "bar")), {},
-                                                  {targetID})];
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(Doc("foo/baz", 2, Map("foo", "baz")), {targetID},
-                                                  {})];
-  FSTAssertContains(Doc("foo/baz", 2, Map("foo", "baz"), DocumentState::kLocalMutations));
-  [self acknowledgeMutationWithVersion:2];
-  FSTAssertContains(Doc("foo/baz", 2, Map("foo", "baz")));
-  FSTAssertContains(Doc("foo/bar", 1, Map("foo", "bar")));
-  FSTAssertContains(Doc("foo/baz", 2, Map("foo", "baz")));
-
-  [self notifyLocalViewChanges:TestViewChanges(targetID, @[], @[ @"foo/bar", @"foo/baz" ])];
-  FSTAssertNotContains("foo/bar");
-  FSTAssertNotContains("foo/baz");
-
-  _localStore->ReleaseQuery(query);
-}
-
-- (void)testThrowsAwayDocumentsWithUnknownTargetIDsImmediately {
-  if ([self isTestBaseClass]) return;
-  if (![self gcIsEager]) return;
-
-  TargetId targetID = 321;
-  [self applyRemoteEvent:FSTTestUpdateRemoteEventWithLimboTargets(Doc("foo/bar", 1, Map()), {}, {},
-                                                                  {targetID})];
-
-  FSTAssertNotContains("foo/bar");
-}
-
-- (void)testCanExecuteDocumentQueries {
-  if ([self isTestBaseClass]) return;
-
-  _localStore->WriteLocally({
-    FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}),
-        FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}),
-        FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"})
-  });
-  core::Query query = Query("foo/bar");
-  DocumentMap docs = _localStore->ExecuteQuery(query);
-  XCTAssertEqual(DocMapToArray(docs),
-                 Vector(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations)));
-}
-
-- (void)testCanExecuteCollectionQueries {
-  if ([self isTestBaseClass]) return;
-
-  _localStore->WriteLocally({
-    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"})
-  });
-  core::Query query = Query("foo");
-  DocumentMap docs = _localStore->ExecuteQuery(query);
-  XCTAssertEqual(DocMapToArray(docs),
-                 Vector(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations),
-                        Doc("foo/baz", 0, Map("foo", "baz"), DocumentState::kLocalMutations)));
-}
-
-- (void)testCanExecuteMixedCollectionQueries {
-  if ([self isTestBaseClass]) return;
-
-  core::Query query = Query("foo");
-  [self allocateQuery:query];
-  FSTAssertTargetID(2);
-
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(Doc("foo/baz", 10, Map("a", "b")), {2}, {})];
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(Doc("foo/bar", 20, Map("a", "b")), {2}, {})];
-
-  _localStore->WriteLocally({ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) });
-
-  DocumentMap docs = _localStore->ExecuteQuery(query);
-  XCTAssertEqual(DocMapToArray(docs),
-                 Vector(Doc("foo/bar", 20, Map("a", "b")), Doc("foo/baz", 10, Map("a", "b")),
-                        Doc("foo/bonk", 0, Map("a", "b"), DocumentState::kLocalMutations)));
-}
-
-- (void)testPersistsResumeTokens {
-  if ([self isTestBaseClass]) return;
-  // This test only works in the absence of the FSTEagerGarbageCollector.
-  if ([self gcIsEager]) return;
-
-  core::Query query = Query("foo/bar");
-  QueryData queryData = _localStore->AllocateQuery(query);
-  ListenSequenceNumber initialSequenceNumber = queryData.sequence_number();
-  TargetId targetID = queryData.target_id();
-  ByteString resumeToken = testutil::ResumeToken(1000);
-
-  WatchTargetChange watchChange{WatchTargetChangeState::Current, {targetID}, resumeToken};
-  auto metadataProvider = FakeTargetMetadataProvider::CreateSingleResultProvider(
-      testutil::Key("foo/bar"), std::vector<TargetId>{targetID});
-  WatchChangeAggregator aggregator{&metadataProvider};
-  aggregator.HandleTargetChange(watchChange);
-  RemoteEvent remoteEvent = aggregator.CreateRemoteEvent(testutil::Version(1000));
-  [self applyRemoteEvent:remoteEvent];
-
-  // Stop listening so that the query should become inactive (but persistent)
-  _localStore->ReleaseQuery(query);
-
-  // Should come back with the same resume token
-  QueryData queryData2 = _localStore->AllocateQuery(query);
-  XCTAssertEqual(queryData2.resume_token(), resumeToken);
-
-  // The sequence number should have been bumped when we saved the new resume token.
-  ListenSequenceNumber newSequenceNumber = queryData2.sequence_number();
-  XCTAssertGreaterThan(newSequenceNumber, initialSequenceNumber);
-}
-
-- (void)testRemoteDocumentKeysForTarget {
-  if ([self isTestBaseClass]) return;
-
-  core::Query query = Query("foo");
-  [self allocateQuery:query];
-  FSTAssertTargetID(2);
-
-  [self applyRemoteEvent:FSTTestAddedRemoteEvent(Doc("foo/baz", 10, Map("a", "b")), {2})];
-  [self applyRemoteEvent:FSTTestAddedRemoteEvent(Doc("foo/bar", 20, Map("a", "b")), {2})];
-
-  _localStore->WriteLocally({ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) });
-
-  DocumentKeySet keys = _localStore->GetRemoteDocumentKeys(2);
-  DocumentKeySet expected{testutil::Key("foo/bar"), testutil::Key("foo/baz")};
-  XCTAssertEqual(keys, expected);
-
-  keys = _localStore->GetRemoteDocumentKeys(2);
-  XCTAssertEqual(keys, (DocumentKeySet{testutil::Key("foo/bar"), testutil::Key("foo/baz")}));
-}
-
-// TODO(mrschmidt): The FieldValue.increment() field transform tests below would probably be
-// better implemented as spec tests but currently they don't support transforms.
-
-- (void)testHandlesSetMutationThenTransformMutationThenTransformMutation {
-  if ([self isTestBaseClass]) return;
-
-  [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"sum" : @0})];
-  FSTAssertContains(Doc("foo/bar", 0, Map("sum", 0), DocumentState::kLocalMutations));
-  FSTAssertChanged(Doc("foo/bar", 0, Map("sum", 0), DocumentState::kLocalMutations));
-
-  [self writeMutation:FSTTestTransformMutation(
-                          @"foo/bar", @{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1]})];
-  FSTAssertContains(Doc("foo/bar", 0, Map("sum", 1), DocumentState::kLocalMutations));
-  FSTAssertChanged(Doc("foo/bar", 0, Map("sum", 1), DocumentState::kLocalMutations));
-
-  [self writeMutation:FSTTestTransformMutation(
-                          @"foo/bar", @{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:2]})];
-  FSTAssertContains(Doc("foo/bar", 0, Map("sum", 3), DocumentState::kLocalMutations));
-  FSTAssertChanged(Doc("foo/bar", 0, Map("sum", 3), DocumentState::kLocalMutations));
-}
-
-- (void)testHandlesSetMutationThenAckThenTransformMutationThenAckThenTransformMutation {
-  if ([self isTestBaseClass]) return;
-
-  // Since this test doesn't start a listen, Eager GC removes the documents from the cache as
-  // soon as the mutation is applied. This creates a lot of special casing in this unit test but
-  // does not expand its test coverage.
-  if ([self gcIsEager]) return;
-
-  [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"sum" : @0})];
-  FSTAssertContains(Doc("foo/bar", 0, Map("sum", 0), DocumentState::kLocalMutations));
-  FSTAssertChanged(Doc("foo/bar", 0, Map("sum", 0), DocumentState::kLocalMutations));
-
-  [self acknowledgeMutationWithVersion:1];
-  FSTAssertContains(Doc("foo/bar", 1, Map("sum", 0), DocumentState::kCommittedMutations));
-  FSTAssertChanged(Doc("foo/bar", 1, Map("sum", 0), DocumentState::kCommittedMutations));
-
-  [self writeMutation:FSTTestTransformMutation(
-                          @"foo/bar", @{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1]})];
-  FSTAssertContains(Doc("foo/bar", 1, Map("sum", 1), DocumentState::kLocalMutations));
-  FSTAssertChanged(Doc("foo/bar", 1, Map("sum", 1), DocumentState::kLocalMutations));
-
-  [self acknowledgeMutationWithVersion:2 transformResult:@1];
-  FSTAssertContains(Doc("foo/bar", 2, Map("sum", 1), DocumentState::kCommittedMutations));
-  FSTAssertChanged(Doc("foo/bar", 2, Map("sum", 1), DocumentState::kCommittedMutations));
-
-  [self writeMutation:FSTTestTransformMutation(
-                          @"foo/bar", @{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:2]})];
-  FSTAssertContains(Doc("foo/bar", 2, Map("sum", 3), DocumentState::kLocalMutations));
-  FSTAssertChanged(Doc("foo/bar", 2, Map("sum", 3), DocumentState::kLocalMutations));
-}
-
-- (void)testHandlesSetMutationThenTransformMutationThenRemoteEventThenTransformMutation {
-  if ([self isTestBaseClass]) return;
-
-  core::Query query = Query("foo");
-  [self allocateQuery:query];
-  FSTAssertTargetID(2);
-
-  [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"sum" : @0})];
-  FSTAssertContains(Doc("foo/bar", 0, Map("sum", 0), DocumentState::kLocalMutations));
-  FSTAssertChanged(Doc("foo/bar", 0, Map("sum", 0), DocumentState::kLocalMutations));
-
-  [self applyRemoteEvent:FSTTestAddedRemoteEvent(Doc("foo/bar", 1, Map("sum", 0)), {2})];
-
-  [self acknowledgeMutationWithVersion:1];
-  FSTAssertContains(Doc("foo/bar", 1, Map("sum", 0)));
-  FSTAssertChanged(Doc("foo/bar", 1, Map("sum", 0)));
-
-  [self writeMutation:FSTTestTransformMutation(
-                          @"foo/bar", @{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1]})];
-  FSTAssertContains(Doc("foo/bar", 1, Map("sum", 1), DocumentState::kLocalMutations));
-  FSTAssertChanged(Doc("foo/bar", 1, Map("sum", 1), DocumentState::kLocalMutations));
-
-  // The value in this remote event gets ignored since we still have a pending transform mutation.
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(Doc("foo/bar", 2, Map("sum", 0)), {2}, {})];
-  FSTAssertContains(Doc("foo/bar", 2, Map("sum", 1), DocumentState::kLocalMutations));
-  FSTAssertChanged(Doc("foo/bar", 2, Map("sum", 1), DocumentState::kLocalMutations));
-
-  // Add another increment. Note that we still compute the increment based on the local value.
-  [self writeMutation:FSTTestTransformMutation(
-                          @"foo/bar", @{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:2]})];
-  FSTAssertContains(Doc("foo/bar", 2, Map("sum", 3), DocumentState::kLocalMutations));
-  FSTAssertChanged(Doc("foo/bar", 2, Map("sum", 3), DocumentState::kLocalMutations));
-
-  [self acknowledgeMutationWithVersion:3 transformResult:@1];
-  FSTAssertContains(Doc("foo/bar", 3, Map("sum", 3), DocumentState::kLocalMutations));
-  FSTAssertChanged(Doc("foo/bar", 3, Map("sum", 3), DocumentState::kLocalMutations));
-
-  [self acknowledgeMutationWithVersion:4 transformResult:@1339];
-  FSTAssertContains(Doc("foo/bar", 4, Map("sum", 1339), DocumentState::kCommittedMutations));
-  FSTAssertChanged(Doc("foo/bar", 4, Map("sum", 1339), DocumentState::kCommittedMutations));
-}
-
-- (void)testHoldsBackOnlyNonIdempotentTransforms {
-  if ([self isTestBaseClass]) return;
-
-  core::Query query = Query("foo");
-  [self allocateQuery:query];
-  FSTAssertTargetID(2);
-
-  [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"sum" : @0, @"array_union" : @[]})];
-  FSTAssertChanged(
-      Doc("foo/bar", 0, Map("sum", 0, "array_union", Array()), DocumentState::kLocalMutations));
-
-  [self acknowledgeMutationWithVersion:1];
-  FSTAssertChanged(
-      Doc("foo/bar", 1, Map("sum", 0, "array_union", Array()), DocumentState::kCommittedMutations));
-
-  [self applyRemoteEvent:FSTTestAddedRemoteEvent(
-                             Doc("foo/bar", 1, Map("sum", 0, "array_union", Array())), {2})];
-  FSTAssertChanged(Doc("foo/bar", 1, Map("sum", 0, "array_union", Array())));
-
-  [self writeMutations:{
-    FSTTestTransformMutation(@"foo/bar",
-                             @{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1]}),
-        FSTTestTransformMutation(
-            @"foo/bar",
-            @{@"array_union" : [FIRFieldValue fieldValueForArrayUnion:@[ @"foo" ]]})
-  }];
-
-  FSTAssertChanged(Doc("foo/bar", 1, Map("sum", 1, "array_union", Array("foo")),
-                       DocumentState::kLocalMutations));
-
-  // The sum transform is not idempotent and the backend's updated value is ignored. The
-  // ArrayUnion transform is recomputed and includes the backend value.
-  [self applyRemoteEvent:FSTTestUpdateRemoteEvent(
-                             Doc("foo/bar", 2, Map("sum", 1337, "array_union", Array("bar"))), {2},
-                             {})];
-  FSTAssertChanged(Doc("foo/bar", 2, Map("sum", 1, "array_union", Array("bar", "foo")),
-                       DocumentState::kLocalMutations));
-}
-
-- (void)testHandlesMergeMutationWithTransformThenRemoteEvent {
-  if ([self isTestBaseClass]) return;
-
-  core::Query query = Query("foo");
-  [self allocateQuery:query];
-  FSTAssertTargetID(2);
-
-  [self writeMutations:{
-    FSTTestPatchMutation("foo/bar", @{}, {firebase::firestore::testutil::Field("sum")}),
-        FSTTestTransformMutation(@"foo/bar",
-                                 @{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1]})
-  }];
-
-  FSTAssertContains(Doc("foo/bar", 0, Map("sum", 1), DocumentState::kLocalMutations));
-  FSTAssertChanged(Doc("foo/bar", 0, Map("sum", 1), DocumentState::kLocalMutations));
-
-  [self applyRemoteEvent:FSTTestAddedRemoteEvent(Doc("foo/bar", 1, Map("sum", 1337)), {2})];
-
-  FSTAssertContains(Doc("foo/bar", 1, Map("sum", 1), DocumentState::kLocalMutations));
-  FSTAssertChanged(Doc("foo/bar", 1, Map("sum", 1), DocumentState::kLocalMutations));
-}
-
-- (void)testHandlesPatchMutationWithTransformThenRemoteEvent {
-  if ([self isTestBaseClass]) return;
-
-  core::Query query = Query("foo");
-  [self allocateQuery:query];
-  FSTAssertTargetID(2);
-
-  [self writeMutations:{
-    FSTTestPatchMutation("foo/bar", @{}, {}),
-        FSTTestTransformMutation(@"foo/bar",
-                                 @{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1]})
-  }];
-
-  FSTAssertNotContains("foo/bar");
-  FSTAssertChanged(DeletedDoc("foo/bar"));
-
-  // Note: This test reflects the current behavior, but it may be preferable to replay the
-  // mutation once we receive the first value from the remote event.
-  [self applyRemoteEvent:FSTTestAddedRemoteEvent(Doc("foo/bar", 1, Map("sum", 1337)), {2})];
-
-  FSTAssertContains(Doc("foo/bar", 1, Map("sum", 1), DocumentState::kLocalMutations));
-  FSTAssertChanged(Doc("foo/bar", 1, Map("sum", 1), DocumentState::kLocalMutations));
-}
-
-- (void)testGetHighestUnacknowledgeBatchId {
-  if ([self isTestBaseClass]) return;
-
-  XCTAssertEqual(-1, _localStore->GetHighestUnacknowledgedBatchId());
-
-  [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"abc" : @123})];
-  XCTAssertEqual(1, _localStore->GetHighestUnacknowledgedBatchId());
-
-  [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"abc" : @321}, {})];
-  XCTAssertEqual(2, _localStore->GetHighestUnacknowledgedBatchId());
-
-  [self acknowledgeMutationWithVersion:1];
-  XCTAssertEqual(2, _localStore->GetHighestUnacknowledgedBatchId());
-
-  [self rejectMutation];
-  XCTAssertEqual(-1, _localStore->GetHighestUnacknowledgedBatchId());
-}
-
-- (void)testOnlyPersistsUpdatesForDocumentsWhenVersionChanges {
-  if ([self isTestBaseClass]) return;
-
-  core::Query query = Query("foo");
-  [self allocateQuery:query];
-  FSTAssertTargetID(2);
-
-  [self applyRemoteEvent:FSTTestAddedRemoteEvent(Doc("foo/bar", 1, Map("val", "old")), {2})];
-  FSTAssertContains(Doc("foo/bar", 1, Map("val", "old")));
-  FSTAssertChanged(Doc("foo/bar", 1, Map("val", "old")));
-
-  [self applyRemoteEvent:FSTTestAddedRemoteEvent({Doc("foo/bar", 1, Map("val", "new")),
-                                                  Doc("foo/baz", 2, Map("val", "new"))},
-                                                 {2})];
-  // The update to foo/bar is ignored.
-  FSTAssertContains(Doc("foo/bar", 1, Map("val", "old")));
-  FSTAssertContains(Doc("foo/baz", 2, Map("val", "new")));
-  FSTAssertChanged(Doc("foo/baz", 2, Map("val", "new")));
-}
-
-@end
-
-NS_ASSUME_NONNULL_END

+ 5 - 0
Firestore/core/test/firebase/firestore/local/CMakeLists.txt

@@ -20,6 +20,7 @@ cc_test(
     index_manager_test.h
     leveldb_index_manager_test.cc
     leveldb_key_test.cc
+    leveldb_local_store_test.cc
     leveldb_lru_garbage_collector_test.cc
     leveldb_migrations_test.cc
     leveldb_mutation_queue_test.cc
@@ -29,9 +30,12 @@ cc_test(
     leveldb_transaction_test.cc
     leveldb_util_test.cc
     local_serializer_test.cc
+    local_store_test.cc
+    local_store_test.h
     lru_garbage_collector_test.cc
     lru_garbage_collector_test.h
     memory_index_manager_test.cc
+    memory_local_store_test.cc
     memory_lru_garbage_collector_test.cc
     memory_mutation_queue_test.cc
     memory_query_cache_test.cc
@@ -54,5 +58,6 @@ cc_test(
     firebase_firestore_local_persistence_leveldb
     firebase_firestore_model
     firebase_firestore_protos_libprotobuf
+    firebase_firestore_remote_testing
     firebase_firestore_testutil
 )

+ 50 - 0
Firestore/core/test/firebase/firestore/local/leveldb_local_store_test.cc

@@ -0,0 +1,50 @@
+/*
+ * Copyright 2019 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 "Firestore/core/src/firebase/firestore/local/leveldb_persistence.h"
+#include "Firestore/core/test/firebase/firestore/local/local_store_test.h"
+#include "Firestore/core/test/firebase/firestore/local/persistence_testing.h"
+
+namespace firebase {
+namespace firestore {
+namespace local {
+namespace {
+
+class TestHelper : public LocalStoreTestHelper {
+ public:
+  std::unique_ptr<Persistence> MakePersistence() override {
+    return LevelDbPersistenceForTesting();
+  }
+
+  /** Returns true if the garbage collector is eager, false if LRU. */
+  bool IsGcEager() const override {
+    return false;
+  }
+};
+
+std::unique_ptr<LocalStoreTestHelper> Factory() {
+  return absl::make_unique<TestHelper>();
+}
+
+}  // namespace
+
+INSTANTIATE_TEST_SUITE_P(LevelDbLocalStoreTest,
+                         LocalStoreTest,
+                         ::testing::Values(Factory));
+
+}  // namespace local
+}  // namespace firestore
+}  // namespace firebase

+ 1213 - 0
Firestore/core/test/firebase/firestore/local/local_store_test.cc

@@ -0,0 +1,1213 @@
+/*
+ * Copyright 2019 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 "Firestore/core/test/firebase/firestore/local/local_store_test.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "Firestore/core/include/firebase/firestore/timestamp.h"
+#include "Firestore/core/src/firebase/firestore/auth/user.h"
+#include "Firestore/core/src/firebase/firestore/local/local_store.h"
+#include "Firestore/core/src/firebase/firestore/local/local_view_changes.h"
+#include "Firestore/core/src/firebase/firestore/local/local_write_result.h"
+#include "Firestore/core/src/firebase/firestore/local/persistence.h"
+#include "Firestore/core/src/firebase/firestore/local/query_data.h"
+#include "Firestore/core/src/firebase/firestore/model/document_map.h"
+#include "Firestore/core/src/firebase/firestore/model/document_set.h"
+#include "Firestore/core/src/firebase/firestore/model/mutation_batch_result.h"
+#include "Firestore/core/src/firebase/firestore/model/transform_mutation.h"
+#include "Firestore/core/src/firebase/firestore/model/transform_operation.h"
+#include "Firestore/core/src/firebase/firestore/remote/remote_event.h"
+#include "Firestore/core/src/firebase/firestore/remote/watch_change.h"
+#include "Firestore/core/src/firebase/firestore/util/status.h"
+#include "Firestore/core/test/firebase/firestore/remote/fake_target_metadata_provider.h"
+#include "Firestore/core/test/firebase/firestore/testutil/testutil.h"
+#include "absl/memory/memory.h"
+#include "gtest/gtest.h"
+
+namespace firebase {
+namespace firestore {
+namespace local {
+namespace {
+
+using auth::User;
+using model::Document;
+using model::DocumentKey;
+using model::DocumentKeySet;
+using model::DocumentMap;
+using model::DocumentState;
+using model::FieldValue;
+using model::ListenSequenceNumber;
+using model::MaybeDocument;
+using model::MaybeDocumentMap;
+using model::Mutation;
+using model::MutationBatch;
+using model::MutationBatchResult;
+using model::MutationResult;
+using model::NumericIncrementTransform;
+using model::ResourcePath;
+using model::SnapshotVersion;
+using model::TargetId;
+using nanopb::ByteString;
+using remote::DocumentWatchChange;
+using remote::FakeTargetMetadataProvider;
+using remote::RemoteEvent;
+using remote::WatchChangeAggregator;
+using remote::WatchTargetChange;
+using remote::WatchTargetChangeState;
+using util::Status;
+
+using testutil::Array;
+using testutil::DeletedDoc;
+using testutil::Doc;
+using testutil::Key;
+using testutil::Map;
+using testutil::Query;
+using testutil::UnknownDoc;
+using testutil::Value;
+using testutil::Vector;
+
+std::vector<MaybeDocument> DocMapToArray(const MaybeDocumentMap& docs) {
+  std::vector<MaybeDocument> result;
+  for (const auto& kv : docs) {
+    result.push_back(kv.second);
+  }
+  return result;
+}
+
+std::vector<Document> DocMapToArray(const DocumentMap& docs) {
+  std::vector<Document> result;
+  for (const auto& kv : docs.underlying_map()) {
+    result.push_back(Document(kv.second));
+  }
+  return result;
+}
+
+RemoteEvent UpdateRemoteEventWithLimboTargets(
+    const MaybeDocument& doc,
+    const std::vector<TargetId>& updated_in_targets,
+    const std::vector<TargetId>& removed_from_targets,
+    const std::vector<TargetId>& limbo_targets) {
+  HARD_ASSERT(!doc.is_document() || !Document(doc).has_local_mutations(),
+              "Docs from remote updates shouldn't have local changes.");
+  DocumentWatchChange change{updated_in_targets, removed_from_targets,
+                             doc.key(), doc};
+
+  std::vector<TargetId> listens = updated_in_targets;
+  listens.insert(listens.end(), removed_from_targets.begin(),
+                 removed_from_targets.end());
+
+  auto metadata_provider =
+      FakeTargetMetadataProvider::CreateSingleResultProvider(doc.key(), listens,
+                                                             limbo_targets);
+  WatchChangeAggregator aggregator{&metadata_provider};
+  aggregator.HandleDocumentChange(change);
+  return aggregator.CreateRemoteEvent(doc.version());
+}
+
+/** Creates a remote event that inserts a list of documents. */
+RemoteEvent AddedRemoteEvent(const std::vector<MaybeDocument>& docs,
+                             const std::vector<TargetId>& added_to_targets) {
+  HARD_ASSERT(!docs.empty(), "Cannot pass empty docs array");
+
+  const ResourcePath& collection_path = docs[0].key().path().PopLast();
+  auto metadata_provider =
+      FakeTargetMetadataProvider::CreateEmptyResultProvider(collection_path,
+                                                            added_to_targets);
+  WatchChangeAggregator aggregator{&metadata_provider};
+  for (const MaybeDocument& doc : docs) {
+    HARD_ASSERT(!doc.is_document() || !Document(doc).has_local_mutations(),
+                "Docs from remote updates shouldn't have local changes.");
+    DocumentWatchChange change{added_to_targets, {}, doc.key(), doc};
+    aggregator.HandleDocumentChange(change);
+  }
+  return aggregator.CreateRemoteEvent(docs[0].version());
+}
+
+/** Creates a remote event that inserts a new document. */
+RemoteEvent AddedRemoteEvent(const MaybeDocument& doc,
+                             const std::vector<TargetId>& added_to_targets) {
+  std::vector<MaybeDocument> docs{doc};
+  return AddedRemoteEvent(docs, added_to_targets);
+}
+
+/** Creates a remote event with changes to a document. */
+RemoteEvent UpdateRemoteEvent(
+    const MaybeDocument& doc,
+    const std::vector<TargetId>& updated_in_targets,
+    const std::vector<TargetId>& removed_from_targets) {
+  return UpdateRemoteEventWithLimboTargets(doc, updated_in_targets,
+                                           removed_from_targets, {});
+}
+
+LocalViewChanges TestViewChanges(TargetId target_id,
+                                 std::vector<std::string> added_keys,
+                                 std::vector<std::string> removed_keys) {
+  DocumentKeySet added;
+  for (const std::string& key_path : added_keys) {
+    added = added.insert(Key(key_path));
+  }
+  DocumentKeySet removed;
+  for (const std::string& key_path : removed_keys) {
+    removed = removed.insert(Key(key_path));
+  }
+  return LocalViewChanges(target_id, std::move(added), std::move(removed));
+}
+
+}  // namespace
+
+LocalStoreTest::LocalStoreTest()
+    : test_helper_(GetParam()()),
+      persistence_(test_helper_->MakePersistence()),
+      local_store_(persistence_.get(), User::Unauthenticated()) {
+  local_store_.Start();
+}
+
+void LocalStoreTest::WriteMutation(Mutation mutation) {
+  WriteMutations({std::move(mutation)});
+}
+
+void LocalStoreTest::WriteMutations(std::vector<Mutation>&& mutations) {
+  auto mutations_copy = mutations;
+  LocalWriteResult result =
+      local_store_.WriteLocally(std::move(mutations_copy));
+  batches_.emplace_back(result.batch_id(), Timestamp::Now(),
+                        std::vector<Mutation>{}, std::move(mutations));
+  last_changes_ = result.changes();
+}
+
+void LocalStoreTest::ApplyRemoteEvent(const RemoteEvent& event) {
+  last_changes_ = local_store_.ApplyRemoteEvent(event);
+}
+
+void LocalStoreTest::NotifyLocalViewChanges(LocalViewChanges changes) {
+  local_store_.NotifyLocalViewChanges(
+      std::vector<LocalViewChanges>{std::move(changes)});
+}
+
+void LocalStoreTest::AcknowledgeMutationWithVersion(
+    int64_t document_version, absl::optional<FieldValue> transform_result) {
+  ASSERT_GT(batches_.size(), 0) << "Missing batch to acknowledge.";
+  MutationBatch batch = batches_.front();
+  batches_.erase(batches_.begin());
+
+  ASSERT_EQ(batch.mutations().size(), 1)
+      << "Acknowledging more than one mutation not supported.";
+  SnapshotVersion version = testutil::Version(document_version);
+
+  absl::optional<std::vector<FieldValue>> mutation_transform_result;
+  if (transform_result) {
+    mutation_transform_result = std::vector<FieldValue>{*transform_result};
+  }
+
+  MutationResult mutation_result(version, mutation_transform_result);
+  MutationBatchResult result(batch, version, {mutation_result}, {});
+  last_changes_ = local_store_.AcknowledgeBatch(result);
+}
+
+void LocalStoreTest::RejectMutation() {
+  MutationBatch batch = batches_.front();
+  batches_.erase(batches_.begin());
+  last_changes_ = local_store_.RejectBatch(batch.batch_id());
+}
+
+TargetId LocalStoreTest::AllocateQuery(core::Query query) {
+  QueryData query_data = local_store_.AllocateQuery(std::move(query));
+  last_target_id_ = query_data.target_id();
+  return query_data.target_id();
+}
+
+/** Asserts that the last target ID is the given number. */
+#define FSTAssertTargetID(target_id)       \
+  do {                                     \
+    ASSERT_EQ(last_target_id_, target_id); \
+  } while (0)
+
+/** Asserts that a the last_changes contain the docs in the given array. */
+#define FSTAssertChanged(...)                              \
+  do {                                                     \
+    std::vector<MaybeDocument> expected = {__VA_ARGS__};   \
+    ASSERT_EQ(last_changes_.size(), expected.size());      \
+    auto last_changes_list = DocMapToArray(last_changes_); \
+    ASSERT_EQ(last_changes_list, expected);                \
+    last_changes_ = MaybeDocumentMap{};                    \
+  } while (0)
+
+/** Asserts that the given keys were removed. */
+#define FSTAssertRemoved(...)                             \
+  do {                                                    \
+    std::vector<std::string> key_paths = {__VA_ARGS__};   \
+    ASSERT_EQ(last_changes_.size(), key_paths.size());    \
+    auto key_path_iterator = key_paths.begin();           \
+    for (const auto& kv : last_changes_) {                \
+      const DocumentKey& actual_key = kv.first;           \
+      const MaybeDocument& value = kv.second;             \
+      DocumentKey expected_key = Key(*key_path_iterator); \
+      ASSERT_EQ(actual_key, expected_key);                \
+      ASSERT_TRUE(value.is_no_document());                \
+      ++key_path_iterator;                                \
+    }                                                     \
+    last_changes_ = MaybeDocumentMap{};                   \
+  } while (0)
+
+/** Asserts that the given local store contains the given document. */
+#define FSTAssertContains(document)                \
+  do {                                             \
+    MaybeDocument expected = (document);           \
+    absl::optional<MaybeDocument> actual =         \
+        local_store_.ReadDocument(expected.key()); \
+    ASSERT_EQ(actual, expected);                   \
+  } while (0)
+
+/** Asserts that the given local store does not contain the given document. */
+#define FSTAssertNotContains(key_path_string)                              \
+  do {                                                                     \
+    DocumentKey key = Key(key_path_string);                                \
+    absl::optional<MaybeDocument> actual = local_store_.ReadDocument(key); \
+    ASSERT_EQ(actual, absl::nullopt);                                      \
+  } while (0)
+
+TEST_P(LocalStoreTest, MutationBatchKeys) {
+  Mutation base = testutil::SetMutation("foo/ignore", Map("foo", "bar"));
+  Mutation set1 = testutil::SetMutation("foo/bar", Map("foo", "bar"));
+  Mutation set2 = testutil::SetMutation("bar/baz", Map("bar", "baz"));
+  MutationBatch batch =
+      MutationBatch(1, Timestamp::Now(), {base}, {set1, set2});
+  DocumentKeySet keys = batch.keys();
+  ASSERT_EQ(keys.size(), 2u);
+}
+
+TEST_P(LocalStoreTest, HandlesSetMutation) {
+  WriteMutation(testutil::SetMutation("foo/bar", Map("foo", "bar")));
+  FSTAssertChanged(
+      Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
+  FSTAssertContains(
+      Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
+
+  AcknowledgeMutationWithVersion(0);
+  FSTAssertChanged(
+      Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kCommittedMutations));
+  if (IsGcEager()) {
+    // Nothing is pinning this anymore, as it has been acknowledged and there
+    // are no targets active.
+    FSTAssertNotContains("foo/bar");
+  } else {
+    FSTAssertContains(Doc("foo/bar", 0, Map("foo", "bar"),
+                          DocumentState::kCommittedMutations));
+  }
+}
+
+TEST_P(LocalStoreTest, HandlesSetMutationThenDocument) {
+  WriteMutation(testutil::SetMutation("foo/bar", Map("foo", "bar")));
+  FSTAssertChanged(
+      Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
+  FSTAssertContains(
+      Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
+
+  TargetId target_id = AllocateQuery(Query("foo"));
+
+  ApplyRemoteEvent(UpdateRemoteEvent(Doc("foo/bar", 2, Map("it", "changed")),
+                                     {target_id}, {}));
+  FSTAssertChanged(
+      Doc("foo/bar", 2, Map("foo", "bar"), DocumentState::kLocalMutations));
+  FSTAssertContains(
+      Doc("foo/bar", 2, Map("foo", "bar"), DocumentState::kLocalMutations));
+}
+
+TEST_P(LocalStoreTest, HandlesAckThenRejectThenRemoteEvent) {
+  // Start a query that requires acks to be held.
+  core::Query query = Query("foo");
+  TargetId target_id = AllocateQuery(query);
+
+  WriteMutation(testutil::SetMutation("foo/bar", Map("foo", "bar")));
+  FSTAssertChanged(
+      Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
+  FSTAssertContains(
+      Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
+
+  // The last seen version is zero, so this ack must be held.
+  AcknowledgeMutationWithVersion(1);
+  FSTAssertChanged(
+      Doc("foo/bar", 1, Map("foo", "bar"), DocumentState::kCommittedMutations));
+
+  // Under eager GC, there is no longer a reference for the document, and it
+  // should be deleted.
+  if (IsGcEager()) {
+    FSTAssertNotContains("foo/bar");
+  } else {
+    FSTAssertContains(Doc("foo/bar", 1, Map("foo", "bar"),
+                          DocumentState::kCommittedMutations));
+  }
+
+  WriteMutation(testutil::SetMutation("bar/baz", Map("bar", "baz")));
+  FSTAssertChanged(
+      Doc("bar/baz", 0, Map("bar", "baz"), DocumentState::kLocalMutations));
+  FSTAssertContains(
+      Doc("bar/baz", 0, Map("bar", "baz"), DocumentState::kLocalMutations));
+
+  RejectMutation();
+  FSTAssertRemoved("bar/baz");
+  FSTAssertNotContains("bar/baz");
+
+  ApplyRemoteEvent(
+      AddedRemoteEvent(Doc("foo/bar", 2, Map("it", "changed")), {target_id}));
+  FSTAssertChanged(Doc("foo/bar", 2, Map("it", "changed")));
+  FSTAssertContains(Doc("foo/bar", 2, Map("it", "changed")));
+  FSTAssertNotContains("bar/baz");
+}
+
+TEST_P(LocalStoreTest, HandlesDeletedDocumentThenSetMutationThenAck) {
+  core::Query query = Query("foo");
+  TargetId target_id = AllocateQuery(query);
+
+  ApplyRemoteEvent(
+      UpdateRemoteEvent(DeletedDoc("foo/bar", 2), {target_id}, {}));
+  FSTAssertRemoved("foo/bar");
+  // Under eager GC, there is no longer a reference for the document, and it
+  // should be deleted.
+  if (!IsGcEager()) {
+    FSTAssertContains(DeletedDoc("foo/bar", 2, false));
+  } else {
+    FSTAssertNotContains("foo/bar");
+  }
+
+  WriteMutation(testutil::SetMutation("foo/bar", Map("foo", "bar")));
+  FSTAssertChanged(
+      Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
+  FSTAssertContains(
+      Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
+  // Can now remove the target, since we have a mutation pinning the document
+  local_store_.ReleaseQuery(query);
+  // Verify we didn't lose anything
+  FSTAssertContains(
+      Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
+
+  AcknowledgeMutationWithVersion(3);
+  FSTAssertChanged(
+      Doc("foo/bar", 3, Map("foo", "bar"), DocumentState::kCommittedMutations));
+  // It has been acknowledged, and should no longer be retained as there is no
+  // target and mutation
+  if (IsGcEager()) {
+    FSTAssertNotContains("foo/bar");
+  }
+}
+
+TEST_P(LocalStoreTest, HandlesSetMutationThenDeletedDocument) {
+  core::Query query = Query("foo");
+  TargetId target_id = AllocateQuery(query);
+
+  WriteMutation(testutil::SetMutation("foo/bar", Map("foo", "bar")));
+  FSTAssertChanged(
+      Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
+
+  ApplyRemoteEvent(
+      UpdateRemoteEvent(DeletedDoc("foo/bar", 2), {target_id}, {}));
+  FSTAssertChanged(
+      Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
+  FSTAssertContains(
+      Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
+}
+
+TEST_P(LocalStoreTest, HandlesDocumentThenSetMutationThenAckThenDocument) {
+  // Start a query that requires acks to be held.
+  core::Query query = Query("foo");
+  TargetId target_id = AllocateQuery(query);
+
+  ApplyRemoteEvent(
+      AddedRemoteEvent(Doc("foo/bar", 2, Map("it", "base")), {target_id}));
+  FSTAssertChanged(Doc("foo/bar", 2, Map("it", "base")));
+  FSTAssertContains(Doc("foo/bar", 2, Map("it", "base")));
+
+  WriteMutation(testutil::SetMutation("foo/bar", Map("foo", "bar")));
+  FSTAssertChanged(
+      Doc("foo/bar", 2, Map("foo", "bar"), DocumentState::kLocalMutations));
+  FSTAssertContains(
+      Doc("foo/bar", 2, Map("foo", "bar"), DocumentState::kLocalMutations));
+
+  AcknowledgeMutationWithVersion(3);
+  // we haven't seen the remote event yet, so the write is still held.
+  FSTAssertChanged(
+      Doc("foo/bar", 3, Map("foo", "bar"), DocumentState::kCommittedMutations));
+  FSTAssertContains(
+      Doc("foo/bar", 3, Map("foo", "bar"), DocumentState::kCommittedMutations));
+
+  ApplyRemoteEvent(UpdateRemoteEvent(Doc("foo/bar", 3, Map("it", "changed")),
+                                     {target_id}, {}));
+  FSTAssertChanged(Doc("foo/bar", 3, Map("it", "changed")));
+  FSTAssertContains(Doc("foo/bar", 3, Map("it", "changed")));
+}
+
+TEST_P(LocalStoreTest, HandlesPatchWithoutPriorDocument) {
+  WriteMutation(testutil::PatchMutation("foo/bar", Map("foo", "bar"), {}));
+  FSTAssertRemoved("foo/bar");
+  FSTAssertNotContains("foo/bar");
+
+  AcknowledgeMutationWithVersion(1);
+  FSTAssertChanged(UnknownDoc("foo/bar", 1));
+  if (IsGcEager()) {
+    FSTAssertNotContains("foo/bar");
+  } else {
+    FSTAssertContains(UnknownDoc("foo/bar", 1));
+  }
+}
+
+TEST_P(LocalStoreTest, HandlesPatchMutationThenDocumentThenAck) {
+  WriteMutation(testutil::PatchMutation("foo/bar", Map("foo", "bar"), {}));
+  FSTAssertRemoved("foo/bar");
+  FSTAssertNotContains("foo/bar");
+
+  core::Query query = Query("foo");
+  TargetId target_id = AllocateQuery(query);
+
+  ApplyRemoteEvent(
+      AddedRemoteEvent(Doc("foo/bar", 1, Map("it", "base")), {target_id}));
+  FSTAssertChanged(Doc("foo/bar", 1, Map("foo", "bar", "it", "base"),
+                       DocumentState::kLocalMutations));
+  FSTAssertContains(Doc("foo/bar", 1, Map("foo", "bar", "it", "base"),
+                        DocumentState::kLocalMutations));
+
+  AcknowledgeMutationWithVersion(2);
+  // We still haven't seen the remote events for the patch, so the local changes
+  // remain, and there are no changes
+  FSTAssertChanged(Doc("foo/bar", 2, Map("foo", "bar", "it", "base"),
+                       DocumentState::kCommittedMutations));
+  FSTAssertContains(Doc("foo/bar", 2, Map("foo", "bar", "it", "base"),
+                        DocumentState::kCommittedMutations));
+
+  ApplyRemoteEvent(UpdateRemoteEvent(
+      Doc("foo/bar", 2, Map("foo", "bar", "it", "base")), {target_id}, {}));
+
+  FSTAssertChanged(Doc("foo/bar", 2, Map("foo", "bar", "it", "base")));
+  FSTAssertContains(Doc("foo/bar", 2, Map("foo", "bar", "it", "base")));
+}
+
+TEST_P(LocalStoreTest, HandlesPatchMutationThenAckThenDocument) {
+  WriteMutation(testutil::PatchMutation("foo/bar", Map("foo", "bar"), {}));
+  FSTAssertRemoved("foo/bar");
+  FSTAssertNotContains("foo/bar");
+
+  AcknowledgeMutationWithVersion(1);
+  FSTAssertChanged(UnknownDoc("foo/bar", 1));
+
+  // There's no target pinning the doc, and we've ack'd the mutation.
+  if (IsGcEager()) {
+    FSTAssertNotContains("foo/bar");
+  } else {
+    FSTAssertContains(UnknownDoc("foo/bar", 1));
+  }
+
+  core::Query query = Query("foo");
+  TargetId target_id = AllocateQuery(query);
+
+  ApplyRemoteEvent(
+      UpdateRemoteEvent(Doc("foo/bar", 1, Map("it", "base")), {target_id}, {}));
+  FSTAssertChanged(Doc("foo/bar", 1, Map("it", "base")));
+  FSTAssertContains(Doc("foo/bar", 1, Map("it", "base")));
+}
+
+TEST_P(LocalStoreTest, HandlesDeleteMutationThenAck) {
+  WriteMutation(testutil::DeleteMutation("foo/bar"));
+  FSTAssertRemoved("foo/bar");
+  FSTAssertContains(DeletedDoc("foo/bar"));
+
+  AcknowledgeMutationWithVersion(1);
+  FSTAssertRemoved("foo/bar");
+  // There's no target pinning the doc, and we've ack'd the mutation.
+  if (IsGcEager()) {
+    FSTAssertNotContains("foo/bar");
+  }
+}
+
+TEST_P(LocalStoreTest, HandlesDocumentThenDeleteMutationThenAck) {
+  core::Query query = Query("foo");
+  TargetId target_id = AllocateQuery(query);
+
+  ApplyRemoteEvent(
+      UpdateRemoteEvent(Doc("foo/bar", 1, Map("it", "base")), {target_id}, {}));
+  FSTAssertChanged(Doc("foo/bar", 1, Map("it", "base")));
+  FSTAssertContains(Doc("foo/bar", 1, Map("it", "base")));
+
+  WriteMutation(testutil::DeleteMutation("foo/bar"));
+  FSTAssertRemoved("foo/bar");
+  FSTAssertContains(DeletedDoc("foo/bar"));
+
+  // Remove the target so only the mutation is pinning the document
+  local_store_.ReleaseQuery(query);
+
+  AcknowledgeMutationWithVersion(2);
+  FSTAssertRemoved("foo/bar");
+  if (IsGcEager()) {
+    // Neither the target nor the mutation pin the document, it should be gone.
+    FSTAssertNotContains("foo/bar");
+  }
+}
+
+TEST_P(LocalStoreTest, HandlesDeleteMutationThenDocumentThenAck) {
+  core::Query query = Query("foo");
+  TargetId target_id = AllocateQuery(query);
+
+  WriteMutation(testutil::DeleteMutation("foo/bar"));
+  FSTAssertRemoved("foo/bar");
+  FSTAssertContains(DeletedDoc("foo/bar"));
+
+  // Add the document to a target so it will remain in persistence even when
+  // ack'd
+  ApplyRemoteEvent(
+      UpdateRemoteEvent(Doc("foo/bar", 1, Map("it", "base")), {target_id}, {}));
+  FSTAssertRemoved("foo/bar");
+  FSTAssertContains(DeletedDoc("foo/bar"));
+
+  // Don't need to keep it pinned anymore
+  local_store_.ReleaseQuery(query);
+
+  AcknowledgeMutationWithVersion(2);
+  FSTAssertRemoved("foo/bar");
+  if (IsGcEager()) {
+    // The doc is not pinned in a target and we've acknowledged the mutation. It
+    // shouldn't exist anymore.
+    FSTAssertNotContains("foo/bar");
+  }
+}
+
+TEST_P(LocalStoreTest, HandlesDocumentThenDeletedDocumentThenDocument) {
+  core::Query query = Query("foo");
+  TargetId target_id = AllocateQuery(query);
+
+  ApplyRemoteEvent(
+      UpdateRemoteEvent(Doc("foo/bar", 1, Map("it", "base")), {target_id}, {}));
+  FSTAssertChanged(Doc("foo/bar", 1, Map("it", "base")));
+  FSTAssertContains(Doc("foo/bar", 1, Map("it", "base")));
+
+  ApplyRemoteEvent(
+      UpdateRemoteEvent(DeletedDoc("foo/bar", 2), {target_id}, {}));
+  FSTAssertRemoved("foo/bar");
+  if (!IsGcEager()) {
+    FSTAssertContains(DeletedDoc("foo/bar", 2));
+  }
+
+  ApplyRemoteEvent(UpdateRemoteEvent(Doc("foo/bar", 3, Map("it", "changed")),
+                                     {target_id}, {}));
+  FSTAssertChanged(Doc("foo/bar", 3, Map("it", "changed")));
+  FSTAssertContains(Doc("foo/bar", 3, Map("it", "changed")));
+}
+
+TEST_P(LocalStoreTest,
+       HandlesSetMutationThenPatchMutationThenDocumentThenAckThenAck) {
+  WriteMutation(testutil::SetMutation("foo/bar", Map("foo", "old")));
+  FSTAssertChanged(
+      Doc("foo/bar", 0, Map("foo", "old"), DocumentState::kLocalMutations));
+  FSTAssertContains(
+      Doc("foo/bar", 0, Map("foo", "old"), DocumentState::kLocalMutations));
+
+  WriteMutation(testutil::PatchMutation("foo/bar", Map("foo", "bar"), {}));
+  FSTAssertChanged(
+      Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
+  FSTAssertContains(
+      Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
+
+  core::Query query = Query("foo");
+  TargetId target_id = AllocateQuery(query);
+
+  ApplyRemoteEvent(
+      UpdateRemoteEvent(Doc("foo/bar", 1, Map("it", "base")), {target_id}, {}));
+  FSTAssertChanged(
+      Doc("foo/bar", 1, Map("foo", "bar"), DocumentState::kLocalMutations));
+  FSTAssertContains(
+      Doc("foo/bar", 1, Map("foo", "bar"), DocumentState::kLocalMutations));
+
+  local_store_.ReleaseQuery(query);
+  AcknowledgeMutationWithVersion(2);  // delete mutation
+  FSTAssertChanged(
+      Doc("foo/bar", 2, Map("foo", "bar"), DocumentState::kLocalMutations));
+  FSTAssertContains(
+      Doc("foo/bar", 2, Map("foo", "bar"), DocumentState::kLocalMutations));
+
+  AcknowledgeMutationWithVersion(3);  // patch mutation
+  FSTAssertChanged(
+      Doc("foo/bar", 3, Map("foo", "bar"), DocumentState::kCommittedMutations));
+  if (IsGcEager()) {
+    // we've ack'd all of the mutations, nothing is keeping this pinned anymore
+    FSTAssertNotContains("foo/bar");
+  } else {
+    FSTAssertContains(Doc("foo/bar", 3, Map("foo", "bar"),
+                          DocumentState::kCommittedMutations));
+  }
+}
+
+TEST_P(LocalStoreTest, HandlesSetMutationAndPatchMutationTogether) {
+  WriteMutations({testutil::SetMutation("foo/bar", Map("foo", "old")),
+                  testutil::PatchMutation("foo/bar", Map("foo", "bar"), {})});
+
+  FSTAssertChanged(
+      Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
+  FSTAssertContains(
+      Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
+}
+
+TEST_P(LocalStoreTest, HandlesSetMutationThenPatchMutationThenReject) {
+  if (!IsGcEager()) return;
+
+  WriteMutation(testutil::SetMutation("foo/bar", Map("foo", "old")));
+  FSTAssertContains(
+      Doc("foo/bar", 0, Map("foo", "old"), DocumentState::kLocalMutations));
+  AcknowledgeMutationWithVersion(1);
+  FSTAssertNotContains("foo/bar");
+
+  WriteMutation(testutil::PatchMutation("foo/bar", Map("foo", "bar"), {}));
+  // A blind patch is not visible in the cache
+  FSTAssertNotContains("foo/bar");
+
+  RejectMutation();
+  FSTAssertNotContains("foo/bar");
+}
+
+TEST_P(LocalStoreTest, HandlesSetMutationsAndPatchMutationOfJustOneTogether) {
+  WriteMutations({testutil::SetMutation("foo/bar", Map("foo", "old")),
+                  testutil::SetMutation("bar/baz", Map("bar", "baz")),
+                  testutil::PatchMutation("foo/bar", Map("foo", "bar"), {})});
+
+  FSTAssertChanged(
+      Doc("bar/baz", 0, Map("bar", "baz"), DocumentState::kLocalMutations),
+      Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
+  FSTAssertContains(
+      Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations));
+  FSTAssertContains(
+      Doc("bar/baz", 0, Map("bar", "baz"), DocumentState::kLocalMutations));
+}
+
+TEST_P(LocalStoreTest, HandlesDeleteMutationThenPatchMutationThenAckThenAck) {
+  WriteMutation(testutil::DeleteMutation("foo/bar"));
+  FSTAssertRemoved("foo/bar");
+  FSTAssertContains(DeletedDoc("foo/bar"));
+
+  WriteMutation(testutil::PatchMutation("foo/bar", Map("foo", "bar"), {}));
+  FSTAssertRemoved("foo/bar");
+  FSTAssertContains(DeletedDoc("foo/bar"));
+
+  AcknowledgeMutationWithVersion(2);  // delete mutation
+  FSTAssertRemoved("foo/bar");
+  FSTAssertContains(
+      DeletedDoc("foo/bar", 2, /* has_committed_mutations= */ true));
+
+  AcknowledgeMutationWithVersion(3);  // patch mutation
+  FSTAssertChanged(UnknownDoc("foo/bar", 3));
+  if (IsGcEager()) {
+    // There are no more pending mutations, the doc has been dropped
+    FSTAssertNotContains("foo/bar");
+  } else {
+    FSTAssertContains(UnknownDoc("foo/bar", 3));
+  }
+}
+
+TEST_P(LocalStoreTest, CollectsGarbageAfterChangeBatchWithNoTargetIDs) {
+  if (!IsGcEager()) return;
+
+  ApplyRemoteEvent(
+      UpdateRemoteEventWithLimboTargets(DeletedDoc("foo/bar", 2), {}, {}, {1}));
+  FSTAssertNotContains("foo/bar");
+
+  ApplyRemoteEvent(UpdateRemoteEventWithLimboTargets(
+      Doc("foo/bar", 2, Map("foo", "bar")), {}, {}, {1}));
+  FSTAssertNotContains("foo/bar");
+}
+
+TEST_P(LocalStoreTest, CollectsGarbageAfterChangeBatch) {
+  if (!IsGcEager()) return;
+
+  core::Query query = Query("foo");
+  TargetId target_id = AllocateQuery(query);
+
+  ApplyRemoteEvent(
+      AddedRemoteEvent(Doc("foo/bar", 2, Map("foo", "bar")), {target_id}));
+  FSTAssertContains(Doc("foo/bar", 2, Map("foo", "bar")));
+
+  ApplyRemoteEvent(
+      UpdateRemoteEvent(Doc("foo/bar", 2, Map("foo", "baz")), {}, {target_id}));
+
+  FSTAssertNotContains("foo/bar");
+}
+
+TEST_P(LocalStoreTest, CollectsGarbageAfterAcknowledgedMutation) {
+  if (!IsGcEager()) return;
+
+  core::Query query = Query("foo");
+  TargetId target_id = AllocateQuery(query);
+
+  ApplyRemoteEvent(
+      UpdateRemoteEvent(Doc("foo/bar", 1, Map("foo", "old")), {target_id}, {}));
+  WriteMutation(testutil::PatchMutation("foo/bar", Map("foo", "bar"), {}));
+  // Release the query so that our target count goes back to 0 and we are
+  // considered up-to-date.
+  local_store_.ReleaseQuery(query);
+
+  WriteMutation(testutil::SetMutation("foo/bah", Map("foo", "bah")));
+  WriteMutation(testutil::DeleteMutation("foo/baz"));
+  FSTAssertContains(
+      Doc("foo/bar", 1, Map("foo", "bar"), DocumentState::kLocalMutations));
+  FSTAssertContains(
+      Doc("foo/bah", 0, Map("foo", "bah"), DocumentState::kLocalMutations));
+  FSTAssertContains(DeletedDoc("foo/baz"));
+
+  AcknowledgeMutationWithVersion(3);
+  FSTAssertNotContains("foo/bar");
+  FSTAssertContains(
+      Doc("foo/bah", 0, Map("foo", "bah"), DocumentState::kLocalMutations));
+  FSTAssertContains(DeletedDoc("foo/baz"));
+
+  AcknowledgeMutationWithVersion(4);
+  FSTAssertNotContains("foo/bar");
+  FSTAssertNotContains("foo/bah");
+  FSTAssertContains(DeletedDoc("foo/baz"));
+
+  AcknowledgeMutationWithVersion(5);
+  FSTAssertNotContains("foo/bar");
+  FSTAssertNotContains("foo/bah");
+  FSTAssertNotContains("foo/baz");
+}
+
+TEST_P(LocalStoreTest, CollectsGarbageAfterRejectedMutation) {
+  if (!IsGcEager()) return;
+
+  core::Query query = Query("foo");
+  TargetId target_id = AllocateQuery(query);
+
+  ApplyRemoteEvent(
+      UpdateRemoteEvent(Doc("foo/bar", 1, Map("foo", "old")), {target_id}, {}));
+  WriteMutation(testutil::PatchMutation("foo/bar", Map("foo", "bar"), {}));
+  // Release the query so that our target count goes back to 0 and we are
+  // considered up-to-date.
+  local_store_.ReleaseQuery(query);
+
+  WriteMutation(testutil::SetMutation("foo/bah", Map("foo", "bah")));
+  WriteMutation(testutil::DeleteMutation("foo/baz"));
+  FSTAssertContains(
+      Doc("foo/bar", 1, Map("foo", "bar"), DocumentState::kLocalMutations));
+  FSTAssertContains(
+      Doc("foo/bah", 0, Map("foo", "bah"), DocumentState::kLocalMutations));
+  FSTAssertContains(DeletedDoc("foo/baz"));
+
+  RejectMutation();  // patch mutation
+  FSTAssertNotContains("foo/bar");
+  FSTAssertContains(
+      Doc("foo/bah", 0, Map("foo", "bah"), DocumentState::kLocalMutations));
+  FSTAssertContains(DeletedDoc("foo/baz"));
+
+  RejectMutation();  // set mutation
+  FSTAssertNotContains("foo/bar");
+  FSTAssertNotContains("foo/bah");
+  FSTAssertContains(DeletedDoc("foo/baz"));
+
+  RejectMutation();  // delete mutation
+  FSTAssertNotContains("foo/bar");
+  FSTAssertNotContains("foo/bah");
+  FSTAssertNotContains("foo/baz");
+}
+
+TEST_P(LocalStoreTest, PinsDocumentsInTheLocalView) {
+  if (!IsGcEager()) return;
+
+  core::Query query = Query("foo");
+  TargetId target_id = AllocateQuery(query);
+
+  ApplyRemoteEvent(
+      AddedRemoteEvent(Doc("foo/bar", 1, Map("foo", "bar")), {target_id}));
+  WriteMutation(testutil::SetMutation("foo/baz", Map("foo", "baz")));
+  FSTAssertContains(Doc("foo/bar", 1, Map("foo", "bar")));
+  FSTAssertContains(
+      Doc("foo/baz", 0, Map("foo", "baz"), DocumentState::kLocalMutations));
+
+  NotifyLocalViewChanges(
+      TestViewChanges(target_id, {"foo/bar", "foo/baz"}, {}));
+  FSTAssertContains(Doc("foo/bar", 1, Map("foo", "bar")));
+  ApplyRemoteEvent(
+      UpdateRemoteEvent(Doc("foo/bar", 1, Map("foo", "bar")), {}, {target_id}));
+  ApplyRemoteEvent(
+      UpdateRemoteEvent(Doc("foo/baz", 2, Map("foo", "baz")), {target_id}, {}));
+  FSTAssertContains(
+      Doc("foo/baz", 2, Map("foo", "baz"), DocumentState::kLocalMutations));
+  AcknowledgeMutationWithVersion(2);
+  FSTAssertContains(Doc("foo/baz", 2, Map("foo", "baz")));
+  FSTAssertContains(Doc("foo/bar", 1, Map("foo", "bar")));
+  FSTAssertContains(Doc("foo/baz", 2, Map("foo", "baz")));
+
+  NotifyLocalViewChanges(
+      TestViewChanges(target_id, {}, {"foo/bar", "foo/baz"}));
+  FSTAssertNotContains("foo/bar");
+  FSTAssertNotContains("foo/baz");
+
+  local_store_.ReleaseQuery(query);
+}
+
+TEST_P(LocalStoreTest, ThrowsAwayDocumentsWithUnknownTargetIDsImmediately) {
+  if (!IsGcEager()) return;
+
+  TargetId target_id = 321;
+  ApplyRemoteEvent(UpdateRemoteEventWithLimboTargets(Doc("foo/bar", 1, Map()),
+                                                     {}, {}, {target_id}));
+
+  FSTAssertNotContains("foo/bar");
+}
+
+TEST_P(LocalStoreTest, CanExecuteDocumentQueries) {
+  local_store_.WriteLocally(
+      {testutil::SetMutation("foo/bar", Map("foo", "bar")),
+       testutil::SetMutation("foo/baz", Map("foo", "baz")),
+       testutil::SetMutation("foo/bar/Foo/Bar", Map("Foo", "Bar"))});
+  core::Query query = Query("foo/bar");
+  DocumentMap docs = local_store_.ExecuteQuery(query);
+  ASSERT_EQ(DocMapToArray(docs), Vector(Doc("foo/bar", 0, Map("foo", "bar"),
+                                            DocumentState::kLocalMutations)));
+}
+
+TEST_P(LocalStoreTest, CanExecuteCollectionQueries) {
+  local_store_.WriteLocally(
+      {testutil::SetMutation("fo/bar", Map("fo", "bar")),
+       testutil::SetMutation("foo/bar", Map("foo", "bar")),
+       testutil::SetMutation("foo/baz", Map("foo", "baz")),
+       testutil::SetMutation("foo/bar/Foo/Bar", Map("Foo", "Bar")),
+       testutil::SetMutation("fooo/blah", Map("fooo", "blah"))});
+  core::Query query = Query("foo");
+  DocumentMap docs = local_store_.ExecuteQuery(query);
+  ASSERT_EQ(DocMapToArray(docs), Vector(Doc("foo/bar", 0, Map("foo", "bar"),
+                                            DocumentState::kLocalMutations),
+                                        Doc("foo/baz", 0, Map("foo", "baz"),
+                                            DocumentState::kLocalMutations)));
+}
+
+TEST_P(LocalStoreTest, CanExecuteMixedCollectionQueries) {
+  core::Query query = Query("foo");
+  AllocateQuery(query);
+  FSTAssertTargetID(2);
+
+  ApplyRemoteEvent(
+      UpdateRemoteEvent(Doc("foo/baz", 10, Map("a", "b")), {2}, {}));
+  ApplyRemoteEvent(
+      UpdateRemoteEvent(Doc("foo/bar", 20, Map("a", "b")), {2}, {}));
+
+  local_store_.WriteLocally({testutil::SetMutation("foo/bonk", Map("a", "b"))});
+
+  DocumentMap docs = local_store_.ExecuteQuery(query);
+  ASSERT_EQ(DocMapToArray(docs), Vector(Doc("foo/bar", 20, Map("a", "b")),
+                                        Doc("foo/baz", 10, Map("a", "b")),
+                                        Doc("foo/bonk", 0, Map("a", "b"),
+                                            DocumentState::kLocalMutations)));
+}
+
+TEST_P(LocalStoreTest, PersistsResumeTokens) {
+  // This test only works in the absence of the FSTEagerGarbageCollector.
+  if (IsGcEager()) return;
+
+  core::Query query = Query("foo/bar");
+  QueryData query_data = local_store_.AllocateQuery(query);
+  ListenSequenceNumber initial_sequence_number = query_data.sequence_number();
+  TargetId target_id = query_data.target_id();
+  ByteString resume_token = testutil::ResumeToken(1000);
+
+  WatchTargetChange watch_change{
+      WatchTargetChangeState::Current, {target_id}, resume_token};
+  auto metadata_provider =
+      FakeTargetMetadataProvider::CreateSingleResultProvider(
+          testutil::Key("foo/bar"), std::vector<TargetId>{target_id});
+  WatchChangeAggregator aggregator{&metadata_provider};
+  aggregator.HandleTargetChange(watch_change);
+  RemoteEvent remote_event =
+      aggregator.CreateRemoteEvent(testutil::Version(1000));
+  ApplyRemoteEvent(remote_event);
+
+  // Stop listening so that the query should become inactive (but persistent)
+  local_store_.ReleaseQuery(query);
+
+  // Should come back with the same resume token
+  QueryData query_data2 = local_store_.AllocateQuery(query);
+  ASSERT_EQ(query_data2.resume_token(), resume_token);
+
+  // The sequence number should have been bumped when we saved the new resume
+  // token.
+  ListenSequenceNumber new_sequence_number = query_data2.sequence_number();
+  ASSERT_GT(new_sequence_number, initial_sequence_number);
+}
+
+TEST_P(LocalStoreTest, RemoteDocumentKeysForTarget) {
+  core::Query query = Query("foo");
+  AllocateQuery(query);
+  FSTAssertTargetID(2);
+
+  ApplyRemoteEvent(AddedRemoteEvent(Doc("foo/baz", 10, Map("a", "b")), {2}));
+  ApplyRemoteEvent(AddedRemoteEvent(Doc("foo/bar", 20, Map("a", "b")), {2}));
+
+  local_store_.WriteLocally({testutil::SetMutation("foo/bonk", Map("a", "b"))});
+
+  DocumentKeySet keys = local_store_.GetRemoteDocumentKeys(2);
+  DocumentKeySet expected{testutil::Key("foo/bar"), testutil::Key("foo/baz")};
+  ASSERT_EQ(keys, expected);
+
+  keys = local_store_.GetRemoteDocumentKeys(2);
+  ASSERT_EQ(keys, (DocumentKeySet{testutil::Key("foo/bar"),
+                                  testutil::Key("foo/baz")}));
+}
+
+// TODO(mrschmidt): The FieldValue.increment() field transform tests below would
+// probably be better implemented as spec tests but currently they don't support
+// transforms.
+
+TEST_P(LocalStoreTest,
+       HandlesSetMutationThenTransformMutationThenTransformMutation) {
+  WriteMutation(testutil::SetMutation("foo/bar", Map("sum", 0)));
+  FSTAssertContains(
+      Doc("foo/bar", 0, Map("sum", 0), DocumentState::kLocalMutations));
+  FSTAssertChanged(
+      Doc("foo/bar", 0, Map("sum", 0), DocumentState::kLocalMutations));
+
+  WriteMutation(testutil::TransformMutation(
+      "foo/bar", {testutil::Increment("sum", Value(1))}));
+  FSTAssertContains(
+      Doc("foo/bar", 0, Map("sum", 1), DocumentState::kLocalMutations));
+  FSTAssertChanged(
+      Doc("foo/bar", 0, Map("sum", 1), DocumentState::kLocalMutations));
+
+  WriteMutation(testutil::TransformMutation(
+      "foo/bar", {testutil::Increment("sum", Value(2))}));
+  FSTAssertContains(
+      Doc("foo/bar", 0, Map("sum", 3), DocumentState::kLocalMutations));
+  FSTAssertChanged(
+      Doc("foo/bar", 0, Map("sum", 3), DocumentState::kLocalMutations));
+}
+
+TEST_P(
+    LocalStoreTest,
+    HandlesSetMutationThenAckThenTransformMutationThenAckThenTransformMutation) {  // NOLINT
+  // Since this test doesn't start a listen, Eager GC removes the documents from
+  // the cache as soon as the mutation is applied. This creates a lot of special
+  // casing in this unit test but does not expand its test coverage.
+  if (IsGcEager()) return;
+
+  WriteMutation(testutil::SetMutation("foo/bar", Map("sum", 0)));
+  FSTAssertContains(
+      Doc("foo/bar", 0, Map("sum", 0), DocumentState::kLocalMutations));
+  FSTAssertChanged(
+      Doc("foo/bar", 0, Map("sum", 0), DocumentState::kLocalMutations));
+
+  AcknowledgeMutationWithVersion(1);
+  FSTAssertContains(
+      Doc("foo/bar", 1, Map("sum", 0), DocumentState::kCommittedMutations));
+  FSTAssertChanged(
+      Doc("foo/bar", 1, Map("sum", 0), DocumentState::kCommittedMutations));
+
+  WriteMutation(testutil::TransformMutation(
+      "foo/bar", {testutil::Increment("sum", Value(1))}));
+  FSTAssertContains(
+      Doc("foo/bar", 1, Map("sum", 1), DocumentState::kLocalMutations));
+  FSTAssertChanged(
+      Doc("foo/bar", 1, Map("sum", 1), DocumentState::kLocalMutations));
+
+  AcknowledgeMutationWithVersion(2, Value(1));
+  FSTAssertContains(
+      Doc("foo/bar", 2, Map("sum", 1), DocumentState::kCommittedMutations));
+  FSTAssertChanged(
+      Doc("foo/bar", 2, Map("sum", 1), DocumentState::kCommittedMutations));
+
+  WriteMutation(testutil::TransformMutation(
+      "foo/bar", {testutil::Increment("sum", Value(2))}));
+  FSTAssertContains(
+      Doc("foo/bar", 2, Map("sum", 3), DocumentState::kLocalMutations));
+  FSTAssertChanged(
+      Doc("foo/bar", 2, Map("sum", 3), DocumentState::kLocalMutations));
+}
+
+TEST_P(
+    LocalStoreTest,
+    HandlesSetMutationThenTransformMutationThenRemoteEventThenTransformMutation) {  // NOLINT
+  core::Query query = Query("foo");
+  AllocateQuery(query);
+  FSTAssertTargetID(2);
+
+  WriteMutation(testutil::SetMutation("foo/bar", Map("sum", 0)));
+  FSTAssertContains(
+      Doc("foo/bar", 0, Map("sum", 0), DocumentState::kLocalMutations));
+  FSTAssertChanged(
+      Doc("foo/bar", 0, Map("sum", 0), DocumentState::kLocalMutations));
+
+  ApplyRemoteEvent(AddedRemoteEvent(Doc("foo/bar", 1, Map("sum", 0)), {2}));
+
+  AcknowledgeMutationWithVersion(1);
+  FSTAssertContains(Doc("foo/bar", 1, Map("sum", 0)));
+  FSTAssertChanged(Doc("foo/bar", 1, Map("sum", 0)));
+
+  WriteMutation(testutil::TransformMutation(
+      "foo/bar", {testutil::Increment("sum", Value(1))}));
+  FSTAssertContains(
+      Doc("foo/bar", 1, Map("sum", 1), DocumentState::kLocalMutations));
+  FSTAssertChanged(
+      Doc("foo/bar", 1, Map("sum", 1), DocumentState::kLocalMutations));
+
+  // The value in this remote event gets ignored since we still have a pending
+  // transform mutation.
+  ApplyRemoteEvent(
+      UpdateRemoteEvent(Doc("foo/bar", 2, Map("sum", 0)), {2}, {}));
+  FSTAssertContains(
+      Doc("foo/bar", 2, Map("sum", 1), DocumentState::kLocalMutations));
+  FSTAssertChanged(
+      Doc("foo/bar", 2, Map("sum", 1), DocumentState::kLocalMutations));
+
+  // Add another increment. Note that we still compute the increment based on
+  // the local value.
+  WriteMutation(testutil::TransformMutation(
+      "foo/bar", {testutil::Increment("sum", Value(2))}));
+  FSTAssertContains(
+      Doc("foo/bar", 2, Map("sum", 3), DocumentState::kLocalMutations));
+  FSTAssertChanged(
+      Doc("foo/bar", 2, Map("sum", 3), DocumentState::kLocalMutations));
+
+  AcknowledgeMutationWithVersion(3, Value(1));
+  FSTAssertContains(
+      Doc("foo/bar", 3, Map("sum", 3), DocumentState::kLocalMutations));
+  FSTAssertChanged(
+      Doc("foo/bar", 3, Map("sum", 3), DocumentState::kLocalMutations));
+
+  AcknowledgeMutationWithVersion(4, Value(1339));
+  FSTAssertContains(
+      Doc("foo/bar", 4, Map("sum", 1339), DocumentState::kCommittedMutations));
+  FSTAssertChanged(
+      Doc("foo/bar", 4, Map("sum", 1339), DocumentState::kCommittedMutations));
+}
+
+TEST_P(LocalStoreTest, HoldsBackOnlyNonIdempotentTransforms) {
+  core::Query query = Query("foo");
+  AllocateQuery(query);
+  FSTAssertTargetID(2);
+
+  WriteMutation(
+      testutil::SetMutation("foo/bar", Map("sum", 0, "array_union", Array())));
+  FSTAssertChanged(Doc("foo/bar", 0, Map("sum", 0, "array_union", Array()),
+                       DocumentState::kLocalMutations));
+
+  AcknowledgeMutationWithVersion(1);
+  FSTAssertChanged(Doc("foo/bar", 1, Map("sum", 0, "array_union", Array()),
+                       DocumentState::kCommittedMutations));
+
+  ApplyRemoteEvent(AddedRemoteEvent(
+      Doc("foo/bar", 1, Map("sum", 0, "array_union", Array())), {2}));
+  FSTAssertChanged(Doc("foo/bar", 1, Map("sum", 0, "array_union", Array())));
+
+  WriteMutations({
+      testutil::TransformMutation("foo/bar",
+                                  {testutil::Increment("sum", Value(1))}),
+      testutil::TransformMutation(
+          "foo/bar", {testutil::ArrayUnion("array_union", {Value("foo")})}),
+  });
+
+  FSTAssertChanged(Doc("foo/bar", 1, Map("sum", 1, "array_union", Array("foo")),
+                       DocumentState::kLocalMutations));
+
+  // The sum transform is not idempotent and the backend's updated value is
+  // ignored. The ArrayUnion transform is recomputed and includes the backend
+  // value.
+  ApplyRemoteEvent(UpdateRemoteEvent(
+      Doc("foo/bar", 2, Map("sum", 1337, "array_union", Array("bar"))), {2},
+      {}));
+  FSTAssertChanged(Doc("foo/bar", 2,
+                       Map("sum", 1, "array_union", Array("bar", "foo")),
+                       DocumentState::kLocalMutations));
+}
+
+TEST_P(LocalStoreTest, HandlesMergeMutationWithTransformThenRemoteEvent) {
+  core::Query query = Query("foo");
+  AllocateQuery(query);
+  FSTAssertTargetID(2);
+
+  WriteMutations({
+      testutil::PatchMutation("foo/bar", Map(),
+                              {firebase::firestore::testutil::Field("sum")}),
+      testutil::TransformMutation("foo/bar",
+                                  {testutil::Increment("sum", Value(1))}),
+  });
+
+  FSTAssertContains(
+      Doc("foo/bar", 0, Map("sum", 1), DocumentState::kLocalMutations));
+  FSTAssertChanged(
+      Doc("foo/bar", 0, Map("sum", 1), DocumentState::kLocalMutations));
+
+  ApplyRemoteEvent(AddedRemoteEvent(Doc("foo/bar", 1, Map("sum", 1337)), {2}));
+
+  FSTAssertContains(
+      Doc("foo/bar", 1, Map("sum", 1), DocumentState::kLocalMutations));
+  FSTAssertChanged(
+      Doc("foo/bar", 1, Map("sum", 1), DocumentState::kLocalMutations));
+}
+
+TEST_P(LocalStoreTest, HandlesPatchMutationWithTransformThenRemoteEvent) {
+  core::Query query = Query("foo");
+  AllocateQuery(query);
+  FSTAssertTargetID(2);
+
+  WriteMutations({
+      testutil::PatchMutation("foo/bar", Map(), {}),
+      testutil::TransformMutation("foo/bar",
+                                  {testutil::Increment("sum", Value(1))}),
+  });
+
+  FSTAssertNotContains("foo/bar");
+  FSTAssertChanged(DeletedDoc("foo/bar"));
+
+  // Note: This test reflects the current behavior, but it may be preferable to
+  // replay the mutation once we receive the first value from the remote event.
+  ApplyRemoteEvent(AddedRemoteEvent(Doc("foo/bar", 1, Map("sum", 1337)), {2}));
+
+  FSTAssertContains(
+      Doc("foo/bar", 1, Map("sum", 1), DocumentState::kLocalMutations));
+  FSTAssertChanged(
+      Doc("foo/bar", 1, Map("sum", 1), DocumentState::kLocalMutations));
+}
+
+TEST_P(LocalStoreTest, GetHighestUnacknowledgeBatchId) {
+  ASSERT_EQ(-1, local_store_.GetHighestUnacknowledgedBatchId());
+
+  WriteMutation(testutil::SetMutation("foo/bar", Map("abc", 123)));
+  ASSERT_EQ(1, local_store_.GetHighestUnacknowledgedBatchId());
+
+  WriteMutation(testutil::PatchMutation("foo/bar", Map("abc", 321), {}));
+  ASSERT_EQ(2, local_store_.GetHighestUnacknowledgedBatchId());
+
+  AcknowledgeMutationWithVersion(1);
+  ASSERT_EQ(2, local_store_.GetHighestUnacknowledgedBatchId());
+
+  RejectMutation();
+  ASSERT_EQ(-1, local_store_.GetHighestUnacknowledgedBatchId());
+}
+
+TEST_P(LocalStoreTest, OnlyPersistsUpdatesForDocumentsWhenVersionChanges) {
+  core::Query query = Query("foo");
+  AllocateQuery(query);
+  FSTAssertTargetID(2);
+
+  ApplyRemoteEvent(AddedRemoteEvent(Doc("foo/bar", 1, Map("val", "old")), {2}));
+  FSTAssertContains(Doc("foo/bar", 1, Map("val", "old")));
+  FSTAssertChanged(Doc("foo/bar", 1, Map("val", "old")));
+
+  ApplyRemoteEvent(AddedRemoteEvent({Doc("foo/bar", 1, Map("val", "new")),
+                                     Doc("foo/baz", 2, Map("val", "new"))},
+                                    {2}));
+  // The update to foo/bar is ignored.
+  FSTAssertContains(Doc("foo/bar", 1, Map("val", "old")));
+  FSTAssertContains(Doc("foo/baz", 2, Map("val", "new")));
+  FSTAssertChanged(Doc("foo/baz", 2, Map("val", "new")));
+}
+
+}  // namespace local
+}  // namespace firestore
+}  // namespace firebase

+ 105 - 0
Firestore/core/test/firebase/firestore/local/local_store_test.h

@@ -0,0 +1,105 @@
+/*
+ * Copyright 2019 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_LOCAL_LOCAL_STORE_TEST_H_
+#define FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_LOCAL_LOCAL_STORE_TEST_H_
+
+#include <memory>
+#include <vector>
+
+#include "Firestore/core/src/firebase/firestore/local/local_store.h"
+#include "Firestore/core/src/firebase/firestore/model/document_map.h"
+#include "Firestore/core/src/firebase/firestore/model/mutation_batch.h"
+#include "gtest/gtest.h"
+
+namespace firebase {
+namespace firestore {
+
+namespace core {
+class Query;
+}
+
+namespace remote {
+class RemoteEvent;
+}
+
+namespace local {
+
+class Persistence;
+class LocalStore;
+class LocalViewChanges;
+
+/**
+ * A set of helper methods needed by LocalStoreTest that customize it to the
+ * specific implementation it is testing.
+ */
+class LocalStoreTestHelper {
+ public:
+  virtual ~LocalStoreTestHelper() = default;
+
+  /** Creates a new instance of Persistence. */
+  virtual std::unique_ptr<Persistence> MakePersistence() = 0;
+
+  /** Returns true if the garbage collector is eager, false if LRU. */
+  virtual bool IsGcEager() const = 0;
+};
+
+using FactoryFunc = std::unique_ptr<LocalStoreTestHelper> (*)();
+
+/**
+ * These are tests for any configuration of the LocalStore.
+ *
+ * To test a specific configuration of LocalStore:
+ *
+ * + Write an implementation of LocalStoreTestHelper
+ * + Call INSTANTIATE_TEST_SUITE_P(MyNewLocalStoreTest,
+ *                                 LocalStoreTest,
+ *                                 testing::Values(MyNewLocalStoreTestHelper));
+ */
+class LocalStoreTest : public ::testing::TestWithParam<FactoryFunc> {
+ protected:
+  LocalStoreTest();
+
+  bool IsGcEager() const {
+    return test_helper_->IsGcEager();
+  }
+
+  void WriteMutation(model::Mutation mutation);
+  void WriteMutations(std::vector<model::Mutation>&& mutations);
+
+  void ApplyRemoteEvent(const remote::RemoteEvent& event);
+  void NotifyLocalViewChanges(LocalViewChanges changes);
+  void AcknowledgeMutationWithVersion(
+      int64_t document_version,
+      absl::optional<model::FieldValue> transform_result = absl::nullopt);
+  void RejectMutation();
+  model::TargetId AllocateQuery(core::Query query);
+
+  std::unique_ptr<LocalStoreTestHelper> test_helper_;
+
+  std::unique_ptr<Persistence> persistence_;
+  LocalStore local_store_;
+  std::vector<model::MutationBatch> batches_;
+  model::MaybeDocumentMap last_changes_;
+
+  model::TargetId last_target_id_ = 0;
+};
+
+}  // namespace local
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_LOCAL_LOCAL_STORE_TEST_H_

+ 28 - 25
Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.mm → Firestore/core/test/firebase/firestore/local/memory_local_store_test.cc

@@ -1,5 +1,5 @@
 /*
- * Copyright 2017 Google
+ * Copyright 2019 Google
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,35 +14,38 @@
  * limitations under the License.
  */
 
-#import "Firestore/Example/Tests/Local/FSTLocalStoreTests.h"
-
 #include "Firestore/core/src/firebase/firestore/local/memory_persistence.h"
 #include "Firestore/core/src/firebase/firestore/local/reference_delegate.h"
+#include "Firestore/core/test/firebase/firestore/local/local_store_test.h"
 #include "Firestore/core/test/firebase/firestore/local/persistence_testing.h"
 
-NS_ASSUME_NONNULL_BEGIN
-
-using firebase::firestore::local::MemoryPersistenceWithEagerGcForTesting;
-using firebase::firestore::local::Persistence;
-
-/**
- * This tests the FSTLocalStore with an FSTMemoryPersistence persistence implementation. The tests
- * are in FSTLocalStoreTests and this class is merely responsible for creating a new Persistence
- * implementation on demand.
- */
-@interface FSTMemoryLocalStoreTests : FSTLocalStoreTests
-@end
-
-@implementation FSTMemoryLocalStoreTests
-
-- (std::unique_ptr<Persistence>)persistence {
-  return MemoryPersistenceWithEagerGcForTesting();
+namespace firebase {
+namespace firestore {
+namespace local {
+namespace {
+
+class TestHelper : public LocalStoreTestHelper {
+ public:
+  std::unique_ptr<Persistence> MakePersistence() override {
+    return MemoryPersistenceWithEagerGcForTesting();
+  }
+
+  /** Returns true if the garbage collector is eager, false if LRU. */
+  bool IsGcEager() const override {
+    return true;
+  }
+};
+
+std::unique_ptr<LocalStoreTestHelper> Factory() {
+  return absl::make_unique<TestHelper>();
 }
 
-- (BOOL)gcIsEager {
-  return YES;
-}
+}  // namespace
 
-@end
+INSTANTIATE_TEST_SUITE_P(MemoryLocalStoreTest,
+                         LocalStoreTest,
+                         ::testing::Values(Factory));
 
-NS_ASSUME_NONNULL_END
+}  // namespace local
+}  // namespace firestore
+}  // namespace firebase

+ 17 - 0
Firestore/core/test/firebase/firestore/testutil/testutil.cc

@@ -93,6 +93,23 @@ model::TransformMutation TransformMutation(
   return model::TransformMutation(Key(key), std::move(field_transforms));
 }
 
+std::pair<std::string, model::TransformOperation> Increment(
+    std::string field, model::FieldValue operand) {
+  model::NumericIncrementTransform transform(std::move(operand));
+
+  return std::pair<std::string, model::TransformOperation>(
+      std::move(field), std::move(transform));
+}
+
+std::pair<std::string, model::TransformOperation> ArrayUnion(
+    std::string field, std::vector<model::FieldValue> operands) {
+  model::ArrayTransform transform(model::TransformOperation::Type::ArrayUnion,
+                                  std::move(operands));
+
+  return std::pair<std::string, model::TransformOperation>(
+      std::move(field), std::move(transform));
+}
+
 }  // namespace testutil
 }  // namespace firestore
 }  // namespace firebase

+ 19 - 4
Firestore/core/test/firebase/firestore/testutil/testutil.h

@@ -49,11 +49,10 @@
 
 namespace firebase {
 namespace firestore {
-namespace model {
 
+namespace model {
 class TransformMutation;
 class TransformOperation;
-
 }  // namespace model
 
 namespace testutil {
@@ -124,7 +123,7 @@ inline model::FieldValue Value(double value) {
 }
 
 inline model::FieldValue Value(Timestamp value) {
-  return model::FieldValue::FromTimestamp(std::move(value));
+  return model::FieldValue::FromTimestamp(value);
 }
 
 inline model::FieldValue Value(const char* value) {
@@ -418,7 +417,7 @@ inline core::OrderBy OrderBy(absl::string_view key,
 
 inline core::OrderBy OrderBy(model::FieldPath field_path,
                              core::Direction direction) {
-  return core::OrderBy(std::move(field_path), std::move(direction));
+  return core::OrderBy(std::move(field_path), direction);
 }
 
 inline core::Query Query(absl::string_view path) {
@@ -446,6 +445,22 @@ model::TransformMutation TransformMutation(
     absl::string_view path,
     std::vector<std::pair<std::string, model::TransformOperation>> transforms);
 
+/**
+ * Creates a pair of field name, TransformOperation that represents a numeric
+ * increment on the given field, suitable for passing to TransformMutation,
+ * above.
+ */
+std::pair<std::string, model::TransformOperation> Increment(
+    std::string field, model::FieldValue operand);
+
+/**
+ * Creates a pair of field name, TransformOperation that represents an array
+ * union on the given field, suitable for passing to TransformMutation,
+ * above.
+ */
+std::pair<std::string, model::TransformOperation> ArrayUnion(
+    std::string field, std::vector<model::FieldValue> operands);
+
 inline model::DeleteMutation DeleteMutation(absl::string_view path) {
   return model::DeleteMutation(Key(path), model::Precondition::None());
 }