|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|