You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
EntityFrameworkCore.Jet/test/Shared/TestUtilities/Xunit/JetXunitTestRunner.cs

225 lines
7.7 KiB
C#

using System;
using System.Collections.Generic;
using System.Data.Odbc;
using System.Data.OleDb;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace EntityFrameworkCore.Jet.FunctionalTests.TestUtilities.Xunit;
public class JetXunitTestRunner(
ITest test,
IMessageBus messageBus,
Type testClass,
object[] constructorArguments,
MethodInfo testMethod,
object[] testMethodArguments,
string skipReason,
IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
: XunitTestRunner(test,
messageBus,
testClass,
constructorArguments,
testMethod,
testMethodArguments,
skipReason,
beforeAfterAttributes,
aggregator,
cancellationTokenSource)
{
private const int ResourceExceededMaxRetries = 3;
private const int ResourceExceededDelayMilliseconds = 5000;
public new async Task<RunSummary> RunAsync()
{
var runSummary = new RunSummary { Total = 1 };
var output = string.Empty;
if (!MessageBus.QueueMessage(new TestStarting(Test)))
{
CancellationTokenSource.Cancel();
}
else
{
AfterTestStarting();
if (!string.IsNullOrEmpty(SkipReason))
{
++runSummary.Skipped;
if (!MessageBus.QueueMessage(new TestSkipped(Test, SkipReason)))
{
CancellationTokenSource.Cancel();
}
}
else
{
var aggregator = new ExceptionAggregator(Aggregator);
if (!aggregator.HasExceptions)
{
// Retry loop for transient \"system resource exceeded\" errors.
for (int attempt = 1; attempt <= ResourceExceededMaxRetries; attempt++)
{
var attemptAggregator = new ExceptionAggregator(aggregator);
var tuple = await attemptAggregator.RunAsync(() => InvokeTestAsync(attemptAggregator));
var attemptException = attemptAggregator.ToException();
if (attemptException != null && ContainsSystemResourceExceeded(attemptException))
{
if (attempt < ResourceExceededMaxRetries)
{
// Pause then retry.
await Task.Delay(ResourceExceededDelayMilliseconds, CancellationTokenSource.Token);
continue;
}
}
// Either success, non-retryable failure, or final failed retry.
if (tuple != null)
{
runSummary.Time = tuple.Item1;
output = tuple.Item2;
}
// Merge attempt exceptions back into main aggregator.
aggregator.Add(attemptAggregator.ToException());
break;
}
}
TestResultMessage testResultMessage;
var exception = aggregator.ToException();
if (exception == null)
{
testResultMessage = new TestPassed(Test, runSummary.Time, output);
}
#region Customized
/// This is what we are after. Mark failed tests as 'Skipped', if the failure is expected.
else if (SkipFailedTest(exception))
{
testResultMessage = new TestSkipped(Test, exception.Message);
++runSummary.Skipped;
}
#endregion Customized
else
{
testResultMessage = new TestFailed(Test, runSummary.Time, output, exception);
++runSummary.Failed;
}
if (!CancellationTokenSource.IsCancellationRequested &&
!MessageBus.QueueMessage(testResultMessage))
{
CancellationTokenSource.Cancel();
}
}
Aggregator.Clear();
BeforeTestFinished();
if (Aggregator.HasExceptions &&
!MessageBus.QueueMessage(new TestCleanupFailure(Test, Aggregator.ToException())))
{
CancellationTokenSource.Cancel();
}
if (!MessageBus.QueueMessage(new TestFinished(Test, runSummary.Time, output)))
{
CancellationTokenSource.Cancel();
}
}
return runSummary;
}
private static bool ContainsSystemResourceExceeded(Exception exception)
{
const string marker = "system resource exceeded";
var aggregate = exception as AggregateException ?? new AggregateException(exception);
foreach (var inner in aggregate.Flatten().InnerExceptions.SelectMany(e => e.FlattenHierarchy()))
{
if (inner.Message.IndexOf(marker, StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
}
return false;
}
/// <summary>
/// Mark failed tests as 'Skipped', if they failed because they use an expression, that we explicitly marked as
/// supported by Jet.
/// </summary>
protected virtual bool SkipFailedTest(Exception exception)
{
var skip = false;
var unexpectedUnsupportedTranslation = false;
var aggregateException = exception as AggregateException ??
new AggregateException(exception);
foreach (var innerException in aggregateException.Flatten().InnerExceptions.SelectMany(e => e.FlattenHierarchy()))
{
if (innerException is InvalidOperationException or OleDbException or OdbcException)
{
var message = innerException.Message;
if (message.StartsWith("Jet does not support "))
{
var expectedUnsupportedTranslation = message.Contains("APPLY statements") ||
message.Contains("skipping rows");
skip = expectedUnsupportedTranslation;
unexpectedUnsupportedTranslation = !expectedUnsupportedTranslation;
}
else if (message.StartsWith("Unsupported Jet expression"))
{
skip = true;
}
else if (message.StartsWith("No value given for one or more required parameters."))
{
skip = true;
}
else if (message.StartsWith("Syntax error in PARAMETER clause"))
{
skip = true;
}
if (skip)
{
var sb = new StringBuilder();
sb.AppendLine(message.ReplaceLineEndings(" "));
sb.AppendLine("-----");
File.AppendAllText("ExpectedUnsupportedTranslations.txt", sb.ToString());
break;
}
if (unexpectedUnsupportedTranslation)
{
var sb = new StringBuilder();
sb.AppendLine(message.ReplaceLineEndings(" "));
sb.AppendLine("-----");
File.AppendAllText("UnexpectedUnsupportedTranslations.txt", sb.ToString());
}
}
}
return skip;
}
}