diff --git a/.github/templates/pr.yml b/.github/templates/pr.yml index 1a5273ee4e..ea529077a7 100644 --- a/.github/templates/pr.yml +++ b/.github/templates/pr.yml @@ -47,4 +47,4 @@ jobs: uses: ./.github/workflows/test-weaver.yml name: Test _: #@ template.replace(runTests("Code Coverage")) - _: #@ template.replace(cleanupBaas(["Code Coverage"])) + _: #@ template.replace(cleanupBaas(["Code Coverage"])) \ No newline at end of file diff --git a/.github/templates/test-code-coverage.yml b/.github/templates/test-code-coverage.yml index 04e01689a3..9fb6421714 100644 --- a/.github/templates/test-code-coverage.yml +++ b/.github/templates/test-code-coverage.yml @@ -9,7 +9,7 @@ jobs: run-tests: runs-on: ubuntu-latest name: Code Coverage - timeout-minutes: 45 + timeout-minutes: 90 steps: - #@ setupDotnet() - #@ template.replace(prepareTest(fetchWrappers=True)) diff --git a/.github/templates/test-ios.yml b/.github/templates/test-ios.yml index 3589f01439..c026b4c3c5 100644 --- a/.github/templates/test-ios.yml +++ b/.github/templates/test-ios.yml @@ -9,7 +9,7 @@ jobs: test-xamarin: runs-on: macos-latest name: Xamarin.iOS - timeout-minutes: 45 + timeout-minutes: 90 steps: - #@ template.replace(prepareTest()) - #@ template.replace(buildTests("Tests/Tests.iOS", Platform="iPhoneSimulator")) diff --git a/.github/workflows/deploy-baas.yml b/.github/workflows/deploy-baas.yml index b2352e03d7..c1b2648280 100755 --- a/.github/workflows/deploy-baas.yml +++ b/.github/workflows/deploy-baas.yml @@ -45,7 +45,7 @@ jobs: atlasUrl: ${{ inputs.AtlasBaseUrl}} apiKey: ${{ secrets.AtlasPublicKey}} privateApiKey: ${{ secrets.AtlasPrivateKey }} - clusterSize: M10 + clusterSize: M20 deploy-apps: name: Deploy Apps needs: deploy-baas diff --git a/.github/workflows/test-code-coverage.yml b/.github/workflows/test-code-coverage.yml index 42ca52c30d..cb10761eef 100755 --- a/.github/workflows/test-code-coverage.yml +++ b/.github/workflows/test-code-coverage.yml @@ -28,7 +28,7 @@ jobs: run-tests: runs-on: ubuntu-latest name: Code Coverage - timeout-minutes: 45 + timeout-minutes: 90 steps: - name: Configure .NET uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a diff --git a/.github/workflows/test-ios.yml b/.github/workflows/test-ios.yml index b7ac7f0491..da9dc84874 100755 --- a/.github/workflows/test-ios.yml +++ b/.github/workflows/test-ios.yml @@ -28,7 +28,7 @@ jobs: test-xamarin: runs-on: macos-latest name: Xamarin.iOS - timeout-minutes: 45 + timeout-minutes: 90 steps: - name: Checkout code uses: actions/checkout@v3 diff --git a/Realm/Realm/Extensions/TaskExtensions.cs b/Realm/Realm/Extensions/TaskExtensions.cs index 5f2bb5981a..4d23eb443e 100644 --- a/Realm/Realm/Extensions/TaskExtensions.cs +++ b/Realm/Realm/Extensions/TaskExtensions.cs @@ -22,25 +22,31 @@ internal static class TaskExtensions { - public static Task Timeout(this Task task, int millisecondTimeout) + public static Task Timeout(this Task task, int millisecondTimeout, string detail = null) { return Task.WhenAny(task, Task.Delay(millisecondTimeout)).ContinueWith(t => { - if (t.Result == task) + if (t.Result != task) { - if (task.IsFaulted) + var errorMessage = $"The operation has timed out after {millisecondTimeout} ms."; + if (detail != null) { - throw task.Exception.InnerException; + errorMessage += $" {detail}"; } - return task.Result; + throw new TimeoutException(errorMessage); } - throw new TimeoutException($"The operation has timed out after {millisecondTimeout} ms."); + if (task.IsFaulted) + { + throw task.Exception.InnerException; + } + + return task.Result; }); } - public static async Task Timeout(this Task task, int millisecondTimeout, Task errorTask = null) + public static async Task Timeout(this Task task, int millisecondTimeout, Task errorTask = null, string detail = null) { var tasks = new List { task, Task.Delay(millisecondTimeout) }; if (errorTask != null) @@ -51,7 +57,13 @@ public static async Task Timeout(this Task task, int millisecondTimeout, Task er var completed = await Task.WhenAny(tasks); if (completed != task && completed != errorTask) { - throw new TimeoutException($"The operation has timed out after {millisecondTimeout} ms."); + var errorMessage = $"The operation has timed out after {millisecondTimeout} ms."; + if (detail != null) + { + errorMessage += $" {detail}"; + } + + throw new TimeoutException(errorMessage); } await completed; diff --git a/Realm/Realm/Handles/SubscriptionSetHandle.cs b/Realm/Realm/Handles/SubscriptionSetHandle.cs index 02ac4d6dcf..bcd6cf8cbd 100644 --- a/Realm/Realm/Handles/SubscriptionSetHandle.cs +++ b/Realm/Realm/Handles/SubscriptionSetHandle.cs @@ -269,6 +269,10 @@ public async Task WaitForStateChangeAsync() return await tcs.Task; } + catch (Exception ex) when (ex.Message == "Active SubscriptionSet without a SubscriptionStore") + { + throw new TaskCanceledException("The SubscriptionSet was closed before the wait could complete. This is likely because the Realm it belongs to was disposed."); + } finally { tcsHandle.Free(); @@ -327,7 +331,7 @@ private static void HandleStateWaitCallback(IntPtr taskCompletionSource, Subscri tcs.TrySetResult(state); break; case RealmValueType.Int when message.AsInt() == -1: - tcs.TrySetCanceled(); + tcs.TrySetException(new TaskCanceledException("The SubscriptionSet was closed before the wait could complete. This is likely because the Realm it belongs to was disposed.")); break; default: tcs.TrySetException(new SubscriptionException(message.AsString())); diff --git a/Realm/Realm/Logging/Logger.cs b/Realm/Realm/Logging/Logger.cs index f79f4bc864..17012b520e 100644 --- a/Realm/Realm/Logging/Logger.cs +++ b/Realm/Realm/Logging/Logger.cs @@ -17,8 +17,11 @@ //////////////////////////////////////////////////////////////////////////// using System; +using System.Collections.Concurrent; using System.Runtime.InteropServices; using System.Text; +using System.Threading; +using System.Threading.Tasks; using Realms.Sync; namespace Realms.Logging @@ -165,7 +168,7 @@ protected override void LogImpl(LogLevel level, string message) private class FileLogger : Logger { - private readonly object locker = new object(); + private readonly object locker = new(); private readonly string _filePath; private readonly Encoding _encoding; @@ -205,7 +208,7 @@ protected override void LogImpl(LogLevel level, string message) internal class InMemoryLogger : Logger { - private readonly StringBuilder _builder = new StringBuilder(); + private readonly StringBuilder _builder = new(); protected override void LogImpl(LogLevel level, string message) { @@ -225,5 +228,63 @@ public string GetLog() public void Clear() => _builder.Clear(); } + + internal class AsyncFileLogger : Logger, IDisposable + { + private readonly ConcurrentQueue _queue = new(); + private readonly string _filePath; + private readonly Encoding _encoding; + private readonly AutoResetEvent _hasNewItems = new(false); + private readonly AutoResetEvent _flush = new(false); + private readonly Task _runner; + private volatile bool _isFlushing; + + public AsyncFileLogger(string filePath, Encoding encoding = null) + { + _filePath = filePath; + _encoding = encoding ?? Encoding.UTF8; + _runner = Task.Run(Run); + } + + public void Dispose() + { + _isFlushing = true; + _flush.Set(); + _runner.Wait(); + + _hasNewItems.Dispose(); + _flush.Dispose(); + } + + protected override void LogImpl(LogLevel level, string message) + { + if (!_isFlushing) + { + _queue.Enqueue(FormatLog(level, message)); + _hasNewItems.Set(); + } + } + + private void Run() + { + var sb = new StringBuilder(); + while (true) + { + sb.Clear(); + WaitHandle.WaitAny(new[] { _hasNewItems, _flush }); + while (_queue.TryDequeue(out var item)) + { + sb.AppendLine(item); + } + + System.IO.File.AppendAllText(_filePath, sb.ToString(), _encoding); + + if (_isFlushing) + { + return; + } + } + } + } } } diff --git a/Realm/Realm/Realm.cs b/Realm/Realm/Realm.cs index f0eb4f3ddb..c24b9457be 100644 --- a/Realm/Realm/Realm.cs +++ b/Realm/Realm/Realm.cs @@ -429,11 +429,7 @@ public void Dispose() if (!IsClosed) { _activeTransaction?.Dispose(); - - if (SharedRealmHandle.OwnsNativeRealm) - { - _state.RemoveRealm(this); - } + _state.RemoveRealm(this, closeOnEmpty: SharedRealmHandle.OwnsNativeRealm); _state = null; SharedRealmHandle.Close(); // Note: this closes the *handle*, it does not trigger realm::Realm::close(). @@ -1708,14 +1704,14 @@ public void AddRealm(Realm realm) /// 4. Once the last instance is deleted, the CSharpBindingContext destructor is called, which frees the state GCHandle. /// 5. The State is now eligible for collection, and its fields will be GC-ed. /// - public void RemoveRealm(Realm realm) + public void RemoveRealm(Realm realm, bool closeOnEmpty) { _weakRealms.RemoveAll(r => { return !r.TryGetTarget(out var other) || ReferenceEquals(realm, other); }); - if (!_weakRealms.Any()) + if (closeOnEmpty && !_weakRealms.Any()) { realm.SharedRealmHandle.CloseRealm(); } diff --git a/Tests/Realm.Tests/Database/InstanceTests.cs b/Tests/Realm.Tests/Database/InstanceTests.cs index f65eb87cdf..e725f2a5aa 100644 --- a/Tests/Realm.Tests/Database/InstanceTests.cs +++ b/Tests/Realm.Tests/Database/InstanceTests.cs @@ -617,7 +617,7 @@ public void GetInstanceAsync_ExecutesMigrationsInBackground() var sw = new Stopwatch(); sw.Start(); - using var realm = await GetRealmAsync(config).Timeout(1000); + using var realm = await GetRealmAsync(config, 1000); sw.Stop(); diff --git a/Tests/Realm.Tests/Database/NotificationTests.cs b/Tests/Realm.Tests/Database/NotificationTests.cs index 73e32fe477..e3483825db 100644 --- a/Tests/Realm.Tests/Database/NotificationTests.cs +++ b/Tests/Realm.Tests/Database/NotificationTests.cs @@ -57,14 +57,11 @@ public void ShouldTriggerRealmChangedEvent() [Test] public void RealmError_WhenNoSubscribers_OutputsMessageInConsole() { - using var sw = new StringWriter(); - var original = Logger.Default; - - Logger.Default = Logger.Function(sw.WriteLine); + var logger = new Logger.InMemoryLogger(); + Logger.Default = logger; _realm.NotifyError(new Exception()); - Assert.That(sw.ToString(), Does.Contain("exception").And.Contains("Realm.Error")); - Logger.Default = original; + Assert.That(logger.GetLog(), Does.Contain("exception").And.Contains("Realm.Error")); } [Test] diff --git a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/ObjectWithPartitionValue_generated.cs b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/ObjectWithPartitionValue_generated.cs index 0aebd91844..a115279234 100644 --- a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/ObjectWithPartitionValue_generated.cs +++ b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/ObjectWithPartitionValue_generated.cs @@ -16,7 +16,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.Specialized; using System.ComponentModel; using System.IO; using System.Linq; @@ -41,6 +40,8 @@ public partial class ObjectWithPartitionValue : IRealmObject, INotifyPropertyCha Realms.Schema.Property.Primitive("Guid", Realms.RealmValueType.Guid, isPrimaryKey: false, isIndexed: false, isNullable: false, managedName: "Guid"), }.Build(); + private ObjectWithPartitionValue() {} + #region IRealmObject implementation private IObjectWithPartitionValueAccessor _accessor; @@ -283,7 +284,7 @@ internal class ObjectWithPartitionValueUnmanagedAccessor : Realms.UnmanagedAcces { public override ObjectSchema ObjectSchema => ObjectWithPartitionValue.RealmSchema; - private string _id; + private string _id = Guid.NewGuid().ToString(); public string Id { get => _id; diff --git a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/SyncObjectWithRequiredStringList_generated.cs b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/SyncObjectWithRequiredStringList_generated.cs index 572370d6ed..ff1cd4acb3 100644 --- a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/SyncObjectWithRequiredStringList_generated.cs +++ b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/SyncObjectWithRequiredStringList_generated.cs @@ -16,7 +16,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.Specialized; using System.ComponentModel; using System.IO; using System.Linq; diff --git a/Tests/Realm.Tests/Program.cs b/Tests/Realm.Tests/Program.cs index cd1a3ba627..9fe77e790e 100644 --- a/Tests/Realm.Tests/Program.cs +++ b/Tests/Realm.Tests/Program.cs @@ -31,9 +31,11 @@ public static int Main(string[] args) Console.WriteLine($"Running on {RuntimeInformation.OSDescription} / CPU {RuntimeInformation.ProcessArchitecture} / Framework {RuntimeInformation.FrameworkDescription}"); var autorun = new AutoRun(typeof(Program).GetTypeInfo().Assembly); - var arguments = Sync.SyncTestHelpers.ExtractBaasSettings(args); + IDisposable logger = null; + (args, logger) = Sync.SyncTestHelpers.SetLoggerFromArgs(args); + args = Sync.SyncTestHelpers.ExtractBaasSettings(args); - autorun.Execute(arguments); + autorun.Execute(args); var resultPath = args.FirstOrDefault(a => a.StartsWith("--result="))?.Replace("--result=", string.Empty); if (!string.IsNullOrEmpty(resultPath)) @@ -41,6 +43,8 @@ public static int Main(string[] args) TestHelpers.TransformTestResults(resultPath); } + logger?.Dispose(); + return 0; } } diff --git a/Tests/Realm.Tests/RealmTest.cs b/Tests/Realm.Tests/RealmTest.cs index d68a5e69db..c3fa360587 100644 --- a/Tests/Realm.Tests/RealmTest.cs +++ b/Tests/Realm.Tests/RealmTest.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -93,7 +94,7 @@ public void TearDown() _isSetup = false; try { - Realm.DeleteRealm(RealmConfiguration.DefaultConfiguration); + DeleteRealmWithRetries(RealmConfiguration.DefaultConfiguration); } catch { @@ -111,18 +112,18 @@ protected virtual void CustomTearDown() _realms.DrainQueue(realm => { // TODO: this should be an assertion but fails on our migration tests due to https://github.com/realm/realm-core/issues/4605. - // Assert.That(DeleteRealmWithRetries(realm), Is.True, "Couldn't delete a Realm on teardown."); - DeleteRealmWithRetries(realm); + // Assert.That(DeleteRealmWithRetries(realm.Config), Is.True, "Couldn't delete a Realm on teardown."); + DeleteRealmWithRetries(realm.Config); }); } - protected static bool DeleteRealmWithRetries(Realm realm) + protected static bool DeleteRealmWithRetries(RealmConfigurationBase config) { for (var i = 0; i < 100; i++) { try { - Realm.DeleteRealm(realm.Config); + Realm.DeleteRealm(config); return true; } catch @@ -148,11 +149,25 @@ protected Realm GetRealm(string path) return result; } - protected async Task GetRealmAsync(RealmConfigurationBase config, CancellationToken cancellationToken = default) + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "cts is disposed by the using - the compiler seems to be having a hard time due to the ternary")] + protected async Task GetRealmAsync(RealmConfigurationBase config, int timeout = 10000, CancellationToken? cancellationToken = default) { - var result = await Realm.GetInstanceAsync(config, cancellationToken); - CleanupOnTearDown(result); - return result; + using var cts = cancellationToken != null ? null : new CancellationTokenSource(timeout); + try + { + var result = await Realm.GetInstanceAsync(config, cancellationToken ?? cts.Token); + CleanupOnTearDown(result); + return result; + } + catch (TaskCanceledException) + { + if (cts?.IsCancellationRequested == true) + { + throw new TimeoutException($"Timed out waiting for Realm to open after {timeout} ms"); + } + + throw; + } } protected Realm Freeze(Realm realm) diff --git a/Tests/Realm.Tests/Sync/AsymmetricObjectTests.cs b/Tests/Realm.Tests/Sync/AsymmetricObjectTests.cs index b1ccc4dab8..1de07aa64c 100644 --- a/Tests/Realm.Tests/Sync/AsymmetricObjectTests.cs +++ b/Tests/Realm.Tests/Sync/AsymmetricObjectTests.cs @@ -135,10 +135,10 @@ public void AddCollectionOfAsymmetricObjs() }); }); - await WaitForUploadAsync(realm); + await WaitForUploadAsync(realm).Timeout(10_000, detail: "Wait for upload"); var documents = await GetRemoteObjects( - flxConfig.User, nameof(BasicAsymmetricObject.PartitionLike), partitionLike); + flxConfig.User, nameof(BasicAsymmetricObject.PartitionLike), partitionLike).Timeout(10_000, "Get remote objects"); Assert.That(documents.Length, Is.EqualTo(4)); Assert.That(documents.Where(x => x.PartitionLike == partitionLike).Count, Is.EqualTo(4)); diff --git a/Tests/Realm.Tests/Sync/DataTypeSynchronizationTests.cs b/Tests/Realm.Tests/Sync/DataTypeSynchronizationTests.cs index 189c00fc9f..4e47de8664 100644 --- a/Tests/Realm.Tests/Sync/DataTypeSynchronizationTests.cs +++ b/Tests/Realm.Tests/Sync/DataTypeSynchronizationTests.cs @@ -425,7 +425,7 @@ private void TestDictionaryCore(Func(Func(Func getter, Action setter, T item1, T item2, Func equalsOverride = null) diff --git a/Tests/Realm.Tests/Sync/FlexibleSyncTests.cs b/Tests/Realm.Tests/Sync/FlexibleSyncTests.cs index 1d2a9b5489..0b1d2dfe9a 100644 --- a/Tests/Realm.Tests/Sync/FlexibleSyncTests.cs +++ b/Tests/Realm.Tests/Sync/FlexibleSyncTests.cs @@ -193,7 +193,7 @@ public void SubscriptionSet_Add_AddsSubscription() Assert.That(realm.Subscriptions.Version, Is.EqualTo(1)); Assert.That(realm.Subscriptions.Count, Is.EqualTo(1)); Assert.That(realm.Subscriptions.Error, Is.Null); - Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Pending)); + Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Pending), "State should be 'Pending' after change"); AssertSubscriptionDetails(realm.Subscriptions[0], nameof(SyncAllTypesObject)); } @@ -940,7 +940,7 @@ public void SubscriptionSet_WhenParentRealmIsClosed_GetsClosed() realm.Dispose(); - Assert.That(DeleteRealmWithRetries(realm), Is.True); + Assert.That(DeleteRealmWithRetries(realm.Config), Is.True); Assert.Throws(() => _ = subs.Count); } @@ -973,7 +973,7 @@ await Task.Run(() => Assert.That(subsHandle.IsClosed); Assert.That(updatedSubsHandle.IsClosed); - Assert.That(DeleteRealmWithRetries(realm), Is.True); + Assert.That(DeleteRealmWithRetries(realm.Config), Is.True); Assert.Throws(() => _ = subs.Count); Assert.Throws(() => _ = updatedSubs.Count); @@ -1113,7 +1113,7 @@ public void Integration_WaitForSynchronization_EmptyUpdate() await realm.Subscriptions.WaitForSynchronizationAsync(); - Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete)); + Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete), "State should be 'Complete' after synchronization"); }); } @@ -1139,7 +1139,8 @@ public void Integration_CloseRealmBeforeWaitCompletes() waitTask = WaitForSubscriptionsAsync(realm); } - await TestHelpers.AssertThrows(() => waitTask); + var ex = await TestHelpers.AssertThrows(() => waitTask); + Assert.That(ex.Message, Is.EqualTo("The SubscriptionSet was closed before the wait could complete. This is likely because the Realm it belongs to was disposed.")); }); } @@ -1154,7 +1155,7 @@ public void Integration_SubscriptionSet_AddRemove() var realm = await GetFLXIntegrationRealmAsync(); - Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete)); + Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete), "State should be 'Complete' after GetInstanceAsync"); var query = realm.All().Where(o => o.DoubleProperty > 2 && o.GuidProperty == testGuid); @@ -1191,7 +1192,7 @@ public void Integration_SubscriptionSet_MoveObjectOutsideView() var realm = await GetFLXIntegrationRealmAsync(); - Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete)); + Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete), "State should be 'Complete' after GetInstanceAsync"); var query = realm.All().Where(o => o.DoubleProperty > 2 && o.GuidProperty == testGuid); @@ -1220,7 +1221,7 @@ public void Integration_SubscriptionSet_MoveObjectInsideView() var realm = await GetFLXIntegrationRealmAsync(); - Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete)); + Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete), "State should be 'Complete' after GetInstanceAsync"); var query = realm.All().Where(o => o.DoubleProperty > 2 && o.GuidProperty == testGuid); @@ -1650,15 +1651,15 @@ public void Integration_SubscriptionSet_WaitForSynchronization_CanBeCalledMultip realm.Subscriptions.Add(realm.All().Where(o => o.GuidProperty == testGuid)); }); - Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Pending)); + Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Pending), "State should be 'Pending' after change"); await realm.Subscriptions.WaitForSynchronizationAsync(); - Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete)); + Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete), "State should be 'Complete' after synchronization"); // Call WaitForSynchronizationAsync again await realm.Subscriptions.WaitForSynchronizationAsync(); - Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete)); + Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete), "State should remain 'Complete'"); }); } @@ -1760,7 +1761,7 @@ public void Integration_SubscriptionOnUnqueryableField_ShouldError() Assert.That(ex.Message, Does.Contain(nameof(SyncAllTypesObject.StringProperty)).And.Contains(nameof(SyncAllTypesObject))); } - Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Error)); + Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Error), "State should be 'Error' when querying unqueryable field"); Assert.That(realm.Subscriptions.Error.Message, Does.Contain(nameof(SyncAllTypesObject.StringProperty)).And.Contains(nameof(SyncAllTypesObject))); }); } @@ -1787,7 +1788,7 @@ public void Integration_AfterAnError_CanRecover() Assert.That(ex.Message, Does.Contain(nameof(SyncAllTypesObject.StringProperty)).And.Contains(nameof(SyncAllTypesObject))); } - Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Error)); + Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Error), "State should be 'Error' when querying unqueryable field"); Assert.That(realm.Subscriptions.Error.Message, Does.Contain(nameof(SyncAllTypesObject.StringProperty)).And.Contains(nameof(SyncAllTypesObject))); var testGuid = Guid.NewGuid(); @@ -1798,11 +1799,11 @@ public void Integration_AfterAnError_CanRecover() realm.Subscriptions.Add(realm.All().Where(o => o.GuidProperty == testGuid)); }); - Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Pending)); + Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Pending), "State should be 'Pending' after change"); await realm.Subscriptions.WaitForSynchronizationAsync(); - Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete)); + Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete), "State should be 'Complete' after synchronization"); Assert.That(realm.Subscriptions.Error, Is.Null); }); } @@ -1849,7 +1850,7 @@ await Task.Run(() => Assert.That(subs.State, Is.EqualTo(SubscriptionSetState.Superseded)); Assert.That(subs.Version, Is.EqualTo(version)); - Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete)); + Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete), "State should be 'Complete' after synchronization"); }); } @@ -1876,18 +1877,18 @@ public void Integration_InitialSubscriptions_Unnamed([Values(true, false)] bool if (openAsync) { - Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete)); + Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete), "State should be 'Complete' after GetInstanceAsync"); } else { - Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Pending)); - await WaitForSubscriptionsAsync(realm); + Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Pending), "State should be 'Pending' before synchronization"); + await WaitForSubscriptionsAsync(realm).Timeout(20_000); } var query = realm.All().ToArray().Select(o => o.DoubleProperty); Assert.That(query.Count(), Is.EqualTo(2)); Assert.That(query, Is.EquivalentTo(new[] { 1.5, 2.5 })); - }); + }, timeout: 60_000); } [Test] @@ -1914,11 +1915,11 @@ public void Integration_InitialSubscriptions_Named([Values(true, false)] bool op if (openAsync) { - Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete)); + Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete), "State should be 'Complete' after GetInstanceAsync"); } else { - Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Pending)); + Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Pending), "State should be 'Pending' before synchronization"); await WaitForSubscriptionsAsync(realm); } @@ -2047,11 +2048,11 @@ private static async Task UpdateAndWaitForSubscription(IQueryable query, b } }); - Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Pending)); + Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Pending), "State should be 'Pending' after change"); await WaitForSubscriptionsAsync(realm); - Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete)); + Assert.That(realm.Subscriptions.State, Is.EqualTo(SubscriptionSetState.Complete), "State should be 'Complete' after synchronization"); } private static IntPropertyObject GetIntPropertyObject(int value, Guid guid) => new() diff --git a/Tests/Realm.Tests/Sync/PartitionKeyTests.cs b/Tests/Realm.Tests/Sync/PartitionKeyTests.cs index 01f3e9d92c..115efeabc8 100644 --- a/Tests/Realm.Tests/Sync/PartitionKeyTests.cs +++ b/Tests/Realm.Tests/Sync/PartitionKeyTests.cs @@ -34,8 +34,8 @@ public void OpenRealm_StringPK_Works() SyncTestHelpers.RunBaasTestAsync(async () => { var partitionValue = Guid.NewGuid().ToString(); - var config1 = await GetIntegrationConfigAsync(partitionValue).Timeout(20000); - var config2 = await GetIntegrationConfigAsync(partitionValue).Timeout(20000); + var config1 = await GetIntegrationConfigAsync(partitionValue); + var config2 = await GetIntegrationConfigAsync(partitionValue); await RunPartitionKeyTestsCore(config1, config2); }, timeout: 120_000); @@ -47,11 +47,11 @@ public void OpenRealm_Int64PK_Works() SyncTestHelpers.RunBaasTestAsync(async () => { var partitionValue = TestHelpers.Random.Next(int.MinValue, int.MaxValue); - var config1 = await GetIntegrationConfigAsync(partitionValue).Timeout(10000); - var config2 = await GetIntegrationConfigAsync(partitionValue).Timeout(10000); + var config1 = await GetIntegrationConfigAsync(partitionValue); + var config2 = await GetIntegrationConfigAsync(partitionValue); await RunPartitionKeyTestsCore(config1, config2); - }, timeout: 60000); + }, timeout: 120_000); } [Test] @@ -60,11 +60,11 @@ public void OpenRealm_ObjectIdPK_Works() SyncTestHelpers.RunBaasTestAsync(async () => { var partitionValue = ObjectId.GenerateNewId(); - var config1 = await GetIntegrationConfigAsync(partitionValue).Timeout(10000); - var config2 = await GetIntegrationConfigAsync(partitionValue).Timeout(10000); + var config1 = await GetIntegrationConfigAsync(partitionValue); + var config2 = await GetIntegrationConfigAsync(partitionValue); await RunPartitionKeyTestsCore(config1, config2); - }, timeout: 60000); + }, timeout: 120_000); } [Test] @@ -73,11 +73,11 @@ public void OpenRealm_GuidPK_Works() SyncTestHelpers.RunBaasTestAsync(async () => { var partitionValue = Guid.NewGuid(); - var config1 = await GetIntegrationConfigAsync(partitionValue).Timeout(10000); - var config2 = await GetIntegrationConfigAsync(partitionValue).Timeout(10000); + var config1 = await GetIntegrationConfigAsync(partitionValue); + var config2 = await GetIntegrationConfigAsync(partitionValue); await RunPartitionKeyTestsCore(config1, config2); - }, timeout: 60000); + }, timeout: 120_000); } private async Task RunPartitionKeyTestsCore(PartitionSyncConfiguration config1, PartitionSyncConfiguration config2) @@ -97,11 +97,11 @@ private async Task RunPartitionKeyTestsCore(PartitionSyncConfiguration config1, config1.Schema = schema; config2.Schema = schema; - using var realm1 = await GetRealmAsync(config1).Timeout(5000); - using var realm2 = await GetRealmAsync(config2).Timeout(5000); + using var realm1 = await GetRealmAsync(config1, 30_000); + using var realm2 = await GetRealmAsync(config2, 30_000); - await AssertChangePropagation(realm1, realm2).Timeout(15000); - await AssertChangePropagation(realm2, realm1).Timeout(15000); + await AssertChangePropagation(realm1, realm2).Timeout(30000, detail: "Assert changes 1"); + await AssertChangePropagation(realm2, realm1).Timeout(30000, detail: "Assert changes 2"); async Task AssertChangePropagation(Realm first, Realm second) { diff --git a/Tests/Realm.Tests/Sync/SessionTests.cs b/Tests/Realm.Tests/Sync/SessionTests.cs index 197e2813fa..1295917e5f 100644 --- a/Tests/Realm.Tests/Sync/SessionTests.cs +++ b/Tests/Realm.Tests/Sync/SessionTests.cs @@ -19,7 +19,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.Specialized; using System.ComponentModel; using System.IO; using System.Linq; @@ -48,23 +47,11 @@ public class SessionTests : SyncTestBase { private readonly ConcurrentQueue>> _sessionErrorHandlers = new(); -#pragma warning disable CS0618 // Type or member is obsolete - public static readonly object[] AllClientResetHandlers = new object[] { typeof(DiscardUnsyncedChangesHandler), typeof(RecoverUnsyncedChangesHandler), typeof(RecoverOrDiscardUnsyncedChangesHandler), - - // Just to check that we don't break previous code. Remove in next major version - typeof(DiscardLocalResetHandler), - }; - - // Just to check that we don't break previous code. Remove in next major version - public static readonly object[] ObosoleteHandlerCoexistence = new object[] - { - typeof(DiscardUnsyncedChangesHandler), - typeof(DiscardLocalResetHandler), }; [Preserve] @@ -91,17 +78,6 @@ static SessionTests() OnAfterReset = (beforeFrozen, after) => { }, ManualResetFallback = (clientResetException) => { }, }; - - // Just to check that we don't break previous code. Remove in next major version - var preserveObsoleteDiscardHandler = new DiscardLocalResetHandler - { - OnBeforeReset = (beforeFrozen) => { }, - OnAfterReset = (beforeFrozen, after) => { }, - ManualResetFallback = (clientResetException) => { }, - }; - -#pragma warning restore CS0618 // Type or member is obsolete - } public static readonly string[] AppTypes = new[] @@ -167,7 +143,7 @@ public void Session_Error_ShouldPassCorrectSession() Session session = null; var tcs = new TaskCompletionSource(); - var handler = GetErrorEventHandler(tcs, (sender, error) => + Session.Error += GetErrorEventHandler(tcs, (sender, error) => { if (session == null) { @@ -183,8 +159,6 @@ public void Session_Error_ShouldPassCorrectSession() return true; }); - Session.Error += handler; - var config = GetFakeConfig(); using var realm = GetRealm(config); session = GetSession(realm); @@ -203,12 +177,10 @@ public void Session_ClientReset_ManualRecovery_InitiateClientReset(string appTyp { SyncTestHelpers.RunBaasTestAsync(async () => { - var manualOnClientResetTriggered = false; var errorTcs = new TaskCompletionSource(); - SyncConfigurationBase config = appType == AppConfigType.FlexibleSync ? await GetFLXIntegrationConfigAsync() : await GetIntegrationConfigAsync(); + var (config, _) = await GetConfigForApp(appType); config.ClientResetHandler = new ManualRecoveryHandler((e) => { - manualOnClientResetTriggered = true; errorTcs.TrySetResult(e); }); @@ -218,8 +190,6 @@ public void Session_ClientReset_ManualRecovery_InitiateClientReset(string appTyp var clientEx = await errorTcs.Task; - Assert.That(manualOnClientResetTriggered, Is.True); - Assert.That(clientEx.Message, Does.Contain("Bad client file identifier")); Assert.That(clientEx.InnerException, Is.Null); @@ -227,21 +197,19 @@ public void Session_ClientReset_ManualRecovery_InitiateClientReset(string appTyp }); } - [Test, Obsolete("Also tests Session.Error")] + [Test] public void Session_ClientResetHandlers_ManualResetFallback_InitiateClientReset( [ValueSource(nameof(AppTypes))] string appType, [ValueSource(nameof(AllClientResetHandlers))] Type resetHandlerType) { SyncTestHelpers.RunBaasTestAsync(async () => { - var manualResetFallbackHandled = false; var errorTcs = new TaskCompletionSource(); - var config = await GetConfigForApp(appType); + var (config, _) = await GetConfigForApp(appType); void manualCb(ClientResetException err) { - manualResetFallbackHandled = true; errorTcs.TrySetResult(err); } @@ -252,28 +220,13 @@ void beforeCb(Realm _) config.ClientResetHandler = GetClientResetHandler(resetHandlerType, beforeCb: beforeCb, manualCb: manualCb); - using var realm = await GetRealmAsync(config, waitForSync: true); - - // This should be removed when we remove Session.Error - var obsoleteSessionErrorTriggered = false; - - // priority is given to the newer appoach in SyncConfigurationBase, so this should never be reached - Session.Error += OnSessionError; + using var realm = await GetRealmAsync(config, waitForSync: true, timeout: 20_000); await TriggerClientReset(realm); - var clientEx = await errorTcs.Task; - - Assert.That(manualResetFallbackHandled, Is.True); + var clientEx = await errorTcs.Task.Timeout(20_000, "Expected client reset"); await TryInitiateClientReset(realm, clientEx, (int)ClientError.AutoClientResetFailed); - - Assert.That(obsoleteSessionErrorTriggered, Is.False); - - void OnSessionError(object sender, ErrorEventArgs error) - { - obsoleteSessionErrorTriggered = true; - } }); } @@ -288,7 +241,7 @@ public void Session_ClientResetHandlers_OnBefore_And_OnAfter( var onAfterTriggered = false; var tcs = new TaskCompletionSource(); - var config = await GetConfigForApp(appType); + var (config, _) = await GetConfigForApp(appType); var beforeCb = GetOnBeforeHandler(tcs, beforeFrozen => { @@ -304,15 +257,15 @@ public void Session_ClientResetHandlers_OnBefore_And_OnAfter( onAfterTriggered = true; }); config.ClientResetHandler = GetClientResetHandler(resetHandlerType, beforeCb, afterCb); - using var realm = await GetRealmAsync(config, waitForSync: true); + using var realm = await GetRealmAsync(config, waitForSync: true, timeout: 20000); await TriggerClientReset(realm); - await tcs.Task; + await tcs.Task.Timeout(30_000, "Wait for client reset"); Assert.That(onBeforeTriggered, Is.True); Assert.That(onAfterTriggered, Is.True); - }); + }, timeout: 120_000); } [TestCaseSource(nameof(AppTypes))] @@ -323,31 +276,17 @@ public void Session_AutomaticRecoveryFallsbackToDiscardLocal(string appType) var automaticResetCalled = false; var discardLocalResetCalled = false; - SyncConfigurationBase config = appType == AppConfigType.FlexibleSync ? await GetFLXIntegrationConfigAsync() : await GetIntegrationConfigAsync(); - var flxSyncPartition = Guid.NewGuid(); - - if (config is FlexibleSyncConfiguration flxConf) - { - flxConf.PopulateInitialSubscriptions = (realm) => - { - var query = realm.All().Where(p => p.Guid == flxSyncPartition); - realm.Subscriptions.Add(query); - }; - } + var (config, guid) = await GetConfigForApp(appType); var tcsAfterClientReset = new TaskCompletionSource(); - config.Schema = new[] { typeof(ObjectWithPartitionValue) }; var afterAutomaticResetCb = GetOnAfterHandler(tcsAfterClientReset, (before, after) => { - Assert.That(automaticResetCalled, Is.False); - Assert.That(discardLocalResetCalled, Is.False); automaticResetCalled = true; }); + var afterDiscardLocalResetCb = GetOnAfterHandler(tcsAfterClientReset, (before, after) => { - Assert.That(automaticResetCalled, Is.False); - Assert.That(discardLocalResetCalled, Is.False); discardLocalResetCalled = true; Assert.That(after.All().Count, Is.EqualTo(0)); }); @@ -369,17 +308,13 @@ public void Session_AutomaticRecoveryFallsbackToDiscardLocal(string appType) realm.Write(() => { - realm.Add(new ObjectWithPartitionValue - { - Id = Guid.NewGuid().ToString(), - Guid = flxSyncPartition - }); + realm.Add(new ObjectWithPartitionValue(guid)); }); await DisableClientResetRecoveryOnServer(appType); await TriggerClientReset(realm); - await tcsAfterClientReset.Task; + await tcsAfterClientReset.Task.Timeout(20_000, "Expected client reset"); Assert.That(automaticResetCalled, Is.False); Assert.That(discardLocalResetCalled, Is.True); }); @@ -395,7 +330,7 @@ public void Session_AutomaticRecoveryFallsbackToDiscardLocal(string appType) * 6. only now clientB goes online, downloads and merges the changes. clientB will have innerObj[0,1,3] * 7. clientA will also have innerObj[0,1,3] */ - [Test] + [Test, NUnit.Framework.Explicit("This is an integration test testing the client reset behavior and should probably be in Core")] public void SessionIntegrationTest_ClientResetHandlers_OutOfBoundArrayInsert_AddedToTail() { SyncTestHelpers.RunBaasTestAsync(async () => @@ -431,7 +366,7 @@ public void SessionIntegrationTest_ClientResetHandlers_OutOfBoundArrayInsert_Add toAdd.Strings.Add("2"); return realmA.Add(toAdd); }); - await WaitForUploadAsync(realmA); + await WaitForUploadAsync(realmA).Timeout(10_000, detail: "Wait for upload realm A"); var sessionA = GetSession(realmA); sessionA.Stop(); @@ -458,7 +393,7 @@ public void SessionIntegrationTest_ClientResetHandlers_OutOfBoundArrayInsert_Add }; using var realmB = await GetRealmAsync(configB, waitForSync: true); - await WaitForDownloadAsync(realmB); + await WaitForDownloadAsync(realmB).Timeout(10_000, detail: "Wait for download realm B"); var originalObjStr = realmB.All().Single().Strings; Assert.That(originalObjStr.ToArray(), Is.EqualTo(new[] { "0", "1", "2" })); @@ -477,7 +412,7 @@ public void SessionIntegrationTest_ClientResetHandlers_OutOfBoundArrayInsert_Add await TriggerClientReset(realmB, restartSession: false); // ===== clientA ===== - await tcsAfterClientResetA.Task; + await tcsAfterClientResetA.Task.Timeout(10_000, "Client Reset A"); var tcsAfterRemoteUpdateA = new TaskCompletionSource(); @@ -502,29 +437,39 @@ public void SessionIntegrationTest_ClientResetHandlers_OutOfBoundArrayInsert_Add // ===== clientB ===== sessionB.Start(); - await tcsAfterClientResetB.Task; - await tcsAfterRemoteUpdateA.Task; + await tcsAfterClientResetB.Task.Timeout(10_000, "Client Reset B"); + await tcsAfterRemoteUpdateA.Task.Timeout(10_000, "After remote update A"); Assert.That(stringsA.ToArray(), Is.EquivalentTo(new[] { "0", "1", "3" })); - }); + }, timeout: 120_000); } - private async Task GetConfigForApp(string appType) + private async Task<(SyncConfigurationBase Config, Guid Guid)> GetConfigForApp(string appType) { var appConfig = SyncTestHelpers.GetAppConfig(appType); var app = App.Create(appConfig); var user = await GetUserAsync(app); + var guid = Guid.NewGuid(); SyncConfigurationBase config; if (appType == AppConfigType.FlexibleSync) { - config = GetFLXIntegrationConfig(user); + var flxConfig = GetFLXIntegrationConfig(user); + flxConfig.PopulateInitialSubscriptions = (realm) => + { + var query = realm.All().Where(o => o.Guid == guid); + realm.Subscriptions.Add(query); + }; + + config = flxConfig; } else { config = GetIntegrationConfig(user); } - return config; + config.Schema = new[] { typeof(ObjectWithPartitionValue) }; + + return (config, guid); } [Test] @@ -539,18 +484,8 @@ public void Session_ClientResetHandlers_AccessRealm_OnBeforeReset( { var tcs = new TaskCompletionSource(); var onBeforeTriggered = false; - var guid = Guid.NewGuid(); - var config = await GetConfigForApp(appType); - config.Schema = new[] { typeof(ObjectWithPartitionValue) }; - if (config is FlexibleSyncConfiguration flxConfig) - { - flxConfig.PopulateInitialSubscriptions = (realm) => - { - var query = realm.All().Where(o => o.Guid == guid); - realm.Subscriptions.Add(query); - }; - } + var (config, guid) = await GetConfigForApp(appType); var beforeCb = GetOnBeforeHandler(tcs, beforeFrozen => { @@ -566,25 +501,21 @@ public void Session_ClientResetHandlers_AccessRealm_OnBeforeReset( realm.Write(() => { - realm.Add(new ObjectWithPartitionValue + realm.Add(new ObjectWithPartitionValue(guid) { - Id = Guid.NewGuid().ToString(), Value = alwaysSynced, - Guid = guid }); }); - await WaitForUploadAsync(realm); + await WaitForUploadAsync(realm).Timeout(15_000, detail: "Wait for upload"); var session = GetSession(realm); session.Stop(); realm.Write(() => { - realm.Add(new ObjectWithPartitionValue + realm.Add(new ObjectWithPartitionValue(guid) { - Id = Guid.NewGuid().ToString(), Value = maybeSynced, - Guid = guid }); }); @@ -592,7 +523,7 @@ public void Session_ClientResetHandlers_AccessRealm_OnBeforeReset( await TriggerClientReset(realm); - await tcs.Task; + await tcs.Task.Timeout(20_000, "Expected client reset"); Assert.That(onBeforeTriggered, Is.True); var objs = realm.All(); @@ -610,12 +541,12 @@ public void Session_ClientResetHandlers_AccessRealm_OnBeforeReset( AssertOnObjectPair(realm); } - void AssertOnObjectPair(Realm realm) + static void AssertOnObjectPair(Realm realm) { Assert.That(realm.All().ToArray().Select(o => o.Value), Is.EquivalentTo(new[] { alwaysSynced, maybeSynced })); } - }); + }, timeout: 120_000); } [Test] @@ -630,18 +561,8 @@ public void Session_ClientResetHandlers_AccessRealms_OnAfterReset( { var tcs = new TaskCompletionSource(); var onAfterTriggered = false; - var guid = Guid.NewGuid(); - var config = await GetConfigForApp(appType); - - if (config is FlexibleSyncConfiguration flxConf) - { - flxConf.PopulateInitialSubscriptions = (realm) => - { - var query = realm.All().Where(o => o.Guid == guid); - realm.Subscriptions.Add(query); - }; - } + var (config, guid) = await GetConfigForApp(appType); var afterCb = GetOnAfterHandler(tcs, (beforeFrozen, after) => { @@ -651,38 +572,33 @@ public void Session_ClientResetHandlers_AccessRealms_OnAfterReset( onAfterTriggered = true; }); config.ClientResetHandler = GetClientResetHandler(resetHandlerType, afterCb: afterCb); - config.Schema = new[] { typeof(ObjectWithPartitionValue) }; using var realm = await GetRealmAsync(config, waitForSync: true); realm.Write(() => { - realm.Add(new ObjectWithPartitionValue + realm.Add(new ObjectWithPartitionValue(guid) { - Id = Guid.NewGuid().ToString(), Value = alwaysSynced, - Guid = guid }); }); - await WaitForUploadAsync(realm); + await WaitForUploadAsync(realm).Timeout(20_000, detail: "Wait for upload"); var session = GetSession(realm); session.Stop(); realm.Write(() => { - realm.Add(new ObjectWithPartitionValue + realm.Add(new ObjectWithPartitionValue(guid) { - Id = Guid.NewGuid().ToString(), Value = maybeSynced, - Guid = guid }); }); await TriggerClientReset(realm); - await tcs.Task; + await tcs.Task.Timeout(30_000, "Expected client reset"); Assert.That(onAfterTriggered, Is.True); var expected = config.ClientResetHandler.ClientResetMode == ClientResyncMode.Discard ? @@ -691,61 +607,58 @@ public void Session_ClientResetHandlers_AccessRealms_OnAfterReset( await TestHelpers.WaitForConditionAsync(() => realm.All().Count() == expected.Length, attempts: 300); Assert.That(realm.All().ToArray().Select(o => o.Value), Is.EquivalentTo(expected)); - }); + }, timeout: 120_000); } - [TestCaseSource(nameof(ObosoleteHandlerCoexistence))] - public void Session_ClientResetDiscard_TriggersNotifications(Type handlerType) + [Test] + public void Session_ClientResetDiscard_TriggersNotifications() { SyncTestHelpers.RunBaasTestAsync(async () => { // We'll add an object with the wrong partition var config = await GetIntegrationConfigAsync(); config.Schema = new[] { typeof(ObjectWithPartitionValue) }; - config.ClientResetHandler = (ClientResetHandlerBase)Activator.CreateInstance(handlerType); + config.ClientResetHandler = new DiscardUnsyncedChangesHandler(); using var realm = await GetRealmAsync(config, waitForSync: true); realm.Write(() => { - realm.Add(new ObjectWithPartitionValue + realm.Add(new ObjectWithPartitionValue(Guid.NewGuid()) { - Id = Guid.NewGuid().ToString(), Value = "this will sync" }); }); - await WaitForUploadAsync(realm); + await WaitForUploadAsync(realm).Timeout(10_000, detail: "Wait for upload"); var session = GetSession(realm); session.Stop(); realm.Write(() => { - realm.Add(new ObjectWithPartitionValue + realm.Add(new ObjectWithPartitionValue(Guid.NewGuid()) { - Id = Guid.NewGuid().ToString(), Value = "this will be merged at client reset" }); }); var objects = realm.All().AsRealmCollection(); Assert.That(objects.Count, Is.EqualTo(2)); - var tcs = new TaskCompletionSource(); - objects.CollectionChanged += onCollectionChanged; + var tcs = new TaskCompletionSource(); + using var token = objects.SubscribeForNotifications((sender, changes, _) => + { + if (changes != null) + { + tcs.TrySetResult(changes); + } + }); await TriggerClientReset(realm); - var args = await tcs.Task; + var args = await tcs.Task.Timeout(15_000, "Wait for notifications"); - Assert.That(args.Action, Is.EqualTo(NotifyCollectionChangedAction.Remove)); + Assert.That(args.DeletedIndices.Length, Is.EqualTo(1)); Assert.That(objects.Count, Is.EqualTo(1)); - - objects.CollectionChanged -= onCollectionChanged; - - void onCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) - { - tcs.TrySetResult(args); - } }, timeout: 120_000); } @@ -761,9 +674,9 @@ public void Session_ClientResetHandlers_ManualResetFallback_Exception_OnBefore( var manualFallbackTriggered = false; var onAfterResetTriggered = false; - var config = await GetConfigForApp(appType); + var (config, _) = await GetConfigForApp(appType); - BeforeResetCallback beforeCb = beforeFrozen => + void beforeCb(Realm beforeFrozen) { try { @@ -778,7 +691,7 @@ public void Session_ClientResetHandlers_ManualResetFallback_Exception_OnBefore( } throw new Exception("Exception thrown in OnBeforeReset"); - }; + } var afterCb = GetOnAfterHandler(tcs, (beforeFrozen, after) => { @@ -820,7 +733,7 @@ public void Session_ClientResetHandlers_ManualResetFallback_Exception_OnAfter( var manualFallbackTriggered = false; var onAfterResetTriggered = false; - var config = await GetConfigForApp(appType); + var (config, _) = await GetConfigForApp(appType); var beforeCb = GetOnBeforeHandler(tcs, beforeFrozen => { @@ -854,12 +767,12 @@ void afterCb(Realm beforeFrozen, Realm after) await TriggerClientReset(realm); - await tcs.Task; + await tcs.Task.Timeout(20_000, "Expect client reset"); Assert.That(manualFallbackTriggered, Is.True); Assert.That(onBeforeTriggered, Is.True); Assert.That(onAfterResetTriggered, Is.True); - }); + }, timeout: 120_000); } [Test] @@ -893,7 +806,7 @@ public void Session_OnSessionError() }); } - [Test, Obsolete("Testing Sesion.Error compatibility")] + [Test, Obsolete("Testing Session.Error compatibility"), NUnit.Framework.Explicit("Testing obsolete functionality")] public void Session_ClientResetHandlers_Coexistence( [ValueSource(nameof(AppTypes))] string appType, [ValueSource(nameof(AllClientResetHandlers))] Type resetHandlerType) @@ -904,7 +817,7 @@ public void Session_ClientResetHandlers_Coexistence( var onAfterTriggered = false; var tcs = new TaskCompletionSource(); - var config = await GetConfigForApp(appType); + var (config, _) = await GetConfigForApp(appType); var beforeCb = GetOnBeforeHandler(tcs, beforeFrozen => { @@ -921,7 +834,7 @@ public void Session_ClientResetHandlers_Coexistence( config.ClientResetHandler = GetClientResetHandler(resetHandlerType, beforeCb, afterCb); - var handler = new EventHandler((session, error) => + var handler = new EventHandler((_, error) => { if (error.Exception is ClientResetException crex) { @@ -940,7 +853,7 @@ public void Session_ClientResetHandlers_Coexistence( // to avoid a race condition where e.g. both methods are called but because of timing differences `tcs.TrySetResult(true);` is reached // earlier in a call not letting the other finish to run. This would hide an issue. - await tcs.Task; + await tcs.Task.Timeout(10_000, "Client reset expected"); await Task.Delay(1000); Assert.That(onBeforeTriggered, Is.True); @@ -948,7 +861,7 @@ public void Session_ClientResetHandlers_Coexistence( }); } - [Test, Obsolete("Testing Sesion.Error compatibility")] + [Test, Obsolete("Testing Sesion.Error compatibility"), NUnit.Framework.Explicit("Testing obsolete functionality")] public void Session_WithNewClientResetHandlers_DoesntRaiseSessionError( [ValueSource(nameof(AppTypes))] string appType, [ValueSource(nameof(AllClientResetHandlers))] Type resetHandlerType) @@ -957,7 +870,7 @@ public void Session_WithNewClientResetHandlers_DoesntRaiseSessionError( { var obsoleteSessionErrorTriggered = false; - var config = await GetConfigForApp(appType); + var (config, _) = await GetConfigForApp(appType); config.ClientResetHandler = GetClientResetHandler(resetHandlerType); using var realm = await GetRealmAsync(config, waitForSync: true); @@ -981,7 +894,7 @@ void onSessionError(object sender, ErrorEventArgs e) }); } - [Test, Obsolete("Testing Sesion.Error compatibility")] + [Test, Obsolete("Testing Sesion.Error compatibility"), NUnit.Framework.Explicit("Testing obsolete functionality")] public void Session_ClientReset_OldSessionError_InitiateClientReset_Coexistence() { SyncTestHelpers.RunBaasTestAsync(async () => @@ -1021,7 +934,7 @@ void onSessionError(object sender, ErrorEventArgs e) }); } - [Test, Obsolete("Testing Sesion.Error compatibility")] + [Test, Obsolete("Testing Sesion.Error compatibility"), NUnit.Framework.Explicit("Testing obsolete functionality")] public void Session_Error_OldSessionError_Coexistence() { SyncTestHelpers.RunBaasTestAsync(async () => @@ -1039,6 +952,8 @@ public void Session_Error_OldSessionError_Coexistence() Assert.That(error.InnerException == null); Assert.That(obsoleteSessionErrorTriggered, Is.False); obsoleteSessionErrorTriggered = true; + + return true; }); Session.Error += handler; @@ -1053,7 +968,7 @@ public void Session_Error_OldSessionError_Coexistence() }); } - [Test, Obsolete("Testing Sesion.Error compatibility")] + [Test, Obsolete("Testing Sesion.Error compatibility"), NUnit.Framework.Explicit("Testing obsolete functionality")] public void Session_ClientReset_ManualRecovery_Coexistence() { SyncTestHelpers.RunBaasTestAsync(async () => @@ -1098,7 +1013,7 @@ public void Session_ClientReset_ManualRecovery_Coexistence() }); } - [Test, Obsolete("Testing Sesion.Error compatibility")] + [Test, Obsolete("Testing Sesion.Error compatibility"), NUnit.Framework.Explicit("Testing obsolete functionality")] public void Session_Error_OnSessionError_Coexistence() { SyncTestHelpers.RunBaasTestAsync(async () => @@ -1123,6 +1038,7 @@ public void Session_Error_OnSessionError_Coexistence() { Assert.That(obsoleteSessionErrorTriggered, Is.False); obsoleteSessionErrorTriggered = true; + return true; }); // priority is given to the newer appoach in SyncConfigurationBase, so this should never be reached @@ -1279,7 +1195,7 @@ public void Session_ConnectionState_FullFlow() session.Start(); await Task.Delay(1000); session.Stop(); - await completionTCS.Task; + await completionTCS.Task.Timeout(10_000); Assert.That(stateChanged, Is.EqualTo(3)); Assert.That(session.ConnectionState, Is.EqualTo(ConnectionState.Disconnected)); session.PropertyChanged -= NotificationChanged; @@ -1608,10 +1524,10 @@ private static ClientResetHandlerBase GetClientResetHandler( if (afterCb != null) { - var cbName = type == typeof(RecoverOrDiscardUnsyncedChangesHandler) - ? nameof(RecoverOrDiscardUnsyncedChangesHandler.OnAfterRecovery) - : nameof(DiscardUnsyncedChangesHandler.OnAfterReset); - type.GetProperty(cbName).SetValue(handler, afterCb); + var prop = type.GetProperty(nameof(DiscardUnsyncedChangesHandler.OnAfterReset)) + ?? type.GetProperty(nameof(RecoverOrDiscardUnsyncedChangesHandler.OnAfterRecovery)); + + prop.SetValue(handler, afterCb); } if (manualCb != null) @@ -1691,9 +1607,9 @@ private static ClientResetCallback GetManualResetHandler(TaskCompletionSource GetErrorEventHandler(TaskCompletionSource tcs, Func assertions) + private EventHandler GetErrorEventHandler(TaskCompletionSource tcs, Func assertions) { - return new((sender, error) => + var result = new EventHandler((sender, error) => { try { @@ -1707,15 +1623,6 @@ private static EventHandler GetErrorEventHandler(TaskCompletionS tcs.TrySetException(ex); } }); - } - - private EventHandler GetErrorEventHandler(TaskCompletionSource tcs, Action assertions) - { - var result = GetErrorEventHandler(tcs, (sender, error) => - { - assertions(sender, error); - return true; - }); CleanupOnTearDown(result); @@ -1760,14 +1667,25 @@ public partial class ObjectWithPartitionValue : TestRealmObject { [PrimaryKey] [MapTo("_id")] - public string Id { get; set; } + public string Id { get; private set; } = Guid.NewGuid().ToString(); public string Value { get; set; } [MapTo("realm_id")] public string Partition { get; set; } - public Guid Guid { get; set; } + public Guid Guid { get; private set; } + + public ObjectWithPartitionValue(Guid guid) + { + Guid = guid; + } + +#if TEST_WEAVER + private ObjectWithPartitionValue() + { + } +#endif } public partial class SyncObjectWithRequiredStringList : TestRealmObject diff --git a/Tests/Realm.Tests/Sync/SyncConfigurationTests.cs b/Tests/Realm.Tests/Sync/SyncConfigurationTests.cs index 0741461328..9ae97dfa5c 100644 --- a/Tests/Realm.Tests/Sync/SyncConfigurationTests.cs +++ b/Tests/Realm.Tests/Sync/SyncConfigurationTests.cs @@ -95,11 +95,13 @@ public void Test_SyncConfigRelease() WeakReference weakConfigRef = null; SyncTestHelpers.RunBaasTestAsync(async () => { - weakConfigRef = new WeakReference(await GetIntegrationConfigAsync()); - using var realm = await GetRealmAsync((PartitionSyncConfiguration)weakConfigRef.Target); - var session = GetSession(realm); + var config = await GetIntegrationConfigAsync(); + weakConfigRef = new WeakReference(config); + using var realm = await Realm.GetInstanceAsync(config); + var session = realm.SyncSession; Assert.That(weakConfigRef.Target, Is.Not.Null); }); + TearDown(); for (var i = 0; i < 50; i++) diff --git a/Tests/Realm.Tests/Sync/SyncTestBase.cs b/Tests/Realm.Tests/Sync/SyncTestBase.cs index d625b54254..17e0edf076 100644 --- a/Tests/Realm.Tests/Sync/SyncTestBase.cs +++ b/Tests/Realm.Tests/Sync/SyncTestBase.cs @@ -22,7 +22,6 @@ using System.Threading.Tasks; using Baas; using MongoDB.Bson; -using Nito.AsyncEx; using Realms.Sync; using Realms.Sync.Exceptions; using static Realms.Tests.TestHelpers; @@ -36,15 +35,7 @@ public abstract class SyncTestBase : RealmTest private readonly ConcurrentQueue> _apps = new(); private readonly ConcurrentQueue> _clientResetAppsToRestore = new(); - private App _defaultApp; - - protected App DefaultApp - { - get - { - return _defaultApp ?? CreateApp(); - } - } + protected App DefaultApp => CreateApp(); protected App CreateApp(AppConfiguration config = null) { @@ -53,11 +44,6 @@ protected App CreateApp(AppConfiguration config = null) var app = App.Create(config); _apps.Enqueue(app); - if (_defaultApp == null) - { - _defaultApp = app; - } - return app; } @@ -69,16 +55,7 @@ protected override void CustomTearDown() _apps.DrainQueue(app => app.Handle.ResetForTesting()); - _defaultApp = null; - - AsyncContext.Run(async () => - { - while (_clientResetAppsToRestore.TryDequeue(out var appConfigType)) - { - await SyncTestHelpers.SetRecoveryModeOnServer(appConfigType.Value, enabled: true); - appConfigType.Value = null; - } - }); + _clientResetAppsToRestore.DrainQueueAsync(appConfigType => SyncTestHelpers.SetRecoveryModeOnServer(appConfigType, enabled: true)); } protected void CleanupOnTearDown(Session session) @@ -114,12 +91,12 @@ protected static async Task WaitForSubscriptionsAsync(Realm realm) await WaitForDownloadAsync(realm); } - protected static async Task WaitForObjectAsync(T obj, Realm realm2) + protected static async Task WaitForObjectAsync(T obj, Realm realm2, string message = null) where T : IRealmObject { var id = obj.DynamicApi.Get("_id"); - return await TestHelpers.WaitForConditionAsync(() => realm2.FindCore(id), o => o != null); + return await TestHelpers.WaitForConditionAsync(() => realm2.FindCore(id), o => o != null, errorMessage: message); } protected async Task GetUserAsync(App app = null, string username = null, string password = null) @@ -127,14 +104,14 @@ protected async Task GetUserAsync(App app = null, string username = null, app ??= DefaultApp; username ??= SyncTestHelpers.GetVerifiedUsername(); password ??= SyncTestHelpers.DefaultPassword; - await app.EmailPasswordAuth.RegisterUserAsync(username, password); + await app.EmailPasswordAuth.RegisterUserAsync(username, password).Timeout(10_000, detail: "Failed to register user"); var credentials = Credentials.EmailPassword(username, password); for (var i = 0; i < 5; i++) { try { - return await app.LogInAsync(credentials); + return await app.LogInAsync(credentials).Timeout(10_000, "Failed to login user"); } catch (AppException ex) when (ex.Message.Contains("confirmation required")) { @@ -154,10 +131,10 @@ protected User GetFakeUser(App app = null, string id = null, string refreshToken return new User(handle, app); } - protected async Task GetIntegrationRealmAsync(string partition = null, App app = null) + protected async Task GetIntegrationRealmAsync(string partition = null, App app = null, int timeout = 10000) { var config = await GetIntegrationConfigAsync(partition, app); - return await GetRealmAsync(config); + return await GetRealmAsync(config, timeout); } protected async Task GetIntegrationConfigAsync(string partition = null, App app = null, string optionalPath = null, User user = null) @@ -223,9 +200,9 @@ protected async Task DisableClientResetRecoveryOnServer(string appConfigType) _clientResetAppsToRestore.Enqueue(appConfigType); } - protected async Task GetRealmAsync(SyncConfigurationBase config, bool waitForSync = false, CancellationToken cancellationToken = default) + protected async Task GetRealmAsync(SyncConfigurationBase config, bool waitForSync = false, int timeout = 10000, CancellationToken cancellationToken = default) { - var realm = await GetRealmAsync(config, cancellationToken); + var realm = await GetRealmAsync(config, timeout, cancellationToken); if (waitForSync) { await WaitForUploadAsync(realm); @@ -269,7 +246,7 @@ protected async Task TriggerClientReset(Realm realm, bool restartSession = true) session.Stop(); } - await SyncTestHelpers.TriggerClientResetOnServer(syncConfig); + await SyncTestHelpers.TriggerClientResetOnServer(syncConfig).Timeout(10_000, detail: "Trigger client reset"); if (restartSession) { diff --git a/Tests/Realm.Tests/Sync/SyncTestHelpers.cs b/Tests/Realm.Tests/Sync/SyncTestHelpers.cs index 30cd8702ce..1440761537 100644 --- a/Tests/Realm.Tests/Sync/SyncTestHelpers.cs +++ b/Tests/Realm.Tests/Sync/SyncTestHelpers.cs @@ -17,12 +17,17 @@ //////////////////////////////////////////////////////////////////////////// using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.IO; +using System.Text; +using System.Threading; using System.Threading.Tasks; using Baas; using Nito.AsyncEx; using NUnit.Framework; +using Realms.Logging; using Realms.Sync; namespace Realms.Tests.Sync @@ -102,6 +107,9 @@ void HandleSessionError(object _, ErrorEventArgs errorArgs) { TestHelpers.RunAsyncTest(testFunc, timeout); } + + // TODO: remove when https://github.com/realm/realm-core/issues/6052 is fixed + Task.Delay(1000).Wait(); } public static string GetVerifiedUsername() => $"realm_tests_do_autoverify-{Guid.NewGuid()}"; @@ -118,7 +126,14 @@ public static async Task TriggerClientResetOnServer(SyncConfigurationBase config } var result = await config.User.Functions.CallAsync("triggerClientResetOnSyncServer", userId, appId); - Assert.That(result.status, Is.EqualTo(BaasClient.FunctionReturn.Result.success)); + if (result.Deleted > 0) + { + // This is kind of a hack, but it appears like there's a race condition on the server, where the deletion might not be + // registered and the server will not respond with a client reset. Doing the request again gives the server some extra time + // to process the deletion. + result = await config.User.Functions.CallAsync("triggerClientResetOnSyncServer", userId, appId); + Assert.That(result.Deleted, Is.EqualTo(0)); + } } public static async Task ExtractBaasSettingsAsync(string[] args) @@ -134,6 +149,39 @@ public static async Task ExtractBaasSettingsAsync(string[] args) return remainingArgs; } + public static (string[] RemainingArgs, IDisposable Logger) SetLoggerFromArgs(string[] args) + { + var (extracted, remaining) = ArgumentHelper.ExtractArguments(args, "realmloglevel", "realmlogfile"); + + if (extracted.TryGetValue("realmloglevel", out var logLevelStr) && Enum.TryParse(logLevelStr, out var logLevel)) + { + TestHelpers.Output.WriteLine($"Setting log level to {logLevel}"); + + Logger.LogLevel = logLevel; + } + + Logger.AsyncFileLogger logger = null; + if (extracted.TryGetValue("realmlogfile", out var logFile)) + { + if (!Process.GetCurrentProcess().ProcessName.ToLower().Contains("testhost")) + { + TestHelpers.Output.WriteLine($"Setting sync logger to file: {logFile}"); + + // We're running in a test runner, so we need to use the sync logger + Logger.Default = Logger.File(logFile); + } + else + { + TestHelpers.Output.WriteLine($"Setting async logger to file: {logFile}"); + + // We're running standalone (likely on CI), so we use the async logger + Logger.Default = logger = new Logger.AsyncFileLogger(logFile); + } + } + + return (remaining, logger); + } + public static string[] ExtractBaasSettings(string[] args) { return AsyncContext.Run(async () => @@ -157,7 +205,7 @@ private static async Task CreateBaasAppsAsync() var privateApiKey = System.Configuration.ConfigurationManager.AppSettings["PrivateApiKey"]; var groupId = System.Configuration.ConfigurationManager.AppSettings["GroupId"]; - _baasClient = await BaasClient.Atlas(_baseUri, "local", TestHelpers.Output, cluster, apiKey, privateApiKey, groupId); + _baasClient ??= await BaasClient.Atlas(_baseUri, "local", TestHelpers.Output, cluster, apiKey, privateApiKey, groupId); } catch { diff --git a/Tests/Realm.Tests/Sync/SynchronizedInstanceTests.cs b/Tests/Realm.Tests/Sync/SynchronizedInstanceTests.cs index 27c0eb02ef..a9fdcddf8b 100644 --- a/Tests/Realm.Tests/Sync/SynchronizedInstanceTests.cs +++ b/Tests/Realm.Tests/Sync/SynchronizedInstanceTests.cs @@ -543,7 +543,10 @@ public void RemoveAll_RemovesAllElements([Values(true, false)] bool originalEncr Assert.That(realm.All().Count(), Is.EqualTo(DummyDataSize / 2)); - realm.Write(() => { realm.RemoveAll(); }); + realm.Write(() => + { + realm.RemoveAll(); + }); Assert.That(realm.All().Count(), Is.EqualTo(0)); await WaitForUploadAsync(realm); @@ -551,7 +554,7 @@ public void RemoveAll_RemovesAllElements([Values(true, false)] bool originalEncr // Ensure that the Realm can be deleted from the filesystem. If the sync // session was still using it, we would get a permission denied error. - Assert.That(DeleteRealmWithRetries(realm), Is.True); + Assert.That(DeleteRealmWithRetries(realm.Config), Is.True); using var asyncRealm = await GetRealmAsync(realmConfig); Assert.That(asyncRealm.All().Count(), Is.EqualTo(0)); @@ -607,8 +610,8 @@ public void DeleteRealmWorksIfCalledMultipleTimes() openRealm.Dispose(); Assert.That(File.Exists(config.DatabasePath)); - Assert.That(() => DeleteRealmWithRetries(openRealm), Is.True); - Assert.That(() => DeleteRealmWithRetries(openRealm), Is.True); + Assert.That(() => DeleteRealmWithRetries(openRealm.Config), Is.True); + Assert.That(() => DeleteRealmWithRetries(openRealm.Config), Is.True); } [Test] @@ -631,7 +634,7 @@ public void DeleteRealm_AfterDispose_Succeeds([Values(true, false)] bool singleT // Ensure that the Realm can be deleted from the filesystem. If the sync // session was still using it, we would get a permission denied error. - Assert.That(DeleteRealmWithRetries(realm), Is.True); + Assert.That(DeleteRealmWithRetries(realm.Config), Is.True); using var asyncRealm = await GetRealmAsync(asyncConfig); Assert.That(asyncRealm.All().Count(), Is.EqualTo(DummyDataSize / 2)); @@ -649,7 +652,7 @@ public void RealmDispose_ClosesSessions() Assert.That(session.IsClosed); // Dispose should close the session and allow us to delete the Realm. - Assert.That(DeleteRealmWithRetries(realm), Is.True); + Assert.That(DeleteRealmWithRetries(realm.Config), Is.True); } private const int DummyDataSize = 100; diff --git a/Tests/Realm.Tests/TestHelpers.cs b/Tests/Realm.Tests/TestHelpers.cs index 6c3edaa9d1..c979326c11 100644 --- a/Tests/Realm.Tests/TestHelpers.cs +++ b/Tests/Realm.Tests/TestHelpers.cs @@ -345,6 +345,21 @@ public static void DrainQueue(this ConcurrentQueue> queue, Actio } } + public static void DrainQueueAsync(this ConcurrentQueue> queue, Func action) + { + AsyncContext.Run(async () => + { + await Task.Run(async () => + { + while (queue.TryDequeue(out var result)) + { + await action(result.Value); + result.Value = default; + } + }).Timeout(20_000, detail: $"Failed to drain queue: {queue.GetType().Name}"); + }); + } + public static IDisposable Subscribe(this IObservable observable, Action onNext) { var observer = new FunctionObserver(onNext); diff --git a/Tests/Tests.XamarinMac/Tests.XamarinMac.csproj b/Tests/Tests.XamarinMac/Tests.XamarinMac.csproj index 3568bc3362..6dcd81db5b 100644 --- a/Tests/Tests.XamarinMac/Tests.XamarinMac.csproj +++ b/Tests/Tests.XamarinMac/Tests.XamarinMac.csproj @@ -22,7 +22,7 @@ DEBUG; prompt 4 - false + False Mac Developer false false @@ -44,7 +44,7 @@ prompt 4 - false + False false false true diff --git a/Tests/Tests.XamarinMac/ViewController.cs b/Tests/Tests.XamarinMac/ViewController.cs index c76a8da836..55144e1f95 100644 --- a/Tests/Tests.XamarinMac/ViewController.cs +++ b/Tests/Tests.XamarinMac/ViewController.cs @@ -39,18 +39,26 @@ public override void ViewDidAppear() private async Task RunTests() { - StateField.StringValue = "Running tests..."; + try + { + StateField.StringValue = "Running tests..."; - await Task.Delay(50); + await Task.Delay(50); - var result = new AutoRun(typeof(TestHelpers).Assembly).Execute(MainClass.Args.Where(a => a != "--headless").ToArray()); + var result = new AutoRun(typeof(TestHelpers).Assembly).Execute(MainClass.Args.Where(a => a != "--headless").ToArray()); - StateField.StringValue = $"Test run complete. Failed: {result}"; + StateField.StringValue = $"Test run complete. Failed: {result}"; - if (TestHelpers.IsHeadlessRun(MainClass.Args)) + if (TestHelpers.IsHeadlessRun(MainClass.Args)) + { + var resultPath = TestHelpers.GetResultsPath(MainClass.Args); + TestHelpers.TransformTestResults(resultPath); + NSApplication.SharedApplication.Terminate(this); + } + } + catch (Exception ex) { - var resultPath = TestHelpers.GetResultsPath(MainClass.Args); - TestHelpers.TransformTestResults(resultPath); + Console.WriteLine($"An error occurred while running the tests: {ex}"); NSApplication.SharedApplication.Terminate(this); } } diff --git a/Tools/DeployApps/BaasClient.cs b/Tools/DeployApps/BaasClient.cs index caaab0f735..493c55b8b5 100644 --- a/Tools/DeployApps/BaasClient.cs +++ b/Tools/DeployApps/BaasClient.cs @@ -21,14 +21,17 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Attributes; +using static Baas.BaasClient.FunctionReturn; namespace Baas { @@ -41,18 +44,46 @@ public static class AppConfigType public const string FlexibleSync = "flx"; } - public class BaasClient + public static class ArgumentHelper { - public class FunctionReturn + public static (Dictionary Extracted, string[] RemainingArgs) ExtractArguments(string[] args, params string[] toExtract) { - [SuppressMessage("StyleCop.Analyzers.DocumentationRules", "SA1602:Enumeration items should be documented", Justification = "The enum is only used internally")] - public enum Result + if (args == null) + { + throw new ArgumentNullException(nameof(args)); + } + + var extracted = new Dictionary(); + var remainingArgs = new List(); + for (var i = 0; i < args.Length; i++) { - success = 0, - failure = 1 + if (!toExtract.Any(name => ExtractArg(i, name))) + { + remainingArgs.Add(args[i]); + } + } + + return (extracted, remainingArgs.ToArray()); + + bool ExtractArg(int index, string name) + { + var arg = args[index]; + if (arg.StartsWith($"--{name}=")) + { + extracted[name] = arg.Replace($"--{name}=", string.Empty); + return true; + } + + return false; } + } + } - public Result status { get; set; } = Result.failure; + public class BaasClient + { + public class FunctionReturn + { + public int Deleted { get; set; } } private const string ConfirmFuncSource = @@ -83,12 +114,12 @@ public enum Result let dbName = '__realm_sync'; if (appId !== '') { - dbName = [dbName, '_', appId].join(''); + dbName += `_${appId}`; } const deletionResult = await mongodb.db(dbName).collection('clientfiles').deleteMany({ ownerId: userId }); console.log('Deleted documents: ' + deletionResult.deletedCount); - return { status: deletionResult.deletedCount > 0 ? 'success' : 'failure' }; + return { Deleted: deletionResult.deletedCount }; } catch(err) { throw 'Deletion failed: ' + err; } @@ -102,6 +133,7 @@ public enum Result private readonly TextWriter _output; private string _groupId; + private string _refreshToken; private string _shortDifferentiator { @@ -176,31 +208,18 @@ public static async Task Atlas(Uri baseUri, string differentiator, T throw new ArgumentNullException(nameof(args)); } - var result = new List(); + var (extracted, remaining) = ArgumentHelper.ExtractArguments(args, "baasurl", "baascluster", "baasapikey", "baasprivateapikey", "baasprojectid", "baasdifferentiator"); - string baasCluster = null; - string baasApiKey = null; - string baasPrivateApiKey = null; - string groupId = null; - string baseUrl = null; - string differentiator = null; - - for (var i = 0; i < args.Length; i++) - { - if (!ExtractArg(i, "baasurl", ref baseUrl) && - !ExtractArg(i, "baascluster", ref baasCluster) && - !ExtractArg(i, "baasapikey", ref baasApiKey) && - !ExtractArg(i, "baasprivateapikey", ref baasPrivateApiKey) && - !ExtractArg(i, "baasprojectid", ref groupId) && - !ExtractArg(i, "baasdifferentiator", ref differentiator)) - { - result.Add(args[i]); - } - } + extracted.TryGetValue("baasurl", out var baseUrl); + extracted.TryGetValue("baascluster", out var baasCluster); + extracted.TryGetValue("baasapikey", out var baasApiKey); + extracted.TryGetValue("baasprivateapikey", out var baasPrivateApiKey); + extracted.TryGetValue("baasprojectid", out var groupId); + extracted.TryGetValue("baasdifferentiator", out var differentiator); if (string.IsNullOrEmpty(baseUrl)) { - return (null, null, result.ToArray()); + return (null, null, remaining); } var baseUri = new Uri(baseUrl); @@ -209,26 +228,15 @@ public static async Task Atlas(Uri baseUri, string differentiator, T ? await Docker(baseUri, differentiator, output) : await Atlas(baseUri, differentiator, output, baasCluster, baasApiKey, baasPrivateApiKey, groupId); - return (client, baseUri, result.ToArray()); - - bool ExtractArg(int index, string name, ref string value) - { - var arg = args[index]; - if (arg.StartsWith($"--{name}=")) - { - value = arg.Replace($"--{name}=", string.Empty); - return true; - } - - return false; - } + return (client, baseUri, remaining); } private async Task Authenticate(string provider, object credentials) { var authDoc = await PostAsync($"auth/providers/{provider}/login", credentials); - _client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"Bearer {authDoc["access_token"].AsString}"); + _refreshToken = authDoc["refresh_token"].AsString; + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authDoc["access_token"].AsString); } public async Task> GetOrCreateApps() @@ -386,7 +394,7 @@ public async Task CreateFlxApp(string name) { _output.WriteLine($"Creating FLX app {name}..."); - var (app, mongoServiceId) = await CreateAppCore(name, new + var (app, _) = await CreateAppCore(name, new { flexible_sync = new { @@ -406,16 +414,10 @@ public async Task CreateFlxApp(string name) write = true, } } - } + }, } }); - var basicAsymmetricObject = Schemas.GenericFlxBaasRule(Differentiator, "BasicAsymmetricObject"); - await PostAsync($"groups/{_groupId}/apps/{app}/services/{mongoServiceId}/rules", basicAsymmetricObject); - - var allTypesAsymmetricObject = Schemas.GenericFlxBaasRule(Differentiator, "AsymmetricObjectWithAllTypes"); - await PostAsync($"groups/{_groupId}/apps/{app}/services/{mongoServiceId}/rules", allTypesAsymmetricObject); - await CreateFunction(app, "triggerClientResetOnSyncServer", TriggerClientResetOnSyncServerFuncSource, runAsSystem: true); return app; @@ -612,6 +614,24 @@ private async Task CreateSchema(BaasApp app, string mongoServiceId, object schem return; } + private async Task RefreshAccessTokenAsync() + { + using var message = new HttpRequestMessage(HttpMethod.Post, new Uri("auth/session", UriKind.Relative)); + message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _refreshToken); + + var response = await _client.SendAsync(message); + if (!response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(); + throw new Exception($"Failed to refresh access token - {response.StatusCode}: {content}"); + } + + var json = await response.Content.ReadAsStringAsync(); + var doc = BsonSerializer.Deserialize(json); + + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", doc["access_token"].AsString); + } + private Task PostAsync(string relativePath, object obj) => SendAsync(HttpMethod.Post, relativePath, obj); private Task GetAsync(string relativePath) => SendAsync(HttpMethod.Get, relativePath); @@ -631,8 +651,14 @@ private async Task SendAsync(HttpMethod method, string relativePath, objec var response = await _client.SendAsync(message); if (!response.IsSuccessStatusCode) { + if (response.StatusCode == HttpStatusCode.Unauthorized && _refreshToken != null) + { + await RefreshAccessTokenAsync(); + return await SendAsync(method, relativePath, payload); + } + var content = await response.Content.ReadAsStringAsync(); - throw new Exception($"An error occurred while executing {method} {relativePath}: {content}"); + throw new Exception($"An error ({response.StatusCode}) occurred while executing {method} {relativePath}: {content}"); } response.EnsureSuccessStatusCode(); diff --git a/wrappers/src/shared_realm_cs.cpp b/wrappers/src/shared_realm_cs.cpp index a5aff8b761..1f638e2906 100644 --- a/wrappers/src/shared_realm_cs.cpp +++ b/wrappers/src/shared_realm_cs.cpp @@ -51,7 +51,7 @@ using HandleTaskCompletionCallbackT = void(void* tcs_ptr, bool invoke_async, Nat using SharedSyncSession = std::shared_ptr; using ErrorCallbackT = void(SharedSyncSession* session, int32_t error_code, realm_value_t message, std::pair* user_info_pairs, size_t user_info_pairs_len, bool is_client_reset, void* managed_sync_config); using ShouldCompactCallbackT = void*(void* managed_delegate, uint64_t total_size, uint64_t data_size, bool* should_compact); -using DataInitializationCallbackT = void*(void* managed_delegate, realm::SharedRealm* realm); +using DataInitializationCallbackT = void*(void* managed_delegate, realm::SharedRealm& realm); using SharedAsyncOpenTask = std::shared_ptr; using SharedSyncSession = std::shared_ptr; @@ -157,7 +157,7 @@ Realm::Config get_shared_realm_config(Configuration configuration, SyncConfigura if (configuration.invoke_initial_data_callback) { config.initialization_function = [configuration_handle](SharedRealm realm) { - auto error = s_initialize_data(configuration_handle->handle(), &realm); + auto error = s_initialize_data(configuration_handle->handle(), realm); if (error) { throw ManagedExceptionDuringCallback("Exception occurred in a Realm.InitialDataCallback callback.", error); } @@ -285,7 +285,7 @@ REALM_EXPORT SharedRealm* shared_realm_open(Configuration configuration, SchemaO if (configuration.invoke_initial_data_callback) { config.initialization_function = [configuration_handle](SharedRealm realm) { - auto error = s_initialize_data(configuration_handle->handle(), &realm); + auto error = s_initialize_data(configuration_handle->handle(), realm); if (error) { throw ManagedExceptionDuringCallback("Exception occurred in a Realm.PopulateInitialDatacallback.", error); } diff --git a/wrappers/src/subscription_set_cs.cpp b/wrappers/src/subscription_set_cs.cpp index 4e7e62e72a..595d9d5e8e 100644 --- a/wrappers/src/subscription_set_cs.cpp +++ b/wrappers/src/subscription_set_cs.cpp @@ -332,6 +332,7 @@ REALM_EXPORT void realm_subscriptionset_wait_for_state(SharedSubscriptionSet& su subs->get_state_change_notification(SubscriptionSet::State::Complete) .get_async([task_completion_source, weak_subs=WeakSubscriptionSet(subs)](StatusWith status) mutable noexcept { try { + // Here -1 being sent to the wait callback indicates the wait was cancelled. if (auto subs = weak_subs.lock()) { subs->refresh(); if (status.is_ok()) {