From f81fc97a91bfef7f718b8e884ced86b1b2eb1620 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 23 Jan 2025 17:06:08 +0000 Subject: [PATCH] Add exception handling for SerializeProcess with CancellationTokenSource (#211) * Add exception handling for SerializeProcess with CancellationTokenSource * formatting * add exception test to make sure we handle a server exception * add extra exception and handling to stop * add comment and another test * one last chance for user to cancel * formatting --- .../Serialization/ChannelSaver.cs | 57 ++++++++-- .../V2/Send/PriorityScheduler.cs | 11 +- .../Serialisation/V2/Send/SerializeProcess.cs | 80 +++++++++---- .../V2/SerializeProcessFactory.cs | 5 +- .../Program.cs | 4 +- .../DetachedTests.cs | 7 +- ...nTests.Test_Exceptions_Cache.verified.json | 10 ++ ...Tests.Test_Exceptions_Upload.verified.json | 30 +++++ .../ExceptionTests.cs | 105 ++++++++++++++++++ .../ExplicitInterfaceTests.cs | 4 +- .../SerializationTests.cs | 2 + .../Speckle.Sdk.Serialization.Tests.csproj | 1 + .../packages.lock.json | 6 + 13 files changed, 285 insertions(+), 37 deletions(-) create mode 100644 tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.Test_Exceptions_Cache.verified.json create mode 100644 tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.Test_Exceptions_Upload.verified.json create mode 100644 tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.cs diff --git a/src/Speckle.Sdk.Dependencies/Serialization/ChannelSaver.cs b/src/Speckle.Sdk.Dependencies/Serialization/ChannelSaver.cs index 99190998..eafa0005 100644 --- a/src/Speckle.Sdk.Dependencies/Serialization/ChannelSaver.cs +++ b/src/Speckle.Sdk.Dependencies/Serialization/ChannelSaver.cs @@ -16,6 +16,7 @@ public abstract class ChannelSaver private const int MAX_CACHE_WRITE_PARALLELISM = 4; private const int MAX_CACHE_BATCH = 500; + private readonly List _lists = new(); private readonly Channel _checkCacheChannel = Channel.CreateBounded( new BoundedChannelOptions(SEND_CAPACITY) { @@ -28,7 +29,7 @@ public abstract class ChannelSaver _ => throw new NotImplementedException("Dropping items not supported.") ); - public Task Start(CancellationToken cancellationToken = default) => + public Task Start(CancellationToken cancellationToken) => _checkCacheChannel .Reader.BatchBySize(HTTP_SEND_CHUNK_SIZE) .WithTimeout(HTTP_BATCH_TIMEOUT) @@ -42,10 +43,32 @@ public abstract class ChannelSaver .Join() .Batch(MAX_CACHE_BATCH) .WithTimeout(HTTP_BATCH_TIMEOUT) - .ReadAllConcurrently(MAX_CACHE_WRITE_PARALLELISM, SaveToCache, cancellationToken); + .ReadAllConcurrently(MAX_CACHE_WRITE_PARALLELISM, SaveToCache, cancellationToken) + .ContinueWith( + t => + { + Exception? ex = t.Exception; + if (ex is null && t.Status is TaskStatus.Canceled && !cancellationToken.IsCancellationRequested) + { + ex = new OperationCanceledException(); + } - public ValueTask Save(T item, CancellationToken cancellationToken = default) => - _checkCacheChannel.Writer.WriteAsync(item, cancellationToken); + if (ex is not null) + { + lock (_lists) + { + _lists.Add(ex); + } + } + _checkCacheChannel.Writer.TryComplete(ex); + }, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Current + ); + + public async ValueTask Save(T item, CancellationToken cancellationToken) => + await _checkCacheChannel.Writer.WriteAsync(item, cancellationToken).ConfigureAwait(true); public async Task> SendToServer(IMemoryOwner batch, CancellationToken cancellationToken) { @@ -55,10 +78,30 @@ public abstract class ChannelSaver public abstract Task SendToServer(Batch batch, CancellationToken cancellationToken); - public Task Done() + public void DoneTraversing() => _checkCacheChannel.Writer.TryComplete(); + + public async Task DoneSaving() { - _checkCacheChannel.Writer.Complete(); - return Task.CompletedTask; + await _checkCacheChannel.Reader.Completion.ConfigureAwait(true); + lock (_lists) + { + if (_lists.Count > 0) + { + var exceptions = new List(); + foreach (var ex in _lists) + { + if (ex is AggregateException ae) + { + exceptions.AddRange(ae.Flatten().InnerExceptions); + } + else + { + exceptions.Add(ex); + } + } + throw new AggregateException(exceptions); + } + } } public abstract void SaveToCache(List item); diff --git a/src/Speckle.Sdk/Serialisation/V2/Send/PriorityScheduler.cs b/src/Speckle.Sdk/Serialisation/V2/Send/PriorityScheduler.cs index 46eacf4a..11917284 100644 --- a/src/Speckle.Sdk/Serialisation/V2/Send/PriorityScheduler.cs +++ b/src/Speckle.Sdk/Serialisation/V2/Send/PriorityScheduler.cs @@ -1,8 +1,13 @@ using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; namespace Speckle.Sdk.Serialisation.V2.Send; -public sealed class PriorityScheduler(ThreadPriority priority, int maximumConcurrencyLevel) : TaskScheduler, IDisposable +public sealed class PriorityScheduler( + ILogger logger, + ThreadPriority priority, + int maximumConcurrencyLevel +) : TaskScheduler, IDisposable { private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly BlockingCollection _tasks = new(); @@ -47,10 +52,10 @@ public sealed class PriorityScheduler(ThreadPriority priority, int maximumConcur } } #pragma warning disable CA1031 - catch (Exception) + catch (Exception e) #pragma warning restore CA1031 { - // ignored + logger.LogError(e, "{name} had an exception", Thread.CurrentThread.Name); } }) { diff --git a/src/Speckle.Sdk/Serialisation/V2/Send/SerializeProcess.cs b/src/Speckle.Sdk/Serialisation/V2/Send/SerializeProcess.cs index 0713d9e1..c6ee2823 100644 --- a/src/Speckle.Sdk/Serialisation/V2/Send/SerializeProcess.cs +++ b/src/Speckle.Sdk/Serialisation/V2/Send/SerializeProcess.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; using Speckle.InterfaceGenerator; using Speckle.Sdk.Common; using Speckle.Sdk.Dependencies; @@ -31,13 +32,23 @@ public sealed class SerializeProcess( IServerObjectManager serverObjectManager, IBaseChildFinder baseChildFinder, IObjectSerializerFactory objectSerializerFactory, + ILoggerFactory loggerFactory, SerializeProcessOptions? options = null ) : ChannelSaver, ISerializeProcess { - private readonly PriorityScheduler _highest = new(ThreadPriority.Highest, 2); - private readonly PriorityScheduler _belowNormal = new(ThreadPriority.BelowNormal, Environment.ProcessorCount * 2); + private readonly PriorityScheduler _highest = new( + loggerFactory.CreateLogger(), + ThreadPriority.Highest, + 2 + ); + private readonly PriorityScheduler _belowNormal = new( + loggerFactory.CreateLogger(), + ThreadPriority.BelowNormal, + Environment.ProcessorCount * 2 + ); private readonly SerializeProcessOptions _options = options ?? new(false, false, false, false); + private readonly ILogger _logger = loggerFactory.CreateLogger(); private readonly ConcurrentDictionary _objectReferences = new(); private readonly Pool> _pool = Pools.CreateListPool<(Id, Json, Closures)>(); @@ -71,16 +82,15 @@ public sealed class SerializeProcess( { findTotalObjectsTask = Task.Factory.StartNew( () => TraverseTotal(root), - default, + cancellationToken, TaskCreationOptions.AttachedToParent | TaskCreationOptions.PreferFairness, _highest ); } - - await Traverse(root, cancellationToken).ConfigureAwait(false); - await Done().ConfigureAwait(true); - await channelTask.ConfigureAwait(false); - await findTotalObjectsTask.ConfigureAwait(false); + await Traverse(root, cancellationToken).ConfigureAwait(true); + DoneTraversing(); + await Task.WhenAll(findTotalObjectsTask, channelTask).ConfigureAwait(true); + await DoneSaving().ConfigureAwait(true); return new(root.id.NotNull(), _objectReferences.Freeze()); } @@ -103,7 +113,10 @@ public sealed class SerializeProcess( var tmp = child; var t = Task .Factory.StartNew( - () => Traverse(tmp, cancellationToken), + async () => + { + return await Traverse(tmp, cancellationToken).ConfigureAwait(true); + }, cancellationToken, TaskCreationOptions.AttachedToParent | TaskCreationOptions.PreferFairness, _belowNormal @@ -204,29 +217,50 @@ public sealed class SerializeProcess( public override async Task SendToServer(Batch batch, CancellationToken cancellationToken) { - if (!_options.SkipServer && batch.Items.Count != 0) + try { - var objectBatch = batch.Items.Distinct().ToList(); - var hasObjects = await serverObjectManager - .HasObjects(objectBatch.Select(x => x.Id.Value).Freeze(), cancellationToken) - .ConfigureAwait(false); - objectBatch = batch.Items.Where(x => !hasObjects[x.Id.Value]).ToList(); - if (objectBatch.Count != 0) + if (!_options.SkipServer && batch.Items.Count != 0) { - await serverObjectManager.UploadObjects(objectBatch, true, progress, cancellationToken).ConfigureAwait(false); - Interlocked.Exchange(ref _uploaded, _uploaded + batch.Items.Count); + var objectBatch = batch.Items.Distinct().ToList(); + var hasObjects = await serverObjectManager + .HasObjects(objectBatch.Select(x => x.Id.Value).Freeze(), cancellationToken) + .ConfigureAwait(false); + objectBatch = batch.Items.Where(x => !hasObjects[x.Id.Value]).ToList(); + if (objectBatch.Count != 0) + { + await serverObjectManager.UploadObjects(objectBatch, true, progress, cancellationToken).ConfigureAwait(false); + Interlocked.Exchange(ref _uploaded, _uploaded + batch.Items.Count); + } + + progress?.Report(new(ProgressEvent.UploadedObjects, _uploaded, null)); } - progress?.Report(new(ProgressEvent.UploadedObjects, _uploaded, null)); + } +#pragma warning disable CA1031 + catch (Exception e) +#pragma warning restore CA1031 + { + _logger.LogError(e, "Error sending objects to server"); + throw; } } public override void SaveToCache(List batch) { - if (!_options.SkipCacheWrite && batch.Count != 0) + try { - sqLiteJsonCacheManager.SaveObjects(batch.Select(x => (x.Id.Value, x.Json.Value))); - Interlocked.Exchange(ref _cached, _cached + batch.Count); - progress?.Report(new(ProgressEvent.CachedToLocal, _cached, _objectsSerialized)); + if (!_options.SkipCacheWrite && batch.Count != 0) + { + sqLiteJsonCacheManager.SaveObjects(batch.Select(x => (x.Id.Value, x.Json.Value))); + Interlocked.Exchange(ref _cached, _cached + batch.Count); + progress?.Report(new(ProgressEvent.CachedToLocal, _cached, _objectsSerialized)); + } + } +#pragma warning disable CA1031 + catch (Exception e) +#pragma warning restore CA1031 + { + _logger.LogError(e, "Error sending objects to server"); + throw; } } } diff --git a/src/Speckle.Sdk/Serialisation/V2/SerializeProcessFactory.cs b/src/Speckle.Sdk/Serialisation/V2/SerializeProcessFactory.cs index 68615ec5..bc7bfc92 100644 --- a/src/Speckle.Sdk/Serialisation/V2/SerializeProcessFactory.cs +++ b/src/Speckle.Sdk/Serialisation/V2/SerializeProcessFactory.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Logging; using Speckle.Sdk.Serialisation.V2.Receive; using Speckle.Sdk.Serialisation.V2.Send; using Speckle.Sdk.SQLite; @@ -28,7 +29,8 @@ public class SerializeProcessFactory( IObjectSerializerFactory objectSerializerFactory, IObjectDeserializerFactory objectDeserializerFactory, ISqLiteJsonCacheManagerFactory sqLiteJsonCacheManagerFactory, - IServerObjectManagerFactory serverObjectManagerFactory + IServerObjectManagerFactory serverObjectManagerFactory, + ILoggerFactory loggerFactory ) : ISerializeProcessFactory { public ISerializeProcess CreateSerializeProcess( @@ -47,6 +49,7 @@ public class SerializeProcessFactory( serverObjectManager, baseChildFinder, objectSerializerFactory, + loggerFactory, options ); } diff --git a/tests/Speckle.Sdk.Serialization.Testing/Program.cs b/tests/Speckle.Sdk.Serialization.Testing/Program.cs index 77e81387..27ec68ca 100644 --- a/tests/Speckle.Sdk.Serialization.Testing/Program.cs +++ b/tests/Speckle.Sdk.Serialization.Testing/Program.cs @@ -1,6 +1,7 @@ #pragma warning disable CA1506 using System.Reflection; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; using Speckle.Sdk; using Speckle.Sdk.Credentials; using Speckle.Sdk.Host; @@ -45,7 +46,8 @@ var factory = new SerializeProcessFactory( new ObjectSerializerFactory(new BasePropertyGatherer()), new ObjectDeserializerFactory(), serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService() + serviceProvider.GetRequiredService(), + new NullLoggerFactory() ); var process = factory.CreateDeserializeProcess(new Uri(url), streamId, token, progress, new(skipCacheReceive)); var @base = await process.Deserialize(rootId, default).ConfigureAwait(false); diff --git a/tests/Speckle.Sdk.Serialization.Tests/DetachedTests.cs b/tests/Speckle.Sdk.Serialization.Tests/DetachedTests.cs index 2a851a02..a33ac4bd 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/DetachedTests.cs +++ b/tests/Speckle.Sdk.Serialization.Tests/DetachedTests.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Text; using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; using Speckle.Newtonsoft.Json.Linq; using Speckle.Objects.Geometry; using Speckle.Sdk.Host; @@ -37,6 +38,7 @@ public class DetachedTests new DummyServerObjectManager(), new BaseChildFinder(new BasePropertyGatherer()), new ObjectSerializerFactory(new BasePropertyGatherer()), + new NullLoggerFactory(), new SerializeProcessOptions(false, false, true, true) ); await process2.Serialize(@base, default); @@ -120,6 +122,7 @@ public class DetachedTests new DummyServerObjectManager(), new BaseChildFinder(new BasePropertyGatherer()), new ObjectSerializerFactory(new BasePropertyGatherer()), + new NullLoggerFactory(), new SerializeProcessOptions(false, false, true, true) ); var results = await process2.Serialize(@base, default); @@ -188,6 +191,7 @@ public class DetachedTests new DummyServerObjectManager(), new BaseChildFinder(new BasePropertyGatherer()), new ObjectSerializerFactory(new BasePropertyGatherer()), + new NullLoggerFactory(), new SerializeProcessOptions(false, false, true, true) ); var results = await process2.Serialize(@base, default); @@ -221,6 +225,7 @@ public class DetachedTests new DummyServerObjectManager(), new BaseChildFinder(new BasePropertyGatherer()), new ObjectSerializerFactory(new BasePropertyGatherer()), + new NullLoggerFactory(), new SerializeProcessOptions(false, false, true, true) ); var results = await process2.Serialize(@base, default); @@ -300,7 +305,7 @@ public class DummyServerObjectManager : IServerObjectManager public Task> HasObjects( IReadOnlyCollection objectIds, CancellationToken cancellationToken - ) => throw new NotImplementedException(); + ) => Task.FromResult(objectIds.ToDictionary(x => x, _ => false)); public Task UploadObjects( IReadOnlyList objects, diff --git a/tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.Test_Exceptions_Cache.verified.json b/tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.Test_Exceptions_Cache.verified.json new file mode 100644 index 00000000..50026526 --- /dev/null +++ b/tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.Test_Exceptions_Cache.verified.json @@ -0,0 +1,10 @@ +{ + "Type": "AggregateException", + "InnerException": { + "Data": {}, + "Message": "The method or operation is not implemented.", + "StackTrace": "at Speckle.Sdk.Serialization.Tests.ExceptionSendCacheManager.SaveObjects(IEnumerable`1 items)\nat Speckle.Sdk.Serialisation.V2.Send.SerializeProcess.SaveToCache(List`1 batch)\nat Open.ChannelExtensions.<28d3e838-dde6-44a1-8f5e-d1c739a178d0>Extensions.<>c__DisplayClass98_0`1.b__0(T e)\nat Open.ChannelExtensions.<28d3e838-dde6-44a1-8f5e-d1c739a178d0>Extensions.<>c__DisplayClass92_0`1.b__2(T item, Int64 _)\nat Open.ChannelExtensions.<28d3e838-dde6-44a1-8f5e-d1c739a178d0>Extensions.ReadUntilCancelledAsync[T](<5e816acc-9cf8-4b52-a8da-ceb5bd7eecc7>ChannelReader`1 reader, CancellationToken cancellationToken, Func`3 receiver, Boolean deferredExecution)\n--- End of stack trace from previous location ---", + "Type": "NotImplementedException" + }, + "StackTrace": "at Speckle.Sdk.Dependencies.Serialization.ChannelSaver`1.DoneSaving()\nat Speckle.Sdk.Serialisation.V2.Send.SerializeProcess.Serialize(Base root, CancellationToken cancellationToken)\n--- End of stack trace from previous location ---\nat Xunit.Assert.RecordExceptionAsync(Func`1 testCode)" +} diff --git a/tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.Test_Exceptions_Upload.verified.json b/tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.Test_Exceptions_Upload.verified.json new file mode 100644 index 00000000..cd87f84a --- /dev/null +++ b/tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.Test_Exceptions_Upload.verified.json @@ -0,0 +1,30 @@ +{ + "Type": "AggregateException", + "InnerExceptions": [ + { + "Data": {}, + "Message": "The method or operation is not implemented.", + "StackTrace": "at Speckle.Sdk.Serialization.Tests.ExceptionServerObjectManager.HasObjects(IReadOnlyCollection`1 objectIds, CancellationToken cancellationToken)\nat Speckle.Sdk.Serialisation.V2.Send.SerializeProcess.SendToServer(Batch`1 batch, CancellationToken cancellationToken)\nat Speckle.Sdk.Dependencies.Serialization.ChannelSaver`1.SendToServer(IMemoryOwner`1 batch, CancellationToken cancellationToken)\n--- End of stack trace from previous location ---\n--- End of stack trace from previous location ---\nat Open.ChannelExtensions.<28d3e838-dde6-44a1-8f5e-d1c739a178d0>Extensions.ReadUntilCancelledAsync[T](<5e816acc-9cf8-4b52-a8da-ceb5bd7eecc7>ChannelReader`1 reader, CancellationToken cancellationToken, Func`3 receiver, Boolean deferredExecution)\n--- End of stack trace from previous location ---", + "Type": "NotImplementedException" + }, + { + "Data": {}, + "Message": "The method or operation is not implemented.", + "StackTrace": "at Speckle.Sdk.Serialization.Tests.ExceptionServerObjectManager.HasObjects(IReadOnlyCollection`1 objectIds, CancellationToken cancellationToken)\nat Speckle.Sdk.Serialisation.V2.Send.SerializeProcess.SendToServer(Batch`1 batch, CancellationToken cancellationToken)\nat Speckle.Sdk.Dependencies.Serialization.ChannelSaver`1.SendToServer(IMemoryOwner`1 batch, CancellationToken cancellationToken)\n--- End of stack trace from previous location ---\n--- End of stack trace from previous location ---\nat Open.ChannelExtensions.<28d3e838-dde6-44a1-8f5e-d1c739a178d0>Extensions.ReadUntilCancelledAsync[T](<5e816acc-9cf8-4b52-a8da-ceb5bd7eecc7>ChannelReader`1 reader, CancellationToken cancellationToken, Func`3 receiver, Boolean deferredExecution)\n--- End of stack trace from previous location ---", + "Type": "NotImplementedException" + }, + { + "Data": {}, + "Message": "The method or operation is not implemented.", + "StackTrace": "at Speckle.Sdk.Serialization.Tests.ExceptionServerObjectManager.HasObjects(IReadOnlyCollection`1 objectIds, CancellationToken cancellationToken)\nat Speckle.Sdk.Serialisation.V2.Send.SerializeProcess.SendToServer(Batch`1 batch, CancellationToken cancellationToken)\nat Speckle.Sdk.Dependencies.Serialization.ChannelSaver`1.SendToServer(IMemoryOwner`1 batch, CancellationToken cancellationToken)\n--- End of stack trace from previous location ---\n--- End of stack trace from previous location ---\nat Open.ChannelExtensions.<28d3e838-dde6-44a1-8f5e-d1c739a178d0>Extensions.ReadUntilCancelledAsync[T](<5e816acc-9cf8-4b52-a8da-ceb5bd7eecc7>ChannelReader`1 reader, CancellationToken cancellationToken, Func`3 receiver, Boolean deferredExecution)\n--- End of stack trace from previous location ---", + "Type": "NotImplementedException" + }, + { + "Data": {}, + "Message": "The method or operation is not implemented.", + "StackTrace": "at Speckle.Sdk.Serialization.Tests.ExceptionServerObjectManager.HasObjects(IReadOnlyCollection`1 objectIds, CancellationToken cancellationToken)\nat Speckle.Sdk.Serialisation.V2.Send.SerializeProcess.SendToServer(Batch`1 batch, CancellationToken cancellationToken)\nat Speckle.Sdk.Dependencies.Serialization.ChannelSaver`1.SendToServer(IMemoryOwner`1 batch, CancellationToken cancellationToken)\n--- End of stack trace from previous location ---\n--- End of stack trace from previous location ---\nat Open.ChannelExtensions.<28d3e838-dde6-44a1-8f5e-d1c739a178d0>Extensions.ReadUntilCancelledAsync[T](<5e816acc-9cf8-4b52-a8da-ceb5bd7eecc7>ChannelReader`1 reader, CancellationToken cancellationToken, Func`3 receiver, Boolean deferredExecution)\n--- End of stack trace from previous location ---", + "Type": "NotImplementedException" + } + ], + "StackTrace": "at Speckle.Sdk.Dependencies.Serialization.ChannelSaver`1.DoneSaving()\nat Speckle.Sdk.Serialisation.V2.Send.SerializeProcess.Serialize(Base root, CancellationToken cancellationToken)\n--- End of stack trace from previous location ---\nat Xunit.Assert.RecordExceptionAsync(Func`1 testCode)" +} diff --git a/tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.cs b/tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.cs new file mode 100644 index 00000000..256030d1 --- /dev/null +++ b/tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.cs @@ -0,0 +1,105 @@ +using Microsoft.Extensions.Logging.Abstractions; +using Speckle.Objects.Geometry; +using Speckle.Sdk.Host; +using Speckle.Sdk.Models; +using Speckle.Sdk.Serialisation.V2; +using Speckle.Sdk.Serialisation.V2.Send; +using Speckle.Sdk.SQLite; +using Speckle.Sdk.Transports; + +namespace Speckle.Sdk.Serialization.Tests; + +public class ExceptionTests +{ + public ExceptionTests() + { + TypeLoader.Reset(); + TypeLoader.Initialize(typeof(Base).Assembly, typeof(DetachedTests).Assembly, typeof(Polyline).Assembly); + } + + [Fact] + public async Task Test_Exceptions_Upload() + { + var testClass = new TestClass() { RegularProperty = "Hello" }; + + var objects = new Dictionary(); + using var process2 = new SerializeProcess( + null, + new DummySendCacheManager(objects), + new ExceptionServerObjectManager(), + new BaseChildFinder(new BasePropertyGatherer()), + new ObjectSerializerFactory(new BasePropertyGatherer()), + new NullLoggerFactory(), + new SerializeProcessOptions(false, false, false, true) + ); + + //4 exceptions are fine because we use 4 threads for saving cache + var ex = await Assert.ThrowsAsync(async () => await process2.Serialize(testClass, default)); + await Verify(ex); + } + + [Fact] + public async Task Test_Exceptions_Cache() + { + var testClass = new TestClass() { RegularProperty = "Hello" }; + + using var process2 = new SerializeProcess( + null, + new ExceptionSendCacheManager(), + new DummyServerObjectManager(), + new BaseChildFinder(new BasePropertyGatherer()), + new ObjectSerializerFactory(new BasePropertyGatherer()), + new NullLoggerFactory(), + new SerializeProcessOptions(false, false, false, true) + ); + + var ex = await Assert.ThrowsAsync(async () => await process2.Serialize(testClass, default)); + await Verify(ex); + } +} + +public class ExceptionServerObjectManager : IServerObjectManager +{ + public IAsyncEnumerable<(string, string)> DownloadObjects( + IReadOnlyCollection objectIds, + IProgress? progress, + CancellationToken cancellationToken + ) => throw new NotImplementedException(); + + public Task DownloadSingleObject( + string objectId, + IProgress? progress, + CancellationToken cancellationToken + ) => throw new NotImplementedException(); + + public Task> HasObjects( + IReadOnlyCollection objectIds, + CancellationToken cancellationToken + ) => throw new NotImplementedException(); + + public Task UploadObjects( + IReadOnlyList objects, + bool compressPayloads, + IProgress? progress, + CancellationToken cancellationToken + ) => throw new NotImplementedException(); +} + +public class ExceptionSendCacheManager : ISqLiteJsonCacheManager +{ + public void Dispose() { } + + public IReadOnlyCollection<(string Id, string Json)> GetAllObjects() => throw new NotImplementedException(); + + public void DeleteObject(string id) => throw new NotImplementedException(); + + public string? GetObject(string id) => null; + + public void SaveObject(string id, string json) => throw new NotImplementedException(); + + public void UpdateObject(string id, string json) => throw new NotImplementedException(); + + public void SaveObjects(IEnumerable<(string id, string json)> items) => throw new NotImplementedException(); + + public bool HasObject(string objectId) => throw new NotImplementedException(); +} diff --git a/tests/Speckle.Sdk.Serialization.Tests/ExplicitInterfaceTests.cs b/tests/Speckle.Sdk.Serialization.Tests/ExplicitInterfaceTests.cs index 6c037413..446e9388 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/ExplicitInterfaceTests.cs +++ b/tests/Speckle.Sdk.Serialization.Tests/ExplicitInterfaceTests.cs @@ -1,4 +1,5 @@ -using Speckle.Sdk.Host; +using Microsoft.Extensions.Logging.Abstractions; +using Speckle.Sdk.Host; using Speckle.Sdk.Models; using Speckle.Sdk.Serialisation.V2.Send; @@ -24,6 +25,7 @@ public class ExplicitInterfaceTests new DummyServerObjectManager(), new BaseChildFinder(new BasePropertyGatherer()), new ObjectSerializerFactory(new BasePropertyGatherer()), + new NullLoggerFactory(), new SerializeProcessOptions(false, false, true, true) ); diff --git a/tests/Speckle.Sdk.Serialization.Tests/SerializationTests.cs b/tests/Speckle.Sdk.Serialization.Tests/SerializationTests.cs index 58c087db..d342d8cb 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/SerializationTests.cs +++ b/tests/Speckle.Sdk.Serialization.Tests/SerializationTests.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.IO.Compression; using System.Reflection; using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; using Speckle.Newtonsoft.Json; using Speckle.Newtonsoft.Json.Linq; using Speckle.Objects.Data; @@ -265,6 +266,7 @@ public class SerializationTests new DummySendServerObjectManager(newIdToJson), new BaseChildFinder(new BasePropertyGatherer()), new ObjectSerializerFactory(new BasePropertyGatherer()), + new NullLoggerFactory(), new SerializeProcessOptions(true, true, false, true) ); var (rootId2, _) = await serializeProcess.Serialize(root, default); diff --git a/tests/Speckle.Sdk.Serialization.Tests/Speckle.Sdk.Serialization.Tests.csproj b/tests/Speckle.Sdk.Serialization.Tests/Speckle.Sdk.Serialization.Tests.csproj index a95ca09a..b53ec44b 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/Speckle.Sdk.Serialization.Tests.csproj +++ b/tests/Speckle.Sdk.Serialization.Tests/Speckle.Sdk.Serialization.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/tests/Speckle.Sdk.Serialization.Tests/packages.lock.json b/tests/Speckle.Sdk.Serialization.Tests/packages.lock.json index 5ad46e73..6592052d 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/packages.lock.json +++ b/tests/Speckle.Sdk.Serialization.Tests/packages.lock.json @@ -55,6 +55,12 @@ "resolved": "0.9.6", "contentHash": "HKH7tYrYYlCK1ct483hgxERAdVdMtl7gUKW9ijWXxA1UsYR4Z+TrRHYmzZ9qmpu1NnTycSrp005NYM78GDKV1w==" }, + "xunit.assert": { + "type": "Direct", + "requested": "[2.9.3, )", + "resolved": "2.9.3", + "contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA==" + }, "xunit.runner.visualstudio": { "type": "Direct", "requested": "[3.0.1, )",