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
This commit is contained in:
Adam Hathcock
2025-01-23 17:06:08 +00:00
committed by GitHub
parent cc23c147be
commit f81fc97a91
13 changed files with 285 additions and 37 deletions
@@ -16,6 +16,7 @@ public abstract class ChannelSaver<T>
private const int MAX_CACHE_WRITE_PARALLELISM = 4;
private const int MAX_CACHE_BATCH = 500;
private readonly List<Exception> _lists = new();
private readonly Channel<T> _checkCacheChannel = Channel.CreateBounded<T>(
new BoundedChannelOptions(SEND_CAPACITY)
{
@@ -28,7 +29,7 @@ public abstract class ChannelSaver<T>
_ => 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<T>
.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<IMemoryOwner<T>> SendToServer(IMemoryOwner<T> batch, CancellationToken cancellationToken)
{
@@ -55,10 +78,30 @@ public abstract class ChannelSaver<T>
public abstract Task SendToServer(Batch<T> 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<Exception>();
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<T> item);
@@ -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<PriorityScheduler> logger,
ThreadPriority priority,
int maximumConcurrencyLevel
) : TaskScheduler, IDisposable
{
private readonly CancellationTokenSource _cancellationTokenSource = new();
private readonly BlockingCollection<Task> _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);
}
})
{
@@ -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<BaseItem>, 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<PriorityScheduler>(),
ThreadPriority.Highest,
2
);
private readonly PriorityScheduler _belowNormal = new(
loggerFactory.CreateLogger<PriorityScheduler>(),
ThreadPriority.BelowNormal,
Environment.ProcessorCount * 2
);
private readonly SerializeProcessOptions _options = options ?? new(false, false, false, false);
private readonly ILogger<SerializeProcess> _logger = loggerFactory.CreateLogger<SerializeProcess>();
private readonly ConcurrentDictionary<Id, ObjectReference> _objectReferences = new();
private readonly Pool<List<(Id, Json, Closures)>> _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<BaseItem> 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<BaseItem> 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;
}
}
}
@@ -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
);
}
@@ -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<ISqLiteJsonCacheManagerFactory>(),
serviceProvider.GetRequiredService<IServerObjectManagerFactory>()
serviceProvider.GetRequiredService<IServerObjectManagerFactory>(),
new NullLoggerFactory()
);
var process = factory.CreateDeserializeProcess(new Uri(url), streamId, token, progress, new(skipCacheReceive));
var @base = await process.Deserialize(rootId, default).ConfigureAwait(false);
@@ -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<Dictionary<string, bool>> HasObjects(
IReadOnlyCollection<string> objectIds,
CancellationToken cancellationToken
) => throw new NotImplementedException();
) => Task.FromResult(objectIds.ToDictionary(x => x, _ => false));
public Task UploadObjects(
IReadOnlyList<BaseItem> objects,
@@ -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.<ReadAllConcurrently>b__0(T e)\nat Open.ChannelExtensions.<28d3e838-dde6-44a1-8f5e-d1c739a178d0>Extensions.<>c__DisplayClass92_0`1.<ReadAllConcurrentlyAsync>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)"
}
@@ -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)"
}
@@ -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<string, string>();
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<AggregateException>(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<AggregateException>(async () => await process2.Serialize(testClass, default));
await Verify(ex);
}
}
public class ExceptionServerObjectManager : IServerObjectManager
{
public IAsyncEnumerable<(string, string)> DownloadObjects(
IReadOnlyCollection<string> objectIds,
IProgress<ProgressArgs>? progress,
CancellationToken cancellationToken
) => throw new NotImplementedException();
public Task<string?> DownloadSingleObject(
string objectId,
IProgress<ProgressArgs>? progress,
CancellationToken cancellationToken
) => throw new NotImplementedException();
public Task<Dictionary<string, bool>> HasObjects(
IReadOnlyCollection<string> objectIds,
CancellationToken cancellationToken
) => throw new NotImplementedException();
public Task UploadObjects(
IReadOnlyList<BaseItem> objects,
bool compressPayloads,
IProgress<ProgressArgs>? 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();
}
@@ -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)
);
@@ -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);
@@ -10,6 +10,7 @@
<PackageReference Include="altcover" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit.assert" />
<PackageReference Include="xunit.runner.visualstudio"/>
</ItemGroup>
@@ -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, )",