diff --git a/docs/readme.md b/docs/readme.md index 697629ec..27a21a7c 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -95,6 +95,7 @@ Release Notes directory contains non-ANSI characters (Unicode above U+00FF). - Storage (Desktop): Fixed a crash on Windows when uploading files from a path containing non-ANSI characters (Unicode above U+00FF). + - Firestore: Add multi-database support. ([#738](https://github.com/firebase/firebase-unity-sdk/pull/738)). ### 11.0.0 - Changes diff --git a/firestore/src/FirebaseFirestore.cs b/firestore/src/FirebaseFirestore.cs index 9e28611d..d853e589 100644 --- a/firestore/src/FirebaseFirestore.cs +++ b/firestore/src/FirebaseFirestore.cs @@ -44,13 +44,16 @@ public sealed class FirebaseFirestore { private readonly FirebaseFirestoreSettings _settings; private readonly TransactionManager _transactionManager; - private static readonly IDictionary _instanceCache = - new Dictionary(); + private static readonly IDictionary _instanceCache = + new Dictionary(); + + private const string DefaultDatabase = "(default)"; + + private string _databaseName; // We rely on e.g. firestore.Document("a/b").Firestore returning the original Firestore - // instance so it's important the constructor remains private and we only create one - // FirebaseFirestore instance per FirebaseApp instance. - private FirebaseFirestore(FirestoreProxy proxy, FirebaseApp app) { + // instance so it's important the constructor remains private. + private FirebaseFirestore(FirestoreProxy proxy, FirebaseApp app, string database) { _proxy = Util.NotNull(proxy); App = app; app.AppDisposed += OnAppDisposed; @@ -63,6 +66,7 @@ private FirebaseFirestore(FirestoreProxy proxy, FirebaseApp app) { _settings = new FirebaseFirestoreSettings(proxy); _transactionManager = new TransactionManager(this, proxy); + _databaseName = database; } /// @@ -99,9 +103,11 @@ private void Dispose() { _isInCppInstanceCache = false; RemoveSelfFromInstanceCache(); } - + _proxy = null; App = null; + _databaseName = null; + } finally { _disposeLock.ReleaseWriterLock(); } @@ -115,41 +121,65 @@ private void Dispose() { public FirebaseApp App { get; private set; } /// - /// Gets the instance of FirebaseFirestore for the default FirebaseApp. + /// Gets the instance of FirebaseFirestore for the default FirebaseApp with the default database name. /// /// A FirebaseFirestore instance. public static FirebaseFirestore DefaultInstance { get { FirebaseApp app = Util.NotNull(FirebaseApp.DefaultInstance); - return GetInstance(app); + return GetInstance(app, DefaultDatabase); } } /// - /// Gets an instance of FirebaseFirestore for a specific FirebaseApp. + /// Gets an instance of FirebaseFirestore for a specific FirebaseApp with the default database name. /// /// The FirebaseApp for which to get a FirebaseFirestore /// instance. /// A FirebaseFirestore instance. public static FirebaseFirestore GetInstance(FirebaseApp app) { + return GetInstance(app, DefaultDatabase); + } + + + /// + /// Gets an instance of FirebaseFirestore for the default FirebaseApp with a spesific database name. + /// + /// The customized name for the database. + /// instance. + /// A FirebaseFirestore instance. + public static FirebaseFirestore GetInstance(string database) { + FirebaseApp app = Util.NotNull(FirebaseApp.DefaultInstance); + return GetInstance(app, database); + } + + /// + /// Gets an instance of FirebaseFirestore for a specific FirebaseApp with a spesific database name. + /// + /// The FirebaseApp for which to get a FirebaseFirestore + /// The customized name for the database. + /// instance. + /// A FirebaseFirestore instance. + public static FirebaseFirestore GetInstance(FirebaseApp app, string database) { Preconditions.CheckNotNull(app, nameof(app)); + Preconditions.CheckNotNull(database, nameof(database)); while (true) { FirebaseFirestore firestore; - - // Acquire the lock on `_instanceCache` to see if the given `FirebaseApp` is in + FirestoreInstanceCacheKey key = new FirestoreInstanceCacheKey(app, database); + // Acquire the lock on `_instanceCache` to see if the given `FirestoreInstanceCacheKey` is in // `_instanceCache`; if it isn't then create the `FirebaseFirestore` instance, put it in the // cache, and return it. lock (_instanceCache) { - if (!_instanceCache.TryGetValue(app, out firestore)) { + if (!_instanceCache.TryGetValue(key, out firestore)) { // NOTE: FirestoreProxy.GetInstance() returns an *owning* reference (see the %newobject // directive in firestore.SWIG); therefore, we must make sure that we *never* call // FirestoreProxy.GetInstance() when it would return a proxy for a C++ object that it // previously returned. Otherwise, if it did, then that C++ object would be deleted // twice, causing a crash. - FirestoreProxy firestoreProxy = Util.NotNull(FirestoreProxy.GetInstance(app)); - firestore = new FirebaseFirestore(firestoreProxy, app); - _instanceCache[app] = firestore; + FirestoreProxy firestoreProxy = Util.NotNull(FirestoreProxy.GetInstance(app, database)); + firestore = new FirebaseFirestore(firestoreProxy, app, database); + _instanceCache[key] = firestore; return firestore; } } @@ -557,7 +587,7 @@ public Task WaitForPendingWritesAsync() { /// used. Calling any other method will result in an error. /// /// To restart after termination, simply create a new instance of FirebaseFirestore with - /// GetInstance() or GetInstance(FirebaseApp). + /// GetInstance() methods. /// /// Terminate() does not cancel any pending writes, and any tasks that are awaiting a /// response from the server will not be resolved. The next time you start this instance, it @@ -663,10 +693,42 @@ private void WithFirestoreProxy(Action action) { private void RemoveSelfFromInstanceCache() { lock (_instanceCache) { FirebaseFirestore cachedFirestore; - if (_instanceCache.TryGetValue(App, out cachedFirestore) && cachedFirestore == this) { - _instanceCache.Remove(App); + FirestoreInstanceCacheKey key = new FirestoreInstanceCacheKey(App, _databaseName); + if (_instanceCache.TryGetValue(key, out cachedFirestore) && cachedFirestore == this) { + _instanceCache.Remove(key); } } } + + private struct FirestoreInstanceCacheKey : IEquatable { + public FirebaseApp App { get; } + public string DatabaseName { get; } + + public FirestoreInstanceCacheKey(FirebaseApp app, string databaseName) + { + App = app; + DatabaseName = databaseName; + } + + public override int GetHashCode() { + return App.Name.GetHashCode() + DatabaseName.GetHashCode(); + } + public override bool Equals(object obj) { + return obj is FirestoreInstanceCacheKey && Equals((FirestoreInstanceCacheKey)obj); + } + public bool Equals(FirestoreInstanceCacheKey other) { + return App.Name == other.App.Name && DatabaseName == other.DatabaseName; + } + + public static bool operator ==(FirestoreInstanceCacheKey lhs, FirestoreInstanceCacheKey rhs) { + return lhs.Equals(rhs); + } + public static bool operator !=(FirestoreInstanceCacheKey lhs, FirestoreInstanceCacheKey rhs) { + return !lhs.Equals(rhs); + } + public override string ToString() { + return String.Format("FirestoreInstanceKey: App = {0}, DatabaseName = {1}", App.Name, DatabaseName); + } + } } } diff --git a/firestore/testapp/Assets/Firebase/Sample/Firestore/InvalidArgumentsTest.cs b/firestore/testapp/Assets/Firebase/Sample/Firestore/InvalidArgumentsTest.cs index 26ac166a..f6bbafba 100644 --- a/firestore/testapp/Assets/Firebase/Sample/Firestore/InvalidArgumentsTest.cs +++ b/firestore/testapp/Assets/Firebase/Sample/Firestore/InvalidArgumentsTest.cs @@ -127,8 +127,14 @@ public static InvalidArgumentsTestCase[] TestCases { action = FieldValue_ArrayRemove_NullArray }, new InvalidArgumentsTestCase { name = "FieldValue_ArrayUnion_NullArray", action = FieldValue_ArrayUnion_NullArray }, - new InvalidArgumentsTestCase { name = "FirebaseFirestore_GetInstance_Null", - action = FirebaseFirestore_GetInstance_Null }, + new InvalidArgumentsTestCase { name = "FirebaseFirestore_GetInstance_Null_App", + action = FirebaseFirestore_GetInstance_Null_App }, + new InvalidArgumentsTestCase { name = "FirebaseFirestore_GetInstance_Null_Database_Name", + action = FirebaseFirestore_GetInstance_Null_Database_Name }, + new InvalidArgumentsTestCase { name = "FirebaseFirestore_GetInstance_App_With_Null_Database_Name", + action = FirebaseFirestore_GetInstance_App_With_Null_Database_Name}, + new InvalidArgumentsTestCase { name = "FirebaseFirestore_GetInstance_Null_App_With_Database_Name", + action = FirebaseFirestore_GetInstance_Null_App_With_Database_Name }, new InvalidArgumentsTestCase { name = "FirebaseFirestore_GetInstance_DisposedApp", action = FirebaseFirestore_GetInstance_DisposedApp }, new InvalidArgumentsTestCase { name = "FirebaseFirestore_Collection_NullStringPath", @@ -628,11 +634,26 @@ private static void FieldValue_ArrayUnion_NullArray(UIHandlerAutomated handler) handler.AssertException(typeof(ArgumentNullException), () => FieldValue.ArrayUnion(null)); } - private static void FirebaseFirestore_GetInstance_Null(UIHandlerAutomated handler) { + private static void FirebaseFirestore_GetInstance_Null_App(UIHandlerAutomated handler) { handler.AssertException(typeof(ArgumentNullException), - () => FirebaseFirestore.GetInstance(null)); + () => FirebaseFirestore.GetInstance((FirebaseApp)null)); } + private static void FirebaseFirestore_GetInstance_Null_Database_Name(UIHandlerAutomated handler) { + handler.AssertException(typeof(ArgumentNullException), + () => FirebaseFirestore.GetInstance((string)null)); + } + private static void FirebaseFirestore_GetInstance_Null_App_With_Database_Name(UIHandlerAutomated handler) { + handler.AssertException(typeof(ArgumentNullException), + () => FirebaseFirestore.GetInstance((FirebaseApp)null, "a")); + } + private static void FirebaseFirestore_GetInstance_App_With_Null_Database_Name(UIHandlerAutomated handler) + { + FirebaseApp app = FirebaseApp.DefaultInstance; + handler.AssertException(typeof(ArgumentNullException), + () => FirebaseFirestore.GetInstance(app, (string)null)); + } + private static void FirebaseFirestore_GetInstance_DisposedApp(UIHandlerAutomated handler) { FirebaseApp disposedApp = FirebaseApp.Create(handler.db.App.Options, "test-getinstance-disposedapp"); diff --git a/firestore/testapp/Assets/Firebase/Sample/Firestore/UIHandler.cs b/firestore/testapp/Assets/Firebase/Sample/Firestore/UIHandler.cs index 22918f78..a5e42303 100644 --- a/firestore/testapp/Assets/Firebase/Sample/Firestore/UIHandler.cs +++ b/firestore/testapp/Assets/Firebase/Sample/Firestore/UIHandler.cs @@ -32,6 +32,8 @@ namespace Firebase.Sample.Firestore { public class UIHandler : MonoBehaviour { private const int kMaxLogSize = 16382; + private const string DefaultDatabase = "(default)"; + public GUISkin fb_GUISkin; private Vector2 controlsScrollViewVector = Vector2.zero; private string logText = ""; @@ -213,6 +215,18 @@ protected internal FirebaseFirestore TestFirestore(FirebaseApp app) { return firestore; } + protected internal FirebaseFirestore TestFirestore(string database) { + FirebaseFirestore firestore = FirebaseFirestore.GetInstance(database); + SetTargetBackend(firestore); + return firestore; + } + + protected internal FirebaseFirestore TestFirestore(FirebaseApp app, string database) { + FirebaseFirestore firestore = FirebaseFirestore.GetInstance(app, database); + SetTargetBackend(firestore); + return firestore; + } + // Update the `Settings` of a Firestore instance to run tests against the production or // Firestore emulator backend. protected internal void SetTargetBackend(FirebaseFirestore db) { @@ -652,4 +666,4 @@ void OnGUI() { GUILayout.EndArea(); } } -} +} \ No newline at end of file diff --git a/firestore/testapp/Assets/Firebase/Sample/Firestore/UIHandlerAutomated.cs b/firestore/testapp/Assets/Firebase/Sample/Firestore/UIHandlerAutomated.cs index 557e6f98..c7cec672 100644 --- a/firestore/testapp/Assets/Firebase/Sample/Firestore/UIHandlerAutomated.cs +++ b/firestore/testapp/Assets/Firebase/Sample/Firestore/UIHandlerAutomated.cs @@ -158,13 +158,33 @@ protected override void Start() { TestFirestoreGetInstance, // clang-format on }; + + // Set the list of tests to run against Firestore Emulator only. + Func[] testsToRunAgainstFirestoreEmulatorOnly = { + // TODO(b/284877917): Move these tests into the `tests` above when production backend doesn't + // require databases to be created in advance. + TestMultiDBSnapshotsInSyncListeners, + TestTransactionsFromMultiDBInParallel, + TestReadDocumentFromMultiDB, + TestTerminateMultiDBIndependently, + TestTerminateAppWithMultiDB, + TestRestartCustomFirestore, + }; // For local development convenience, populate `testFilter` with the tests that you would like // to run (instead of running the entire suite). Func[] testFilter = { // THIS LIST MUST BE EMPTY WHEN CHECKED INTO SOURCE CONTROL! }; - + + /* + * THIS MUST BE COMMENTED OUT WHEN CHECKED INTO SOURCE CONTROL! + * + * To run tests against Firestore emulator locally, set `USE_FIRESTORE_EMULATOR` to "true". + * To switch back to run against prod, set it back to null. + */ + // Environment.SetEnvironmentVariable("USE_FIRESTORE_EMULATOR", "true"); + // Unity "helpfully" adds stack traces whenever you call Debug.Log. Unfortunately, these stack // traces are basically useless, since the good parts are always truncated. (See comments on // LogInBatches.) So just disable them. @@ -179,6 +199,13 @@ protected override void Start() { MainThreadId = Thread.CurrentThread.ManagedThreadId; }); + // If the target backend is Firestore Emulator, include the tests in + // `testsToRunAgainstFirestoreEmulatorOnly` list. + if (IsUsingFirestoreEmulator()) { + Debug.Log("Running tests against Firestore Emulator."); + tests = tests.Concat(testsToRunAgainstFirestoreEmulatorOnly).ToArray(); + } + testRunner = AutomatedTestRunner.CreateTestRunner( testsToRun: testFilter.Length > 0 ? testFilter : tests, logFunc: LogInBatches, @@ -191,14 +218,6 @@ protected override void Start() { UIEnabled = false; base.Start(); - - /* - * THIS MUST BE COMMENTED OUT WHEN CHECKED INTO SOURCE CONTROL! - * - * To run tests against Firestore emulator locally, set `USE_FIRESTORE_EMULATOR` to "true". - * To switch back to run against prod, set it back to null. - */ - // Environment.SetEnvironmentVariable("USE_FIRESTORE_EMULATOR", "true"); } // Passes along the update call to automated test runner. @@ -540,6 +559,10 @@ private List AsList(params T[] elements) { return elements.ToList(); } + private const string DEFAULT_DATABASE = "(default)"; + private const string MULTI_DATABASE_ONE = "test-db-1"; + private const string MULTI_DATABASE_TWO = "test-db-2"; + private const int AUTO_ID_LENGTH = 20; private const string AUTO_ID_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; private static System.Random rand = new System.Random(); @@ -609,6 +632,151 @@ Task TestDeleteDocument() { }); } + Task TestReadDocumentFromMultiDB() { + return Async(() => { + // Create 2 firestore instances and populate with documents. + FirebaseApp customApp = FirebaseApp.Create(FirebaseApp.DefaultInstance.Options, "Interact-multi-DB"); + FirebaseFirestore customDb1 = TestFirestore(customApp, MULTI_DATABASE_ONE); + FirebaseFirestore customDb2 = TestFirestore(customApp, MULTI_DATABASE_TWO); + + DocumentReference doc1 = customDb1.Collection("foo").Document(); + var data1 = TestData(1); + Await(doc1.SetAsync(data1)); + + DocumentReference doc2 = customDb2.Collection("foo").Document(); + var data2 = TestData(2); + Await(doc2.SetAsync(data2)); + + // Documents from different databases should be able to get written and read + // without interfering with each other. + DocumentSnapshot snap1 = Await(doc1.GetSnapshotAsync()); + Assert("Written document in the first firestore should exist", snap1.Exists); + AssertDeepEq(snap1.ToDictionary(), data1); + DocumentSnapshot snap2 = Await(doc2.GetSnapshotAsync()); + Assert("Written document in the second firestore should exist", snap2.Exists); + AssertDeepEq(snap2.ToDictionary(), data2); + AssertEq(snap1.Equals(snap2), false); + + // Delete one of the document. + Await(doc1.DeleteAsync()); + + // Mutating documents in one database should not affect other databases. + DocumentSnapshot snap3 = Await(doc1.GetSnapshotAsync()); + Assert("Deleted document should not exist", !snap3.Exists); + AssertEq(snap3.ToDictionary(), null); + + DocumentSnapshot snap4 = Await(doc2.GetSnapshotAsync()); + Assert("Dcoument in the other database should not be affected", snap4.Exists); + AssertDeepEq(snap4.ToDictionary(), data2); + AssertEq(snap2.Equals(snap4), true); + customApp.Dispose(); + }); + } + + Task TestTerminateMultiDBIndependently() { + return Async(() => { + // Create 2 firestore instances and populate with documents. + FirebaseApp customApp = FirebaseApp.Create(FirebaseApp.DefaultInstance.Options, "terminate-multi-DB-independently"); + FirebaseFirestore customDb1 = TestFirestore(customApp, MULTI_DATABASE_ONE); + FirebaseFirestore customDb2 = TestFirestore(customApp, MULTI_DATABASE_TWO); + + DocumentReference doc1 = customDb1.Collection("foo").Document(); + var data1 = TestData(1); + Await(doc1.SetAsync(data1)); + + DocumentReference doc2 = customDb2.Collection("foo").Document(); + var data2 = TestData(2); + Await(doc2.SetAsync(data2)); + + // Documents from different databases should be able to get written and read + // without interfering with each other. + DocumentSnapshot snap1 = Await(doc1.GetSnapshotAsync()); + Assert("Written document in the first firestore should exist", snap1.Exists); + AssertDeepEq(snap1.ToDictionary(), data1); + DocumentSnapshot snap2 = Await(doc2.GetSnapshotAsync()); + Assert("Written document in the second firestore should exist", snap2.Exists); + AssertDeepEq(snap2.ToDictionary(), data2); + AssertEq(snap1.Equals(snap2), false); + + + // Terminate one of the database. + AssertTaskSucceeds(customDb1.TerminateAsync()); + + // One database should not be affected by the termination of other databases. + DocumentSnapshot snap3 = Await(doc2.GetSnapshotAsync()); + Assert("Dcoument in other database should not be affected", snap3.Exists); + AssertDeepEq(snap3.ToDictionary(), data2); + AssertEq(snap2.Equals(snap3), true); + + customApp.Dispose(); + }); + } + + Task TestTerminateAppWithMultiDB() { + return Async(() => { + // Create 2 firestore instances and populate with documents. + FirebaseApp customApp = FirebaseApp.Create(FirebaseApp.DefaultInstance.Options, "terminate-app-with-multi-DB"); + FirebaseFirestore defaultDb = TestFirestore(customApp); + FirebaseFirestore customDb = TestFirestore(customApp, "test-db"); + + DocumentReference doc1 = defaultDb.Collection("foo").Document(); + var data1 = TestData(1); + Await(doc1.SetAsync(data1)); + + DocumentReference doc2 = customDb.Collection("foo").Document(); + var data2 = TestData(2); + Await(doc2.SetAsync(data2)); + + // Documents from different databases should be able to get written and read + // without interfering with each other. + DocumentSnapshot snap1 = Await(doc1.GetSnapshotAsync()); + Assert("Written document in the first firestore should exist", snap1.Exists); + AssertDeepEq(snap1.ToDictionary(), data1); + DocumentSnapshot snap2 = Await(doc2.GetSnapshotAsync()); + Assert("Written document in the second firestore should exist", snap2.Exists); + AssertDeepEq(snap2.ToDictionary(), data2); + AssertEq(snap1.Equals(snap2), false); + + // Terminate the app. + customApp.Dispose(); + + // All databases under the app should be disposed. + Assert("App property should be null in default database", defaultDb.App == null); + Assert("App property should be null in custom database", customDb.App == null); + AssertException(typeof(InvalidOperationException), () => defaultDb.Collection("foo")); + AssertException(typeof(InvalidOperationException), () => customDb.Collection("foo")); + }); + } + + Task TestRestartCustomFirestore() { + return Async(() => { + // Create a firestore instance and populate it with a document. + FirebaseApp customApp = FirebaseApp.Create(FirebaseApp.DefaultInstance.Options, "getinstance-restart"); + FirebaseFirestore customDb1 = TestFirestore(customApp, "test-db"); + + DocumentReference doc1 = customDb1.Collection("foo").Document(); + var data = TestData(); + Await(doc1.SetAsync(data)); + DocumentSnapshot snap1 = Await(doc1.GetSnapshotAsync()); + Assert("Written document in the firestore should exist", snap1.Exists); + AssertDeepEq(snap1.ToDictionary(), data); + + // Terminate the database and re-create a new instance with same app and same database name. + AssertTaskSucceeds(customDb1.TerminateAsync()); + + FirebaseFirestore customDb2 = TestFirestore(customApp, "test-db"); + Assert("GetInstance() should return a different instance when firestore is re-started", customDb1 != customDb2); + + // The new firestore instance can read previously saved documents. + DocumentReference doc2 = customDb2.Document(doc1.Path); + DocumentSnapshot snap2 = Await(doc2.GetSnapshotAsync()); + Assert("Written document in the firestore should exist", snap2.Exists); + AssertDeepEq(snap2.ToDictionary(), data); + + customApp.Dispose(); + }); + } + Task TestWriteDocument() { return Async(() => { DocumentReference doc = TestDocument(); @@ -794,6 +962,48 @@ Task TestMultiInstanceSnapshotsInSyncListeners() { }); } + Task TestMultiDBSnapshotsInSyncListeners() { + return Async(() => { + FirebaseApp customApp = FirebaseApp.Create(FirebaseApp.DefaultInstance.Options, "multi-DB-snapshots-in-sync"); + FirebaseFirestore customDb1 = TestFirestore(customApp, MULTI_DATABASE_ONE); + FirebaseFirestore customDb2 = TestFirestore(customApp, MULTI_DATABASE_TWO); + + DocumentReference db1Doc = customDb1.Collection("foo").Document(); + DocumentReference db2Doc = customDb2.Collection("foo").Document(); + + var db1SyncAccumulator = new EventAccumulator(MainThreadId, FailTest); + var db1SyncListener = customDb1.ListenForSnapshotsInSync(() => { + db1SyncAccumulator.Listener("customDb1 in sync"); + }); + db1SyncAccumulator.Await(); + + var db2SyncAccumulator = new EventAccumulator(MainThreadId, FailTest); + customDb2.ListenForSnapshotsInSync(() => { db2SyncAccumulator.Listener("customDb2 in sync"); }); + db2SyncAccumulator.Await(); + + db1Doc.Listen((snap) => { }); + db1SyncAccumulator.Await(); + + db2Doc.Listen((snap) => { }); + db2SyncAccumulator.Await(); + + // At this point we have two firestore instances and separate listeners + // attached to each one and all are in an idle state. Once the second + // instance is disposed the listeners on the first instance should + // continue to operate normally. + db2SyncAccumulator.ThrowOnAnyEvent(); + customDb2.TerminateAsync(); + + Await(db1Doc.SetAsync(TestData(2))); + db1SyncAccumulator.Await(); + + // TODO(b/158580488): Remove this line once the null ref exception + // during snapshots-in-sync listener cleanup is fixed in C++. + db1SyncListener.Stop(); + customApp.Dispose(); + }); + } + Task TestMultiInstanceDocumentReferenceListeners() { return Async(() => { var db1Doc = TestDocument(); @@ -817,7 +1027,6 @@ Task TestMultiInstanceDocumentReferenceListeners() { // instance is disposed the listeners on the first instance should // continue to operate normally and the listeners on the second // instance should not receive any more events. - db2DocAccumulator.ThrowOnAnyEvent(); app2.Dispose(); @@ -1488,6 +1697,57 @@ Task TestTransactionsInParallel() { }); } + Task TestTransactionsFromMultiDBInParallel() { + return Async(() => { + FirebaseApp app = FirebaseApp.Create(FirebaseApp.DefaultInstance.Options, "multi-DB-transactions-in-parallel"); + + int numOfFirestores = 3; + int numTransactionsPerFirestore = 3; + + FirebaseFirestore[] firestores = new FirebaseFirestore[3]; + DocumentReference[] docs = new DocumentReference[3]; + for (int i = 0; i < numOfFirestores; i++) { + FirebaseFirestore firestore = TestFirestore(app, "test-db" + ( i==0 ? "" : "-" + i )); + firestores[i] = firestore; + docs[i] = firestore.Collection("foo").Document(); + } + + List tasks = new List(); + for (int i = 0; i < numTransactionsPerFirestore; i++) { + for (int j = 0; j < numOfFirestores; j++) { + FirebaseFirestore firestore = firestores[j]; + DocumentReference currentDoc = docs[j]; + Task txnTask = firestore.RunTransactionAsync(transaction => { + return transaction.GetSnapshotAsync(currentDoc).ContinueWith(task => { + DocumentSnapshot currentSnapshot = task.Result; + int currentValue; + if (currentSnapshot.TryGetValue("count", out currentValue)) { + transaction.Update(currentDoc, "count", currentValue + 1); + } else { + var data = new Dictionary { { "count", 1 } }; + transaction.Set(currentDoc, data); + } + }); + }); + tasks.Add(txnTask); + } + } + + foreach (Task task in tasks) { + AssertTaskSucceeds(task); + } + + foreach (DocumentReference doc in docs) { + DocumentSnapshot snapshot = AssertTaskSucceeds(doc.GetSnapshotAsync(Source.Server)); + int actualValue = snapshot.GetValue("count", ServerTimestampBehavior.None); + int expectedValue = numTransactionsPerFirestore; + AssertEq(actualValue, expectedValue); + } + + app.Dispose(); + }); + } + Task TestTransactionWithExplicitMaxAttempts() { return Async(() => { var options = new TransactionOptions(); @@ -2676,13 +2936,29 @@ Task TestClearPersistence() { string path; // Verify that ClearPersistenceAsync() succeeds when invoked on a newly-created - // FirebaseFirestore instance. + // FirebaseFirestore instance using different GetInstance methods. + { + var db = FirebaseFirestore.DefaultInstance; + AssertTaskSucceeds(db.TerminateAsync()); + AssertTaskSucceeds(db.ClearPersistenceAsync()); + + } + { + var db = FirebaseFirestore.GetInstance("test-db"); + AssertTaskSucceeds(db.ClearPersistenceAsync()); + } { var app = FirebaseApp.Create(defaultOptions, "TestClearPersistenceApp"); var db = FirebaseFirestore.GetInstance(app); AssertTaskSucceeds(db.ClearPersistenceAsync()); app.Dispose(); } + { + var app = FirebaseApp.Create(defaultOptions, "TestClearPersistenceApp"); + var db = FirebaseFirestore.GetInstance(app, "test-db"); + AssertTaskSucceeds(db.ClearPersistenceAsync()); + app.Dispose(); + } // Create a document to use to verify the behavior of ClearPersistenceAsync(). { @@ -3806,7 +4082,13 @@ Task TestFirestoreDispose() { customApp.Dispose(); Assert("App property should be null", customDb.App == null); } - + { + FirebaseApp customApp = FirebaseApp.Create(FirebaseApp.DefaultInstance.Options, "dispose-app-to-multiDB-firestore"); + FirebaseFirestore defaultDb = TestFirestore(customApp); + FirebaseFirestore customDb = TestFirestore(customApp, "test-db"); + customApp.Dispose(); + Assert("App property should be null", defaultDb.App == null); + } // Verify that all non-static methods in `FirebaseFirestore` throw if invoked after // the instance is disposed. { @@ -3913,14 +4195,46 @@ Task TestFirestoreGetInstance() { return Async(() => { // Verify that invoking `FirebaseFirestore.GetInstance()` with the same `FirebaseApp` // returns the exact same `FirebaseFirestore` instance. + { + FirebaseFirestore defaultDb1 = FirebaseFirestore.DefaultInstance; + FirebaseFirestore defaultDb2 = FirebaseFirestore.DefaultInstance; + Assert("DefaultInstance() should return the same default instance", defaultDb1 == defaultDb2); + } { FirebaseApp customApp = FirebaseApp.Create(db.App.Options, "getinstance-same-instance"); FirebaseFirestore customDb1 = FirebaseFirestore.GetInstance(customApp); FirebaseFirestore customDb2 = FirebaseFirestore.GetInstance(customApp); - Assert("GetInstance() should return the same instance", customDb1 == customDb2); + Assert("GetInstance(app) should return the same instance when app is the same", customDb1 == customDb2); customApp.Dispose(); } - + { + FirebaseFirestore customDb1 = FirebaseFirestore.GetInstance("test-db"); + FirebaseFirestore customDb2 = FirebaseFirestore.GetInstance("test-db"); + Assert("GetInstance(database) should return the same instance when database name is the same", customDb1 == customDb2); + } + { + FirebaseApp customApp = FirebaseApp.Create(db.App.Options, "getinstance-same-instance"); + FirebaseFirestore customDb1 = FirebaseFirestore.GetInstance(customApp, "test-db"); + FirebaseFirestore customDb2 = FirebaseFirestore.GetInstance(customApp, "test-db"); + Assert("GetInstance(app, database) should return the same instance when app and database name are the same", customDb1 == customDb2); + customApp.Dispose(); + } + + // Verify that invoking `FirebaseFirestore.GetInstance()` with default `FirebaseApp` + // instances and fefault database name return same `FirebaseFirestore` instances. + { + FirebaseApp defaultApp = FirebaseApp.DefaultInstance; + + FirebaseFirestore defaultDb1 = FirebaseFirestore.DefaultInstance; + FirebaseFirestore defaultDb2 = FirebaseFirestore.GetInstance(defaultApp); + FirebaseFirestore defaultDb3 = FirebaseFirestore.GetInstance(DEFAULT_DATABASE); + FirebaseFirestore defaultDb4 = FirebaseFirestore.GetInstance(defaultApp, DEFAULT_DATABASE); + Assert("GetInstance() should return the same default instance between 1 and 2", defaultDb1 == defaultDb2); + Assert("GetInstance() should return the same default instance between 1 and 3", defaultDb1 == defaultDb3); + Assert("GetInstance() should return the same default instance between 1 and 4", defaultDb1 == defaultDb4); + defaultApp.Dispose(); + } + // Verify that invoking `FirebaseFirestore.GetInstance()` with different `FirebaseApp` // instances return distinct and consistent `FirebaseFirestore` instances. { @@ -3936,25 +4250,40 @@ Task TestFirestoreGetInstance() { customAppB.Dispose(); customAppA.Dispose(); } - + + // Verify that invoking `FirebaseFirestore.GetInstance()` with same `FirebaseApp` instance + // but different database name return distinct and consistent `FirebaseFirestore` instances. + { + FirebaseApp customApp = FirebaseApp.Create(db.App.Options, "getinstance-multi-db"); + FirebaseFirestore customDbA1 = FirebaseFirestore.GetInstance(customApp,"test-db-a"); + FirebaseFirestore customDbB1 = FirebaseFirestore.GetInstance(customApp,"test-db-b"); + FirebaseFirestore customDbA2 = FirebaseFirestore.GetInstance(customApp,"test-db-a"); + FirebaseFirestore customDbB2 = FirebaseFirestore.GetInstance(customApp, "test-db-b"); + Assert("GetInstance() should return the same instance A", customDbA1 == customDbA2); + Assert("GetInstance() should return the same instance B", customDbB1 == customDbB2); + Assert("GetInstance() should return distinct instances", customDbA1 != customDbB1); + customApp.Dispose(); + } + // Verify that invoking `FirebaseFirestore.GetInstance()` with a disposed `FirebaseApp` // does not crash. { FirebaseApp customApp = FirebaseApp.Create(db.App.Options, "getinstance-disposed-app"); - FirebaseFirestore.GetInstance(customApp); + FirebaseFirestore.GetInstance(customApp, "test-db"); customApp.Dispose(); AssertException(typeof(ArgumentException), () => FirebaseFirestore.GetInstance(customApp)); } - + // Verify that invoking `FirebaseFirestore.GetInstance()` after `TerminateAsync()` results // in a distinct, functional `FirebaseFirestore` instance. { FirebaseApp customApp = FirebaseApp.Create(db.App.Options, "getinstance-after-terminate"); - FirebaseFirestore customDbBefore = FirebaseFirestore.GetInstance(customApp); + FirebaseFirestore customDbBefore = TestFirestore(customApp); Task terminateTask = customDbBefore.TerminateAsync(); - FirebaseFirestore customDbAfter = FirebaseFirestore.GetInstance(customApp); - FirebaseFirestore customDbAfter2 = FirebaseFirestore.GetInstance(customApp); + + FirebaseFirestore customDbAfter = TestFirestore(customApp); + FirebaseFirestore customDbAfter2 = TestFirestore(customApp); // Wait for completion of the `Task` returned from `TerminateAsync()` *after* calling // `GetInstance()` to ensure that `TerminateAsync()` *synchronously* evicts the // "firestore" objects from both the C++ and C# instance caches (as opposed to evicting @@ -3966,6 +4295,25 @@ Task TestFirestoreGetInstance() { AssertTaskSucceeds(doc.SetAsync(TestData(1))); customApp.Dispose(); } + + // Verify that invoking `FirebaseFirestore.GetInstance()` with custom database name after + // `TerminateAsync()` results in a distinct, functional `FirebaseFirestore` instance. + { + FirebaseApp customApp = FirebaseApp.Create(db.App.Options, "getinstance-after-terminate"); + FirebaseFirestore customDbBefore = FirebaseFirestore.GetInstance(customApp, "test-db"); + Task terminateTask = customDbBefore.TerminateAsync(); + + FirebaseFirestore customDbAfter1 = FirebaseFirestore.GetInstance(customApp,"test-db"); + FirebaseFirestore customDbAfter2 = FirebaseFirestore.GetInstance(customApp,"test-db"); + // Wait for completion of the `Task` returned from `TerminateAsync()` *after* calling + // `GetInstance()` to ensure that `TerminateAsync()` *synchronously* evicts the + // "firestore" objects from both the C++ and C# instance caches (as opposed to evicting + // from the caches *asynchronously*). + AssertTaskSucceeds(terminateTask); + Assert("GetInstance() should return a new instance", customDbBefore != customDbAfter1); + Assert("GetInstance() should return the same instance", customDbAfter1 == customDbAfter2); + customApp.Dispose(); + } }); } @@ -4197,4 +4545,4 @@ public void Add(T element) { } } } -} +} \ No newline at end of file diff --git a/firestore/testapp/Assets/Tests/InvalidArgumentsTest.cs b/firestore/testapp/Assets/Tests/InvalidArgumentsTest.cs index 83a6e8fd..aabc0c9f 100644 --- a/firestore/testapp/Assets/Tests/InvalidArgumentsTest.cs +++ b/firestore/testapp/Assets/Tests/InvalidArgumentsTest.cs @@ -284,8 +284,24 @@ public void FieldValue_ArrayUnion_NullArray() { } [Test] - public void FirebaseFirestore_GetInstance_Null() { - Assert.Throws(() => FirebaseFirestore.GetInstance(null)); + public void FirebaseFirestore_GetInstance_Null_App() { + Assert.Throws(() => FirebaseFirestore.GetInstance((FirebaseApp)null)); + } + + [Test] + public void FirebaseFirestore_GetInstance_Null_Database_Name() { + Assert.Throws(() => FirebaseFirestore.GetInstance((string)null)); + } + + [Test] + public void FirebaseFirestore_GetInstance_Null_App_With_Database_Name() { + Assert.Throws(() => FirebaseFirestore.GetInstance((FirebaseApp)null,"a")); + } + + [Test] + public void FirebaseFirestore_GetInstance_App_With_Null_Database_Name() { + FirebaseApp app = FirebaseApp.DefaultInstance; + Assert.Throws(() => FirebaseFirestore.GetInstance(app,(string)null)); } [Test]