diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index 6f6e25e705b..2ad7125dc7c 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -145,6 +145,7 @@ 1733601ECCEA33E730DEAF45 /* autoid_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54740A521FC913E500713A1A /* autoid_test.cc */; }; 17473086EBACB98CDC3CC65C /* view_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = C7429071B33BDF80A7FA2F8A /* view_test.cc */; }; 17638F813B9B556FE7718C0C /* FIRQuerySnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04F202154AA00B64F25 /* FIRQuerySnapshotTests.mm */; }; + 179B8752CF4255263B9986FD /* testing_hooks_util.cc in Sources */ = {isa = PBXBuildFile; fileRef = F1ADF4E1991C352F0ECCE1E7 /* testing_hooks_util.cc */; }; 17DC97DE15D200932174EC1F /* defer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 8ABAC2E0402213D837F73DC3 /* defer_test.cc */; }; 17DFF30CF61D87883986E8B6 /* executor_std_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4687208F9B9100554BA2 /* executor_std_test.cc */; }; 17ECB768DA44AE0F49647E22 /* memory_query_engine_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 8EF6A33BC2D84233C355F1D0 /* memory_query_engine_test.cc */; }; @@ -157,6 +158,7 @@ 198F193BD9484E49375A7BE7 /* FSTHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E03A2021401F00B64F25 /* FSTHelpers.mm */; }; 199B778D5820495797E0BE02 /* filesystem_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F51859B394D01C0C507282F1 /* filesystem_test.cc */; }; 1A1299107EFF68DA9DAB19BD /* leveldb_overlay_migration_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = D8A6D52723B1BABE1B7B8D8F /* leveldb_overlay_migration_manager_test.cc */; }; + 1A21C6A5AC8A153E96F91566 /* testing_hooks_util.cc in Sources */ = {isa = PBXBuildFile; fileRef = F1ADF4E1991C352F0ECCE1E7 /* testing_hooks_util.cc */; }; 1A3D8028303B45FCBB21CAD3 /* aggregation_result.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = D872D754B8AD88E28AF28B28 /* aggregation_result.pb.cc */; }; 1B4794A51F4266556CD0976B /* view_snapshot_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CC572A9168BBEF7B83E4BBC5 /* view_snapshot_test.cc */; }; 1B6E74BA33B010D76DB1E2F9 /* FIRGeoPointTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E048202154AA00B64F25 /* FIRGeoPointTests.mm */; }; @@ -210,11 +212,14 @@ 23EFC681986488B033C2B318 /* leveldb_opener_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 75860CD13AF47EB1EA39EC2F /* leveldb_opener_test.cc */; }; 2428E92E063EBAEA44BA5913 /* target_index_matcher_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 63136A2371C0C013EC7A540C /* target_index_matcher_test.cc */; }; 248DE4F56DD938F4DBCCF39B /* bundle_reader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6ECAF7DE28A19C69DF386D88 /* bundle_reader_test.cc */; }; + 24B75C63BDCD5551B2F69901 /* testing_hooks_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A002425BC4FC4E805F4175B6 /* testing_hooks_test.cc */; }; 24CB39421C63CD87242B31DF /* bundle_reader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6ECAF7DE28A19C69DF386D88 /* bundle_reader_test.cc */; }; 254CD651CB621D471BC5AC12 /* target_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B5C37696557C81A6C2B7271A /* target_cache_test.cc */; }; 258B372CF33B7E7984BBA659 /* fake_target_metadata_provider.cc in Sources */ = {isa = PBXBuildFile; fileRef = 71140E5D09C6E76F7C71B2FC /* fake_target_metadata_provider.cc */; }; + 258F2B14F7E4C615707E67B1 /* testing_hooks_util.cc in Sources */ = {isa = PBXBuildFile; fileRef = F1ADF4E1991C352F0ECCE1E7 /* testing_hooks_util.cc */; }; 25A75DFA730BAD21A5538EC5 /* document.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D821C2DDC800EFB9CC /* document.pb.cc */; }; 25C167BAA4284FC951206E1F /* FIRFirestoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5467FAFF203E56F8009C9584 /* FIRFirestoreTests.mm */; }; + 25DCB9BD1C681C6611A17164 /* testing_hooks_util.cc in Sources */ = {isa = PBXBuildFile; fileRef = F1ADF4E1991C352F0ECCE1E7 /* testing_hooks_util.cc */; }; 25FE27330996A59F31713A0C /* FIRDocumentReferenceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E049202154AA00B64F25 /* FIRDocumentReferenceTests.mm */; }; 2620644052E960310DADB298 /* FIRFieldValueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04A202154AA00B64F25 /* FIRFieldValueTests.mm */; }; 2634E1C1971C05790B505824 /* resource_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2B02024FFD70028D6BE /* resource_path_test.cc */; }; @@ -435,6 +440,7 @@ 518BF03D57FBAD7C632D18F8 /* FIRQueryUnitTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = FF73B39D04D1760190E6B84A /* FIRQueryUnitTests.mm */; }; 52967C3DD7896BFA48840488 /* byte_string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5342CDDB137B4E93E2E85CCA /* byte_string_test.cc */; }; 529AB59F636060FEA21BD4FF /* garbage_collection_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = AAED89D7690E194EF3BA1132 /* garbage_collection_spec_test.json */; }; + 5360D52DCAD1069B1E4B0B9D /* testing_hooks_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A002425BC4FC4E805F4175B6 /* testing_hooks_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 */; }; 53F449F69DF8A3ABC711FD59 /* secure_random_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54740A531FC913E500713A1A /* secure_random_test.cc */; }; @@ -662,6 +668,7 @@ 6359EA7D5C76D462BD31B5E5 /* watch_change_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2D7472BC70C024D736FF74D9 /* watch_change_test.cc */; }; 6380CACCF96A9B26900983DC /* leveldb_target_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = E76F0CDF28E5FA62D21DE648 /* leveldb_target_cache_test.cc */; }; 63B91FC476F3915A44F00796 /* query.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D621C2DDC800EFB9CC /* query.pb.cc */; }; + 64055C7B3BF2C4106714DD3C /* testing_hooks_util.cc in Sources */ = {isa = PBXBuildFile; fileRef = F1ADF4E1991C352F0ECCE1E7 /* testing_hooks_util.cc */; }; 650B31A5EC6F8D2AEA79C350 /* index_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AE4A9E38D65688EE000EE2A1 /* index_manager_test.cc */; }; 65537B22A73E3909666FB5BC /* remote_document_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7EB299CF85034F09CFD6F3FD /* remote_document_cache_test.cc */; }; 65D54B964A2021E5A36AB21F /* bundle_loader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A853C81A6A5A51C9D0389EDA /* bundle_loader_test.cc */; }; @@ -913,6 +920,7 @@ 9A8B01AF6F19D248202FBC0A /* FIRQueryUnitTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = FF73B39D04D1760190E6B84A /* FIRQueryUnitTests.mm */; }; 9AC28D928902C6767A11F5FC /* objc_type_traits_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2A0CF41BA5AED6049B0BEB2C /* objc_type_traits_apple_test.mm */; }; 9AC604BF7A76CABDF26F8C8E /* cc_compilation_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1B342370EAE3AA02393E33EB /* cc_compilation_test.cc */; }; + 9B2C6A48A4DBD36080932B4E /* testing_hooks_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A002425BC4FC4E805F4175B6 /* testing_hooks_test.cc */; }; 9B2CD4CBB1DFE8BC3C81A335 /* async_queue_libdispatch_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4680208EA0BE00554BA2 /* async_queue_libdispatch_test.mm */; }; 9B9BFC16E26BDE4AE0CDFF4B /* firebase_auth_credentials_provider_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = F869D85E900E5AF6CD02E2FC /* firebase_auth_credentials_provider_test.mm */; }; 9BEC62D59EB2C68342F493CD /* credentials_provider_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2F4FA4576525144C5069A7A5 /* credentials_provider_test.cc */; }; @@ -1115,6 +1123,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 */; }; C1F8991BD11FFD705D74244F /* random_access_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 014C60628830D95031574D15 /* random_access_queue_test.cc */; }; + C201BE60B8717CDCBEEDF3F0 /* testing_hooks_util.cc in Sources */ = {isa = PBXBuildFile; fileRef = F1ADF4E1991C352F0ECCE1E7 /* testing_hooks_util.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 */; }; @@ -1172,6 +1181,7 @@ CFF1EBC60A00BA5109893C6E /* memory_index_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = DB5A1E760451189DA36028B3 /* memory_index_manager_test.cc */; }; D00E69F7FDF2BE674115AD3F /* field_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2AD2023DDB20028D6BE /* field_path_test.cc */; }; D04CBBEDB8DC16D8C201AC49 /* leveldb_target_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = E76F0CDF28E5FA62D21DE648 /* leveldb_target_cache_test.cc */; }; + D0DA42DC66C4FE508A63B269 /* testing_hooks_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A002425BC4FC4E805F4175B6 /* testing_hooks_test.cc */; }; D143FBD057481C1A59B27E5E /* persistence_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A31F315EE100DD57A1 /* persistence_spec_test.json */; }; D156B9F19B5B29E77664FDFC /* logic_utils_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 28B45B2104E2DAFBBF86DBB7 /* logic_utils_test.cc */; }; D1690214781198276492442D /* event_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6F57521E161450FAF89075ED /* event_manager_test.cc */; }; @@ -1331,6 +1341,7 @@ F0C8EB1F4FB56401CFA4F374 /* object_value_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 214877F52A705012D6720CA0 /* object_value_test.cc */; }; F0EA84FB66813F2BC164EF7C /* token_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A082AFDD981B07B5AD78FDE8 /* token_test.cc */; }; F10A3E4E164A5458DFF7EDE6 /* leveldb_remote_document_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0840319686A223CC4AD3FAB1 /* leveldb_remote_document_cache_test.cc */; }; + F184E5367DF3CA158EDE8532 /* testing_hooks_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A002425BC4FC4E805F4175B6 /* testing_hooks_test.cc */; }; F19B749671F2552E964422F7 /* FIRListenerRegistrationTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06B202154D500B64F25 /* FIRListenerRegistrationTests.mm */; }; F1EAEE9DF819C017A9506AEB /* FIRIndexingTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 795AA8FC31D2AF6864B07D39 /* FIRIndexingTests.mm */; }; F272A8C41D2353700A11D1FB /* field_mask_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA5320A36E1F00BCEB75 /* field_mask_test.cc */; }; @@ -1350,6 +1361,7 @@ F6079BFC9460B190DA85C2E6 /* pretty_printing_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB323F9553050F4F6490F9FF /* pretty_printing_test.cc */; }; F609600E9A88A4D44FD1FCEB /* FSTSpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E03020213FFC00B64F25 /* FSTSpecTests.mm */; }; F660788F69B4336AC6CD2720 /* offline_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A11F315EE100DD57A1 /* offline_spec_test.json */; }; + F6738D3B72352BBEFB87172C /* testing_hooks_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A002425BC4FC4E805F4175B6 /* testing_hooks_test.cc */; }; F696B7467E80E370FDB3EAA7 /* remote_document_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7EB299CF85034F09CFD6F3FD /* remote_document_cache_test.cc */; }; F6BC4D3E336F3CE0782BCC34 /* memory_query_engine_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 8EF6A33BC2D84233C355F1D0 /* memory_query_engine_test.cc */; }; F72DF72447EA7AB9D100816A /* FSTHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E03A2021401F00B64F25 /* FSTHelpers.mm */; }; @@ -1619,6 +1631,7 @@ 5C7942B6244F4C416B11B86C /* leveldb_mutation_queue_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_mutation_queue_test.cc; sourceTree = ""; }; 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 = ""; }; + 5CFCF090AC17BC8D36640F88 /* testing_hooks_util.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = testing_hooks_util.h; sourceTree = ""; }; 5E19B9B2105BA618DA9EE99C /* query_engine_test.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = query_engine_test.h; sourceTree = ""; }; 5FF903AEFA7A3284660FA4C5 /* leveldb_local_store_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_local_store_test.cc; sourceTree = ""; }; 6003F58A195388D20070C39A /* Firestore_Example_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Firestore_Example_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1719,6 +1732,7 @@ 9B0B005A79E765AF02793DCE /* schedule_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = schedule_test.cc; sourceTree = ""; }; 9C1AFCC9E616EC33D6E169CF /* recovery_spec_test.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; path = recovery_spec_test.json; sourceTree = ""; }; 9CFD366B783AE27B9E79EE7A /* string_format_apple_test.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = string_format_apple_test.mm; sourceTree = ""; }; + A002425BC4FC4E805F4175B6 /* testing_hooks_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = testing_hooks_test.cc; sourceTree = ""; }; A082AFDD981B07B5AD78FDE8 /* token_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; name = token_test.cc; path = credentials/token_test.cc; sourceTree = ""; }; A366F6AE1A5A77548485C091 /* bundle.pb.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = bundle.pb.cc; sourceTree = ""; }; A5466E7809AD2871FFDE6C76 /* view_testing.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = view_testing.cc; sourceTree = ""; }; @@ -1823,6 +1837,7 @@ EF83ACD5E1E9F25845A9ACED /* leveldb_migrations_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_migrations_test.cc; sourceTree = ""; }; F02F734F272C3C70D1307076 /* filter_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = filter_test.cc; sourceTree = ""; }; F119BDDF2F06B3C0883B8297 /* firebase_app_check_credentials_provider_test.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; name = firebase_app_check_credentials_provider_test.mm; path = credentials/firebase_app_check_credentials_provider_test.mm; sourceTree = ""; }; + F1ADF4E1991C352F0ECCE1E7 /* testing_hooks_util.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = testing_hooks_util.cc; sourceTree = ""; }; 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 = ""; }; F51859B394D01C0C507282F1 /* filesystem_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = filesystem_test.cc; sourceTree = ""; }; F694C3CE4B77B3C0FA4BBA53 /* Pods_Firestore_Benchmarks_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Benchmarks_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2026,6 +2041,8 @@ 64AA92CFA356A2360F3C5646 /* filesystem_testing.h */, 3CAA33F964042646FDDAF9F9 /* status_testing.cc */, 4334F87873015E3763954578 /* status_testing.h */, + F1ADF4E1991C352F0ECCE1E7 /* testing_hooks_util.cc */, + 5CFCF090AC17BC8D36640F88 /* testing_hooks_util.h */, 54A0352820A3B3BD003E0143 /* testutil.cc */, 54A0352920A3B3BD003E0143 /* testutil.h */, 5497CB76229DECDE000FB92F /* time_testing.cc */, @@ -2104,6 +2121,7 @@ AB380CFC201A2EE200D97691 /* string_util_test.cc */, 79507DF8378D3C42F5B36268 /* string_win_test.cc */, 899FC22684B0F7BEEAE13527 /* task_test.cc */, + A002425BC4FC4E805F4175B6 /* testing_hooks_test.cc */, B68B1E002213A764008977EF /* to_string_apple_test.mm */, B696858D2214B53900271095 /* to_string_test.cc */, ); @@ -3811,6 +3829,8 @@ B384E0F90D4CCC15C88CAF30 /* target_index_matcher_test.cc in Sources */, 55427A6CFFB22E069DCC0CC4 /* target_test.cc in Sources */, 88929ED628DA8DD9592974ED /* task_test.cc in Sources */, + 9B2C6A48A4DBD36080932B4E /* testing_hooks_test.cc in Sources */, + 64055C7B3BF2C4106714DD3C /* testing_hooks_util.cc in Sources */, 32A95242C56A1A230231DB6A /* testutil.cc in Sources */, 5497CB78229DECDE000FB92F /* time_testing.cc in Sources */, ACC9369843F5ED3BD2284078 /* timestamp_test.cc in Sources */, @@ -4019,6 +4039,8 @@ 2428E92E063EBAEA44BA5913 /* target_index_matcher_test.cc in Sources */, EB2137E6FBB0DDE2DF80E3D0 /* target_test.cc in Sources */, 67CF9FAA890307780731E1DA /* task_test.cc in Sources */, + 24B75C63BDCD5551B2F69901 /* testing_hooks_test.cc in Sources */, + 258F2B14F7E4C615707E67B1 /* testing_hooks_util.cc in Sources */, 8388418F43042605FB9BFB92 /* testutil.cc in Sources */, 5497CB79229DECDE000FB92F /* time_testing.cc in Sources */, 26CB3D7C871BC56456C6021E /* timestamp_test.cc in Sources */, @@ -4245,6 +4267,8 @@ C8722550B56CEB96F84DCE94 /* target_index_matcher_test.cc in Sources */, 35FEB53E165518C0DE155CB0 /* target_test.cc in Sources */, 76A5447D76F060E996555109 /* task_test.cc in Sources */, + D0DA42DC66C4FE508A63B269 /* testing_hooks_test.cc in Sources */, + 179B8752CF4255263B9986FD /* testing_hooks_util.cc in Sources */, 409C0F2BFC2E1BECFFAC4D32 /* testutil.cc in Sources */, 6300709ECDE8E0B5A8645F8D /* time_testing.cc in Sources */, 0CEE93636BA4852D3C5EC428 /* timestamp_test.cc in Sources */, @@ -4471,6 +4495,8 @@ 15A5DEC8430E71D64424CBFD /* target_index_matcher_test.cc in Sources */, 035DE410628A8F804F6F2790 /* target_test.cc in Sources */, 93C8F772F4DC5A985FA3D815 /* task_test.cc in Sources */, + F6738D3B72352BBEFB87172C /* testing_hooks_test.cc in Sources */, + 1A21C6A5AC8A153E96F91566 /* testing_hooks_util.cc in Sources */, A17DBC8F24127DA8A381F865 /* testutil.cc in Sources */, A25FF76DEF542E01A2DF3B0E /* time_testing.cc in Sources */, 1E42CD0F60EB22A5D0C86D1F /* timestamp_test.cc in Sources */, @@ -4689,6 +4715,8 @@ F27347560A963E8162C56FF3 /* target_index_matcher_test.cc in Sources */, 205601D1C6A40A4DD3BBAA04 /* target_test.cc in Sources */, 662793139A36E5CFC935B949 /* task_test.cc in Sources */, + F184E5367DF3CA158EDE8532 /* testing_hooks_test.cc in Sources */, + 25DCB9BD1C681C6611A17164 /* testing_hooks_util.cc in Sources */, 54A0352A20A3B3BD003E0143 /* testutil.cc in Sources */, 5497CB77229DECDE000FB92F /* time_testing.cc in Sources */, ABF6506C201131F8005F2C74 /* timestamp_test.cc in Sources */, @@ -4934,6 +4962,8 @@ 84E75527F3739131C09BEAA5 /* target_index_matcher_test.cc in Sources */, 7D25D41B013BB70ADE526055 /* target_test.cc in Sources */, C57B15CADD8C3E806B154C19 /* task_test.cc in Sources */, + 5360D52DCAD1069B1E4B0B9D /* testing_hooks_test.cc in Sources */, + C201BE60B8717CDCBEEDF3F0 /* testing_hooks_util.cc in Sources */, CA989C0E6020C372A62B7062 /* testutil.cc in Sources */, 2D220B9ABFA36CD7AC43D0A7 /* time_testing.cc in Sources */, D91D86B29B86A60C05879A48 /* timestamp_test.cc in Sources */, diff --git a/Firestore/Example/Tests/Integration/API/FIRQueryTests.mm b/Firestore/Example/Tests/Integration/API/FIRQueryTests.mm index 93791bae165..fba25c382f7 100644 --- a/Firestore/Example/Tests/Integration/API/FIRQueryTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRQueryTests.mm @@ -22,6 +22,8 @@ #import "Firestore/Example/Tests/Util/FSTHelpers.h" #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" +#include "Firestore/core/test/unit/testutil/testing_hooks_util.h" + namespace { NSArray *SortedStringsNotIn(NSSet *set, NSSet *remove) { @@ -1191,6 +1193,10 @@ - (void)testOrderByEquality { } - (void)testResumingAQueryShouldUseExistenceFilterToDetectDeletes { + // Set this test to stop when the first failure occurs because some test assertion failures make + // the rest of the test not applicable or will even crash. + [self setContinueAfterFailure:NO]; + // Prepare the names and contents of the 100 documents to create. NSMutableDictionary *> *testDocs = [[NSMutableDictionary alloc] init]; @@ -1236,8 +1242,13 @@ - (void)testResumingAQueryShouldUseExistenceFilterToDetectDeletes { [NSThread sleepForTimeInterval:10.0f]; // Resume the query and save the resulting snapshot for verification. - FIRQuerySnapshot *querySnapshot2 = [self readDocumentSetForRef:collRef - source:FIRFirestoreSourceDefault]; + // Use some internal testing hooks to "capture" the existence filter mismatches to verify them. + FIRQuerySnapshot *querySnapshot2; + std::vector + existence_filter_mismatches = + firebase::firestore::testutil::CaptureExistenceFilterMismatches([&] { + querySnapshot2 = [self readDocumentSetForRef:collRef source:FIRFirestoreSourceDefault]; + }); // Verify that the snapshot from the resumed query contains the expected documents; that is, // that it contains the 50 documents that were _not_ deleted. @@ -1272,6 +1283,21 @@ - (void)testResumingAQueryShouldUseExistenceFilterToDetectDeletes { [missingDocumentIds componentsJoinedByString:@", "]); } } + + // Skip the verification of the existence filter mismatch when testing against the Firestore + // emulator because the Firestore emulator fails to to send an existence filter at all. + // TODO(b/270731363): Enable the verification of the existence filter mismatch once the Firestore + // emulator is fixed to send an existence filter. + if ([FSTIntegrationTestCase isRunningAgainstEmulator]) { + return; + } + + // Verify that Watch sent an existence filter with the correct counts when the query was resumed. + XCTAssertEqual(static_cast(existence_filter_mismatches.size()), 1); + firebase::firestore::util::TestingHooks::ExistenceFilterMismatchInfo &info = + existence_filter_mismatches[0]; + XCTAssertEqual(info.local_cache_count, 100); + XCTAssertEqual(info.existence_filter_count, 50); } @end diff --git a/Firestore/core/src/remote/remote_event.cc b/Firestore/core/src/remote/remote_event.cc index f6f5d86ccca..0c08d983dbf 100644 --- a/Firestore/core/src/remote/remote_event.cc +++ b/Firestore/core/src/remote/remote_event.cc @@ -19,6 +19,7 @@ #include #include "Firestore/core/src/local/target_data.h" +#include "Firestore/core/src/util/testing_hooks.h" namespace firebase { namespace firestore { @@ -34,6 +35,7 @@ using model::MutableDocument; using model::SnapshotVersion; using model::TargetId; using nanopb::ByteString; +using util::TestingHooks; // TargetChange @@ -239,6 +241,8 @@ void WatchChangeAggregator::HandleExistenceFilter( // snapshot with `isFromCache:true`. ResetTarget(target_id); pending_target_resets_.insert(target_id); + TestingHooks::GetInstance().NotifyOnExistenceFilterMismatch( + {current_size, expected_count}); } } } diff --git a/Firestore/core/src/util/testing_hooks.cc b/Firestore/core/src/util/testing_hooks.cc new file mode 100644 index 00000000000..1f48fd0a812 --- /dev/null +++ b/Firestore/core/src/util/testing_hooks.cc @@ -0,0 +1,119 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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/util/testing_hooks.h" + +#include +#include // NOLINT(build/c++11) +#include +#include +#include +#include + +#include "Firestore/core/src/util/no_destructor.h" + +namespace firebase { +namespace firestore { +namespace util { + +namespace { + +/** + * An implementation of `ListenerRegistration` whose `Remove()` method simply + * invokes the function specified to the constructor. This allows easily + * creating `ListenerRegistration` objects that call a lambda. + */ +class RemoveDelegatingListenerRegistration final + : public api::ListenerRegistration { + public: + RemoveDelegatingListenerRegistration(std::function delegate) + : delegate_(std::move(delegate)) { + } + + void Remove() override { + delegate_(); + } + + private: + std::function delegate_; +}; + +} // namespace + +/** Returns the singleton instance of this class. */ +TestingHooks& TestingHooks::GetInstance() { + static NoDestructor instance; + return *instance; +} + +std::shared_ptr +TestingHooks::OnExistenceFilterMismatch( + ExistenceFilterMismatchCallback callback) { + // Register the callback. + std::unique_lock lock(mutex_); + const int id = next_id_++; + existence_filter_mismatch_callbacks_.insert( + {id, + std::make_shared(std::move(callback))}); + lock.unlock(); + + // NOTE: Capturing `this` in the lambda below is safe because the destructor + // is deleted and, therefore, `this` can never be deleted. The static_assert + // statements below verify this invariant. + using this_type = std::remove_pointer::type; + static_assert(std::is_same::value, ""); + static_assert(!std::is_destructible::value, ""); + + // Create a ListenerRegistration that the caller can use to unregister the + // callback. + return std::make_shared([this, id]() { + std::lock_guard lock(mutex_); + auto iter = existence_filter_mismatch_callbacks_.find(id); + if (iter != existence_filter_mismatch_callbacks_.end()) { + existence_filter_mismatch_callbacks_.erase(iter); + } + }); +} + +void TestingHooks::NotifyOnExistenceFilterMismatch( + const ExistenceFilterMismatchInfo& info) { + std::unique_lock lock(mutex_); + + // Short-circuit to avoid any unnecessary work if there is nothing to do. + if (existence_filter_mismatch_callbacks_.empty()) { + return; + } + + // Copy the callbacks into a vector so that they can be invoked after + // releasing the lock. + std::vector> callbacks; + for (auto&& entry : existence_filter_mismatch_callbacks_) { + callbacks.push_back(entry.second); + } + + // Release the lock so that the callback invocations are done _without_ + // holding the lock. This avoids deadlock in the case that invocations are + // re-entrant. + lock.unlock(); + + for (std::shared_ptr callback : callbacks) { + callback->operator()(info); + } +} + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/util/testing_hooks.h b/Firestore/core/src/util/testing_hooks.h new file mode 100644 index 00000000000..1e1ea0b1788 --- /dev/null +++ b/Firestore/core/src/util/testing_hooks.h @@ -0,0 +1,121 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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_SRC_UTIL_TESTING_HOOKS_H_ +#define FIRESTORE_CORE_SRC_UTIL_TESTING_HOOKS_H_ + +#include +#include +#include // NOLINT(build/c++11) +#include + +#include "Firestore/core/src/api/listener_registration.h" +#include "Firestore/core/src/util/no_destructor.h" + +namespace firebase { +namespace firestore { +namespace util { + +/** + * Manages "testing hooks", hooks into the internals of the SDK to verify + * internal state and events during integration tests. Do not use this class + * except for testing purposes. + */ +class TestingHooks final { + public: + /** Returns the singleton instance of this class. */ + static TestingHooks& GetInstance(); + + /** + * Information about an existence filter mismatch, as specified to callbacks + * registered with `OnExistenceFilterMismatch()`. + */ + struct ExistenceFilterMismatchInfo { + /** The number of documents that matched the query in the local cache. */ + int local_cache_count = -1; + + /** + * The number of documents that matched the query on the server, as + * specified in the `ExistenceFilter` message's `count` field. + */ + int existence_filter_count = -1; + }; + + using ExistenceFilterMismatchCallback = + std::function; + + /** + * Registers a callback to be invoked when an existence filter mismatch occurs + * in the Watch listen stream. + * + * The relative order in which callbacks are notified is unspecified; do not + * rely on any particular ordering. If a given callback is registered multiple + * times then it will be notified multiple times, once per registration. + * + * The listener callbacks are performed synchronously in + * `NotifyOnExistenceFilterMismatch()`; therefore, listeners should perform + * their work as quickly as possible and return to avoid blocking any critical + * work. In particular, the listener callbacks should *not* block or perform + * long-running operations. + * + * The `ExistenceFilterMismatchInfo` reference specified to the callback is + * only valid during the lifetime of the callback. Once the callback returns + * then it must not use the given `ExistenceFilterMismatchInfo` reference + * again. + * + * @param callback the callback to invoke upon existence filter mismatch. + * + * @return an object whose `Remove()` member function unregisters the given + * callback; only the first invocation of `Remove()` does anything; all + * subsequent invocations do nothing. Note that due to inherent race + * conditions it is technically possible, although unlikely, that callbacks + * could still occur _after_ unregistering. + */ + std::shared_ptr OnExistenceFilterMismatch( + ExistenceFilterMismatchCallback callback); + + /** + * Invokes all currently-registered `OnExistenceFilterMismatch` callbacks + * synchronously. + * @param info Information about the existence filter mismatch. + */ + void NotifyOnExistenceFilterMismatch(const ExistenceFilterMismatchInfo& info); + + private: + TestingHooks() = default; + + // Delete the destructor so that the singleton instance of this class can + // never be deleted. + ~TestingHooks() = delete; + + TestingHooks(const TestingHooks&) = delete; + TestingHooks(TestingHooks&&) = delete; + TestingHooks& operator=(const TestingHooks&) = delete; + TestingHooks& operator=(TestingHooks&&) = delete; + + friend class NoDestructor; + + mutable std::mutex mutex_; + int next_id_ = 0; + std::unordered_map> + existence_filter_mismatch_callbacks_; +}; + +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_UTIL_TESTING_HOOKS_H_ diff --git a/Firestore/core/test/unit/testutil/async_testing.h b/Firestore/core/test/unit/testutil/async_testing.h index efb44f13086..cd56b16dd1b 100644 --- a/Firestore/core/test/unit/testutil/async_testing.h +++ b/Firestore/core/test/unit/testutil/async_testing.h @@ -21,7 +21,10 @@ #include #include // NOLINT(build/c++11) #include +#include // NOLINT(build/c++11) #include // NOLINT(build/c++11) +#include +#include #include "gtest/gtest.h" @@ -136,6 +139,113 @@ class AsyncTest { << testing::UnitTest::GetInstance()->current_test_info()->name()}; }; +/** + * A class that can be used to "accumulate" objects that is completely thread + * safe. + * + * When testing "listeners" it is common in tests to just create a std::vector, + * register a "listener", then add objects into the vector when the listener is + * notified. This, however, is not thread safe because there is typically no + * synchronization in place, such as via a mutex. Moreover, if the listener + * receives a notification after the test method completes then the vector, + * which was allocated on the stack, is deleted. Both of these problems result + * in undefined behavior, which is bad. + * + * Using `AsyncAccumulator` solves both of these problems. First, it protects + * the std::vector instance with a mutex to eliminate race conditions. Second, + * instances can only be created as shared_ptr, which can be copied into the + * listener and will keep the vector alive until the test completes or the + * listener is deleted, whichever comes last. + * + * The constructor of `AsyncAccumulator` is private, in order to force + * instances to be created with a shared_ptr via the `NewInstance()` method. + */ +template +class AsyncAccumulator final + : public std::enable_shared_from_this> { + public: + /** + * Creates and returns a std::shared_ptr to a new instance of this class. + */ + static std::shared_ptr NewInstance() { + return std::shared_ptr(new AsyncAccumulator); + } + + /** + * Adds a copy of the given object to this object's encapsulated vector and + * resolves any outstanding std::future objects returned from + * `WaitForObject()`. + */ + void AccumulateObject(const T& object) { + std::lock_guard lock(mutex_); + objects_.push_back(object); + for (auto&& promise : promises_) { + promise.set_value(); + } + promises_.clear(); + } + + /** + * Creates and returns a std::future that resolves when an object is + * accumulated via a call to `AccumulateObject()`. If there is an object + * already accumulated in this object's encapsulated vector then the returned + * future will be resolved immediately. + */ + std::future WaitForObject() { + std::lock_guard lock(mutex_); + std::promise promise; + std::future future = promise.get_future(); + + if (objects_.empty()) { + promises_.push_back(std::move(promise)); + } else { + promise.set_value(); + } + + return future; + } + + /** + * Returns whether the encapsulated vector of accumulated objects is empty. + */ + bool IsEmpty() const { + std::lock_guard lock(mutex_); + return objects_.empty(); + } + + /** + * Removes the first element from the encapsulated vector and returns it. + * + * This function exhibits undefined behavior if the encapsulated vector is + * empty. + */ + T Shift() { + std::lock_guard lock(mutex_); + auto iter = objects_.begin(); + T result = std::move(*iter); + objects_.erase(iter); + return result; + } + + /** + * Creates and returns a function that, when invoked, calls + * `AccumulateObject()`. + */ + std::function AsCallback() { + return [shared_this = this->shared_from_this()](const T& object) { + shared_this->AccumulateObject(object); + }; + } + + private: + // Private constructor to force instances to be created via NewInstance(). + AsyncAccumulator() = default; + + mutable std::mutex mutex_; + std::vector objects_; + std::vector> promises_; +}; + } // namespace testutil } // namespace firestore } // namespace firebase diff --git a/Firestore/core/test/unit/testutil/testing_hooks_util.cc b/Firestore/core/test/unit/testutil/testing_hooks_util.cc new file mode 100644 index 00000000000..e987e23be30 --- /dev/null +++ b/Firestore/core/test/unit/testutil/testing_hooks_util.cc @@ -0,0 +1,57 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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/unit/testutil/testing_hooks_util.h" + +#include +#include +#include + +#include "Firestore/core/src/api/listener_registration.h" +#include "Firestore/core/src/util/defer.h" +#include "Firestore/core/src/util/testing_hooks.h" +#include "Firestore/core/test/unit/testutil/async_testing.h" + +namespace firebase { +namespace firestore { +namespace testutil { + +using util::Defer; +using util::TestingHooks; + +std::vector +CaptureExistenceFilterMismatches(std::function callback) { + auto accumulator = AsyncAccumulator< + TestingHooks::ExistenceFilterMismatchInfo>::NewInstance(); + + TestingHooks& testing_hooks = TestingHooks::GetInstance(); + std::shared_ptr registration = + testing_hooks.OnExistenceFilterMismatch(accumulator->AsCallback()); + Defer unregister_callback([registration]() { registration->Remove(); }); + + callback(); + + std::vector mismatches; + while (!accumulator->IsEmpty()) { + mismatches.push_back(accumulator->Shift()); + } + + return mismatches; +} + +} // namespace testutil +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/unit/testutil/testing_hooks_util.h b/Firestore/core/test/unit/testutil/testing_hooks_util.h new file mode 100644 index 00000000000..b6827636fd4 --- /dev/null +++ b/Firestore/core/test/unit/testutil/testing_hooks_util.h @@ -0,0 +1,43 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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_UNIT_TESTUTIL_TESTING_HOOKS_UTIL_H_ +#define FIRESTORE_CORE_TEST_UNIT_TESTUTIL_TESTING_HOOKS_UTIL_H_ + +#include +#include + +#include "Firestore/core/src/util/testing_hooks.h" + +namespace firebase { +namespace firestore { +namespace testutil { + +/** + * Captures all existence filter mismatches in the Watch 'Listen' stream that + * occur during the execution of the given callback. + * @param callback The callback to invoke; during the invocation of this + * callback all existence filter mismatches will be captured. + * @return the captured existence filter mismatches. + */ +std::vector +CaptureExistenceFilterMismatches(std::function callback); + +} // namespace testutil +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_TEST_UNIT_TESTUTIL_TESTING_HOOKS_UTIL_H_ diff --git a/Firestore/core/test/unit/util/testing_hooks_test.cc b/Firestore/core/test/unit/util/testing_hooks_test.cc new file mode 100644 index 00000000000..be7b9a2c8e6 --- /dev/null +++ b/Firestore/core/test/unit/util/testing_hooks_test.cc @@ -0,0 +1,195 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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/util/testing_hooks.h" + +#include // NOLINT(build/c++11) +#include // NOLINT(build/c++11) +#include +#include // NOLINT(build/c++11) +#include // NOLINT(build/c++11) + +#include "Firestore/core/src/api/listener_registration.h" +#include "Firestore/core/src/util/defer.h" +#include "Firestore/core/test/unit/testutil/async_testing.h" + +#include "gtest/gtest.h" + +namespace { + +using namespace std::chrono_literals; // NOLINT(build/namespaces) + +using firebase::firestore::api::ListenerRegistration; +using firebase::firestore::testutil::AsyncTest; +using firebase::firestore::util::Defer; +using firebase::firestore::util::TestingHooks; + +using ExistenceFilterMismatchInfoAccumulator = + firebase::firestore::testutil::AsyncAccumulator< + TestingHooks::ExistenceFilterMismatchInfo>; + +class TestingHooksTest : public ::testing::Test, public AsyncTest { + public: + void AssertAccumulatedObject( + const std::shared_ptr& + accumulator, + TestingHooks::ExistenceFilterMismatchInfo expected) { + Await(accumulator->WaitForObject()); + ASSERT_FALSE(accumulator->IsEmpty()); + TestingHooks::ExistenceFilterMismatchInfo info = accumulator->Shift(); + EXPECT_EQ(info.local_cache_count, expected.local_cache_count); + EXPECT_EQ(info.existence_filter_count, expected.existence_filter_count); + } + + std::future NotifyOnExistenceFilterMismatchAsync( + TestingHooks::ExistenceFilterMismatchInfo info) { + return Async([info]() { + TestingHooks::GetInstance().NotifyOnExistenceFilterMismatch(info); + }); + } +}; + +TEST_F(TestingHooksTest, GetInstanceShouldAlwaysReturnTheSameObject) { + TestingHooks& testing_hooks1 = TestingHooks::GetInstance(); + TestingHooks& testing_hooks2 = TestingHooks::GetInstance(); + EXPECT_EQ(&testing_hooks1, &testing_hooks2); +} + +TEST_F(TestingHooksTest, OnExistenceFilterMismatchCallbackShouldGetNotified) { + auto accumulator = ExistenceFilterMismatchInfoAccumulator::NewInstance(); + std::shared_ptr listener_registration = + TestingHooks::GetInstance().OnExistenceFilterMismatch( + accumulator->AsCallback()); + Defer unregister_listener([=] { listener_registration->Remove(); }); + + NotifyOnExistenceFilterMismatchAsync({123, 456}); + + AssertAccumulatedObject(accumulator, {123, 456}); +} + +TEST_F(TestingHooksTest, + OnExistenceFilterMismatchCallbackShouldGetNotifiedMultipleTimes) { + auto accumulator = ExistenceFilterMismatchInfoAccumulator::NewInstance(); + std::shared_ptr listener_registration = + TestingHooks::GetInstance().OnExistenceFilterMismatch( + accumulator->AsCallback()); + Defer unregister_listener([=] { listener_registration->Remove(); }); + + NotifyOnExistenceFilterMismatchAsync({111, 222}); + AssertAccumulatedObject(accumulator, {111, 222}); + NotifyOnExistenceFilterMismatchAsync({333, 444}); + AssertAccumulatedObject(accumulator, {333, 444}); + NotifyOnExistenceFilterMismatchAsync({555, 666}); + AssertAccumulatedObject(accumulator, {555, 666}); +} + +TEST_F(TestingHooksTest, + OnExistenceFilterMismatchAllCallbacksShouldGetNotified) { + auto accumulator1 = ExistenceFilterMismatchInfoAccumulator::NewInstance(); + auto accumulator2 = ExistenceFilterMismatchInfoAccumulator::NewInstance(); + std::shared_ptr listener_registration1 = + TestingHooks::GetInstance().OnExistenceFilterMismatch( + accumulator1->AsCallback()); + Defer unregister_listener1([=] { listener_registration1->Remove(); }); + std::shared_ptr listener_registration2 = + TestingHooks::GetInstance().OnExistenceFilterMismatch( + accumulator2->AsCallback()); + Defer unregister_listener2([=] { listener_registration2->Remove(); }); + + NotifyOnExistenceFilterMismatchAsync({123, 456}); + + AssertAccumulatedObject(accumulator1, {123, 456}); + AssertAccumulatedObject(accumulator2, {123, 456}); +} + +TEST_F(TestingHooksTest, + OnExistenceFilterMismatchCallbackShouldGetNotifiedOncePerRegistration) { + auto accumulator = ExistenceFilterMismatchInfoAccumulator::NewInstance(); + std::shared_ptr listener_registration1 = + TestingHooks::GetInstance().OnExistenceFilterMismatch( + accumulator->AsCallback()); + Defer unregister_listener1([=] { listener_registration1->Remove(); }); + std::shared_ptr listener_registration2 = + TestingHooks::GetInstance().OnExistenceFilterMismatch( + accumulator->AsCallback()); + Defer unregister_listener2([=] { listener_registration1->Remove(); }); + + NotifyOnExistenceFilterMismatchAsync({123, 456}); + + AssertAccumulatedObject(accumulator, {123, 456}); + AssertAccumulatedObject(accumulator, {123, 456}); + std::this_thread::sleep_for(250ms); + EXPECT_TRUE(accumulator->IsEmpty()); +} + +TEST_F(TestingHooksTest, + OnExistenceFilterMismatchShouldNotBeNotifiedAfterRemove) { + auto accumulator = ExistenceFilterMismatchInfoAccumulator::NewInstance(); + std::shared_ptr registration = + TestingHooks::GetInstance().OnExistenceFilterMismatch( + accumulator->AsCallback()); + registration->Remove(); + + NotifyOnExistenceFilterMismatchAsync({123, 456}); + + std::this_thread::sleep_for(250ms); + EXPECT_TRUE(accumulator->IsEmpty()); +} + +TEST_F(TestingHooksTest, OnExistenceFilterMismatchRemoveShouldOnlyRemoveOne) { + auto accumulator1 = ExistenceFilterMismatchInfoAccumulator::NewInstance(); + auto accumulator2 = ExistenceFilterMismatchInfoAccumulator::NewInstance(); + auto accumulator3 = ExistenceFilterMismatchInfoAccumulator::NewInstance(); + std::shared_ptr listener_registration1 = + TestingHooks::GetInstance().OnExistenceFilterMismatch( + accumulator1->AsCallback()); + Defer unregister_listener1([=] { listener_registration1->Remove(); }); + std::shared_ptr listener_registration2 = + TestingHooks::GetInstance().OnExistenceFilterMismatch( + accumulator2->AsCallback()); + Defer unregister_listener2([=] { listener_registration1->Remove(); }); + std::shared_ptr listener_registration3 = + TestingHooks::GetInstance().OnExistenceFilterMismatch( + accumulator3->AsCallback()); + Defer unregister_listener3([=] { listener_registration3->Remove(); }); + + listener_registration2->Remove(); + + NotifyOnExistenceFilterMismatchAsync({123, 456}); + + AssertAccumulatedObject(accumulator1, {123, 456}); + AssertAccumulatedObject(accumulator3, {123, 456}); + std::this_thread::sleep_for(250ms); + EXPECT_TRUE(accumulator2->IsEmpty()); +} + +TEST_F(TestingHooksTest, OnExistenceFilterMismatchMultipleRemovesHaveNoEffect) { + auto accumulator = ExistenceFilterMismatchInfoAccumulator::NewInstance(); + std::shared_ptr listener_registration = + TestingHooks::GetInstance().OnExistenceFilterMismatch( + accumulator->AsCallback()); + Defer unregister_listener([=] { listener_registration->Remove(); }); + listener_registration->Remove(); + listener_registration->Remove(); + listener_registration->Remove(); + + NotifyOnExistenceFilterMismatchAsync({123, 456}); + + std::this_thread::sleep_for(250ms); + EXPECT_TRUE(accumulator->IsEmpty()); +} + +} // namespace