* fix(runtime): close cancel-dispose race * fix(runtime): recover disposed active checkpoints * fix(runtime): guard disposed checkpoint recovery * test(runtime): share suspended instance fixture * fix(runtime): serialize cycle CTS lifecycle * fix(runtime): serialize cycle CTS cleanup * test(runtime): update lifecycle comment * fix(runtime): avoid CTS callback disposal deadlock * fix(runtime): preserve cancellation recovery races * fix(runtime): preserve cancel state after callback errors * test(runtime): clean up blocked drain callbacks * fix(runtime): guard linked token cancellation cleanup * fix(runtime): preserve fatal cancellation callback failures * fix(common): classify fatal exceptions inside aggregates * fix(runtime): await deferred recovery after drain cancellation * fix(runtime): clarify deferred recovery timeout * test(runtime): await cancellation cleanup before disposal assertion
80 lines
3 KiB
C#
80 lines
3 KiB
C#
using Elsa.Common;
|
|
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
using Xunit;
|
|
|
|
namespace Elsa.Common.UnitTests;
|
|
|
|
public class ExceptionExtensionsTests
|
|
{
|
|
[Fact(DisplayName = "Null is not fatal")]
|
|
public void NullNotFatal() => Assert.False(((Exception?)null).IsFatal());
|
|
|
|
[Theory(DisplayName = "Process-fatal exceptions are classified fatal")]
|
|
[InlineData(typeof(StackOverflowException))]
|
|
[InlineData(typeof(AccessViolationException))]
|
|
[InlineData(typeof(SEHException))]
|
|
[InlineData(typeof(ThreadAbortException))]
|
|
public void FatalExceptions(Type exceptionType)
|
|
{
|
|
#pragma warning disable SYSLIB0050 // FormatterServices is needed to instantiate fatal exception types without throwing them.
|
|
var ex = (Exception)System.Runtime.Serialization.FormatterServices.GetUninitializedObject(exceptionType);
|
|
#pragma warning restore SYSLIB0050
|
|
Assert.True(ex.IsFatal());
|
|
}
|
|
|
|
[Fact(DisplayName = "OutOfMemoryException is fatal")]
|
|
public void OomFatal() => Assert.True(new OutOfMemoryException().IsFatal());
|
|
|
|
[Fact(DisplayName = "InsufficientMemoryException (recoverable subclass of OOM) is NOT fatal")]
|
|
public void InsufficientMemoryNotFatal() => Assert.False(new InsufficientMemoryException().IsFatal());
|
|
|
|
[Theory(DisplayName = "Common recoverable exceptions are NOT fatal")]
|
|
[InlineData(typeof(InvalidOperationException))]
|
|
[InlineData(typeof(ArgumentException))]
|
|
[InlineData(typeof(TimeoutException))]
|
|
[InlineData(typeof(NullReferenceException))]
|
|
[InlineData(typeof(IOException))]
|
|
public void RecoverableExceptions(Type exceptionType)
|
|
{
|
|
var ex = (Exception)Activator.CreateInstance(exceptionType)!;
|
|
Assert.False(ex.IsFatal());
|
|
}
|
|
|
|
[Fact(DisplayName = "TypeInitializationException wrapping a fatal cause is itself fatal")]
|
|
public void WrappedFatalIsFatal()
|
|
{
|
|
var inner = new StackOverflowException();
|
|
var outer = new TypeInitializationException("X", inner);
|
|
Assert.True(outer.IsFatal());
|
|
}
|
|
|
|
[Fact(DisplayName = "AggregateException containing a fatal cause is fatal")]
|
|
public void AggregateWrappingFatalIsFatal()
|
|
{
|
|
var outer = new AggregateException(
|
|
new InvalidOperationException("recoverable"),
|
|
new AggregateException(new OutOfMemoryException("fatal")));
|
|
|
|
Assert.True(outer.IsFatal());
|
|
}
|
|
|
|
[Fact(DisplayName = "AggregateException containing only recoverable causes is not fatal")]
|
|
public void AggregateWrappingRecoverableIsNotFatal()
|
|
{
|
|
var outer = new AggregateException(
|
|
new InvalidOperationException("recoverable"),
|
|
new TimeoutException("recoverable"));
|
|
|
|
Assert.False(outer.IsFatal());
|
|
}
|
|
|
|
[Fact(DisplayName = "TargetInvocationException wrapping a recoverable cause is NOT fatal")]
|
|
public void WrappedRecoverableIsNotFatal()
|
|
{
|
|
var inner = new InvalidOperationException("boom");
|
|
var outer = new TargetInvocationException(inner);
|
|
Assert.False(outer.IsFatal());
|
|
}
|
|
}
|