Skip to content

Commit

Permalink
Add RetryFact to deal with twitchy HRESULT test
Browse files Browse the repository at this point in the history
  • Loading branch information
JeremyKuhne committed Feb 4, 2024
1 parent a6f5533 commit 4d9d689
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 5 deletions.
3 changes: 2 additions & 1 deletion src/thirtytwo/Window.cs
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,8 @@ public string Text

/// <remarks>
/// <para>
/// Note that
/// Note that the <see cref="Handle"/> may be <see cref="HWND.Null"/> when this method is called. When the
/// underlying <see cref="HWND"/> is destroyed, the handle is no longer valid and will be set to null.
/// </para>
/// </remarks>
/// <inheritdoc/>
Expand Down
15 changes: 11 additions & 4 deletions src/thirtytwo_tests/Support/ErrorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,24 @@ namespace Windows.Support;
[Collection(nameof(ErrorTestCollection))]
public class ErrorTests
{
[RetryFact(MaxRetries = 5)]
public void Error_FormatMessage_RuntimeError()
{
// The Marshal.GetExceptionForHR method is not thread safe for CLR (COR_E*) HRESULTs.
// We don't control all threads in the process, so we have to retry a few times.

// .NET exception messages aren't localized. (Only .NET Framework)
string message = Error.FormatMessage(HRESULT.COR_E_OBJECTDISPOSED);
message.Should().Be("Cannot access a disposed object.");
}

[Fact]
public void Error_FormatMesage()
{
// Check an HRESULT with a product string that hopefully isn't localized.
string message = Error.FormatMessage(HRESULT.FVE_E_LOCKED_VOLUME);
message.Should().Contain("BitLocker");

// .NET exception messages aren't localized. (Only .NET Framework)
message = Error.FormatMessage(HRESULT.COR_E_OBJECTDISPOSED);
message.Should().Be("Cannot access a disposed object.");

message = Error.FormatMessage((uint)WIN32_ERROR.ERROR_ACCESS_DENIED);

string asHResult = Error.FormatMessage(WIN32_ERROR.ERROR_ACCESS_DENIED.ToHRESULT());
Expand Down
44 changes: 44 additions & 0 deletions src/thirtytwo_tests/Xunit/DelayedMessageBus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Taken from Xuint samples. https://github.com/xunit/samples.xunit/tree/main/RetryFactExample

using Xunit.Abstractions;
using Xunit.Sdk;

namespace Xunit;

/// <summary>
/// Used to capture messages to potentially be forwarded later. Messages are forwarded by
/// disposing of the message bus.
/// </summary>
public class DelayedMessageBus : IMessageBus
{
private readonly IMessageBus _innerBus;
private readonly List<IMessageSinkMessage> _messages = [];

public DelayedMessageBus(IMessageBus innerBus) => _innerBus = innerBus;

public bool QueueMessage(IMessageSinkMessage message)
{
// Technically speaking, this lock isn't necessary in our case, because we know we're using this
// message bus for a single test (so there's no possibility of parallelism). However, it's good
// practice when something might be used where parallel messages might arrive, so it's here in
// this sample.
lock (_messages)
{
_messages.Add(message);
}

// No way to ask the inner bus if they want to cancel without sending them the message, so
// we just go ahead and continue always.
return true;
}

public void Dispose()
{
foreach (var message in _messages)
{
_innerBus.QueueMessage(message);
}

GC.SuppressFinalize(this);
}
}
8 changes: 8 additions & 0 deletions src/thirtytwo_tests/Xunit/RetryFactAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Taken from Xuint samples. https://github.com/xunit/samples.xunit/tree/main/RetryFactExample

namespace Xunit;

public class RetryFactAttribute : FactAttribute
{
public int MaxRetries { get; set; } = 3;
}
32 changes: 32 additions & 0 deletions src/thirtytwo_tests/Xunit/RetryFactDiscoverer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Taken from Xuint samples. https://github.com/xunit/samples.xunit/tree/main/RetryFactExample

using Xunit.Abstractions;
using Xunit.Sdk;

namespace Xunit;

public class RetryFactDiscoverer : IXunitTestCaseDiscoverer
{
private readonly IMessageSink _diagnosticMessageSink;

public RetryFactDiscoverer(IMessageSink diagnosticMessageSink) => _diagnosticMessageSink = diagnosticMessageSink;

public IEnumerable<IXunitTestCase> Discover(
ITestFrameworkDiscoveryOptions discoveryOptions,
ITestMethod testMethod,
IAttributeInfo factAttribute)
{
var maxRetries = factAttribute.GetNamedArgument<int>("MaxRetries");
if (maxRetries < 1)
{
maxRetries = 3;
}

yield return new RetryTestCase(
_diagnosticMessageSink,
discoveryOptions.MethodDisplayOrDefault(),
discoveryOptions.MethodDisplayOptionsOrDefault(),
testMethod,
maxRetries);
}
}
70 changes: 70 additions & 0 deletions src/thirtytwo_tests/Xunit/RetryTestCase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Taken from Xuint samples. https://github.com/xunit/samples.xunit/tree/main/RetryFactExample

using System.ComponentModel;
using Xunit.Abstractions;
using Xunit.Sdk;

namespace Xunit;

[Serializable]
public class RetryTestCase : XunitTestCase
{
private int _maxRetries;

[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")]
public RetryTestCase() { }

public RetryTestCase(
IMessageSink diagnosticMessageSink,
TestMethodDisplay testMethodDisplay,
TestMethodDisplayOptions defaultMethodDisplayOptions,
ITestMethod testMethod,
int maxRetries)
: base(diagnosticMessageSink, testMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments: null)
{
_maxRetries = maxRetries;
}

// This method is called by the xUnit test framework classes to run the test case. We will do the
// loop here, forwarding on to the implementation in XunitTestCase to do the heavy lifting. We will
// continue to re-run the test until the aggregator has an error (meaning that some internal error
// condition happened), or the test runs without failure, or we've hit the maximum number of tries.
public override async Task<RunSummary> RunAsync(
IMessageSink diagnosticMessageSink,
IMessageBus messageBus,
object[] constructorArguments,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
{
var runCount = 0;

while (true)
{
// This is really the only tricky bit: we need to capture and delay messages (since those will
// contain run status) until we know we've decided to accept the final result;
var delayedMessageBus = new DelayedMessageBus(messageBus);

var summary = await base.RunAsync(diagnosticMessageSink, delayedMessageBus, constructorArguments, aggregator, cancellationTokenSource);
if (aggregator.HasExceptions || summary.Failed == 0 || ++runCount >= _maxRetries)
{
delayedMessageBus.Dispose(); // Sends all the delayed messages
return summary;
}

diagnosticMessageSink.OnMessage(new DiagnosticMessage($"Execution of '{DisplayName}' failed (attempt #{runCount}), retrying..."));
}
}

public override void Serialize(IXunitSerializationInfo data)
{
base.Serialize(data);
data.AddValue("MaxRetries", _maxRetries);
}

public override void Deserialize(IXunitSerializationInfo data)
{
base.Deserialize(data);
_maxRetries = data.GetValue<int>("MaxRetries");
}
}

0 comments on commit 4d9d689

Please sign in to comment.