Skip to content

Commit

Permalink
Moved to wait helper classes with locking and better protection for r…
Browse files Browse the repository at this point in the history
…ace conditions
  • Loading branch information
egil committed May 7, 2020
1 parent 98bfb08 commit 46176c3
Show file tree
Hide file tree
Showing 16 changed files with 712 additions and 644 deletions.
3 changes: 2 additions & 1 deletion src/bunit.core/Extensions/TimeSpanExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ public static class TimeSpanExtensions
/// </summary>
public static TimeSpan GetRuntimeTimeout(this TimeSpan? timeout)
{
return Debugger.IsAttached ? Timeout.InfiniteTimeSpan : timeout ?? TimeSpan.FromSeconds(1);
//return Debugger.IsAttached ? Timeout.InfiniteTimeSpan : timeout ?? TimeSpan.FromSeconds(1);
return timeout ?? TimeSpan.FromSeconds(1);
}

/// <summary>
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

namespace Bunit
{
/// <summary>
/// Represents an exception thrown when the awaited assertion does not pass.
/// </summary>
public class WaitForAssertionFailedException : TimeoutException
{
internal const string MESSAGE = "The assertion did not pass within the timeout period.";
internal WaitForAssertionFailedException(Exception assertionException) : base(MESSAGE, assertionException)
{
}
}
/// <summary>
/// Represents an exception thrown when the awaited assertion does not pass.
/// </summary>
public class WaitForAssertionFailedException : TimeoutException
{
internal const string MESSAGE = "The assertion did not pass within the timeout period.";

internal WaitForAssertionFailedException(Exception? assertionException) : base(MESSAGE, assertionException)
{
}
}
}
100 changes: 100 additions & 0 deletions src/bunit.core/Extensions/WaitForExtensions/WaitForAssertionHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.DependencyInjection;

namespace Bunit
{

public class WaitForAssertionHelper : IDisposable
{
private readonly IRenderedFragmentBase _renderedFragment;
private readonly Action _assertion;
private readonly Timer _timer;
private readonly ILogger _logger;
private readonly TaskCompletionSource<object?> _completionSouce;
private bool _disposed = false;
private Exception? _capturedException;

public Task WaitTask => _completionSouce.Task;

public WaitForAssertionHelper(IRenderedFragmentBase renderedFragment, Action assertion, TimeSpan? timeout = null)
{
_logger = GetLogger<WaitForAssertionHelper>(renderedFragment.Services);
_completionSouce = new TaskCompletionSource<object?>();
_renderedFragment = renderedFragment;
_assertion = assertion;
_timer = new Timer(HandleTimeout, this, timeout.GetRuntimeTimeout(), TimeSpan.FromMilliseconds(Timeout.Infinite));
_renderedFragment.OnAfterRender += TryAssertion;
TryAssertion();
}

void TryAssertion()
{
if (_disposed)
return;
lock (_completionSouce)
{
if (_disposed)
return;
_logger.LogDebug(new EventId(1, nameof(TryAssertion)), $"Trying the assertion for component {_renderedFragment.ComponentId}");

try
{
_assertion();
_capturedException = null;
_completionSouce.TrySetResult(null);
_logger.LogDebug(new EventId(2, nameof(TryAssertion)), $"The assertion for component {_renderedFragment.ComponentId} passed");
Dispose();
}
catch (Exception ex)
{
_logger.LogDebug(new EventId(3, nameof(TryAssertion)), $"The assertion for component {_renderedFragment.ComponentId} did not pass. The error message was '{ex.Message}'");
_capturedException = ex;
}
}
}

void HandleTimeout(object state)
{
if (_disposed)
return;

lock (_completionSouce)
{
if (_disposed)
return;

_logger.LogDebug(new EventId(5, nameof(HandleTimeout)), $"The assertion wait helper for component {_renderedFragment.ComponentId} timed out");

var error = new WaitForAssertionFailedException(_capturedException);
_completionSouce.TrySetException(error);

Dispose();
}
}

/// <summary>
/// Disposes the wait helper and sets the <see cref="WaitTask"/> to canceled, if it is not
/// already in one of the other completed states.
/// </summary>
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_renderedFragment.OnAfterRender -= TryAssertion;
_timer.Dispose();
_logger.LogDebug(new EventId(6, nameof(Dispose)), $"The state wait helper for component {_renderedFragment.ComponentId} disposed");
_completionSouce.TrySetCanceled();
}

private static ILogger<T> GetLogger<T>(IServiceProvider services)
{
var loggerFactory = services.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
return loggerFactory.CreateLogger<T>();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.DependencyInjection;

namespace Bunit
{
public class WaitForContextAssertionHelper : IDisposable
{
private readonly ITestContext _testContext;
private readonly Action _assertion;
private readonly Timer _timer;
private readonly ILogger _logger;
private readonly TaskCompletionSource<object?> _completionSouce;
private bool _disposed = false;
private Exception? _capturedException;

public Task WaitTask => _completionSouce.Task;

public WaitForContextAssertionHelper(ITestContext testContext, Action assertion, TimeSpan? timeout = null)
{
_logger = GetLogger<WaitForContextAssertionHelper>(testContext.Services);
_completionSouce = new TaskCompletionSource<object?>();
_testContext = testContext;
_assertion = assertion;
_timer = new Timer(HandleTimeout, this, timeout.GetRuntimeTimeout(), TimeSpan.FromMilliseconds(Timeout.Infinite));
_testContext.OnAfterRender += TryAssertion;
TryAssertion();
}

void TryAssertion()
{
if (_disposed)
return;
lock (_completionSouce)
{
if (_disposed)
return;
_logger.LogDebug(new EventId(1, nameof(TryAssertion)), $"Trying the assertion for the test context");

try
{
_assertion();
_capturedException = null;
_completionSouce.TrySetResult(null);
_logger.LogDebug(new EventId(2, nameof(TryAssertion)), $"The assertion for the test context passed");
Dispose();
}
catch (Exception ex)
{
_logger.LogDebug(new EventId(3, nameof(TryAssertion)), $"The assertion for the test context did not pass. The error message was '{ex.Message}'");
_capturedException = ex;
}
}
}

void HandleTimeout(object state)
{
if (_disposed)
return;

lock (_completionSouce)
{
if (_disposed)
return;

_logger.LogDebug(new EventId(5, nameof(HandleTimeout)), $"The assertion wait helper for the test context timed out");

var error = new WaitForAssertionFailedException(_capturedException);
_completionSouce.TrySetException(error);

Dispose();
}
}

/// <summary>
/// Disposes the wait helper and sets the <see cref="WaitTask"/> to canceled, if it is not
/// already in one of the other completed states.
/// </summary>
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_testContext.OnAfterRender -= TryAssertion;
_timer.Dispose();
_logger.LogDebug(new EventId(6, nameof(Dispose)), $"The state wait helper for the test context disposed");
_completionSouce.TrySetCanceled();
}

private static ILogger<T> GetLogger<T>(IServiceProvider services)
{
var loggerFactory = services.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
return loggerFactory.CreateLogger<T>();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.DependencyInjection;

namespace Bunit
{
public class WaitForContextStateHelper : IDisposable
{
private readonly ITestContext _testContext;
private readonly Func<bool> _statePredicate;
private readonly Timer _timer;
private readonly ILogger _logger;
private readonly TaskCompletionSource<bool> _completionSouce;
private bool _disposed = false;
private Exception? _capturedException;

public Task WaitTask => _completionSouce.Task;

public WaitForContextStateHelper(ITestContext testContext, Func<bool> statePredicate, TimeSpan? timeout = null)
{
_logger = GetLogger<WaitForContextStateHelper>(testContext.Services);
_completionSouce = new TaskCompletionSource<bool>();
_testContext = testContext;
_statePredicate = statePredicate;
_timer = new Timer(HandleTimeout, this, timeout.GetRuntimeTimeout(), TimeSpan.FromMilliseconds(Timeout.Infinite));
_testContext.OnAfterRender += TryPredicate;
TryPredicate();
}

void TryPredicate()
{
if (_disposed)
return;
lock (_completionSouce)
{
if (_disposed)
return;
_logger.LogDebug(new EventId(1, nameof(TryPredicate)), $"Trying the state predicate for the test context");

try
{
var result = _statePredicate();
if (result)
{
_logger.LogDebug(new EventId(2, nameof(TryPredicate)), $"The state predicate for the test context");
_completionSouce.TrySetResult(result);

Dispose();
}
else
{
_logger.LogDebug(new EventId(3, nameof(TryPredicate)), $"The state predicate for the test context did not pass");
}
}
catch (Exception ex)
{
_logger.LogDebug(new EventId(4, nameof(TryPredicate)), $"The state predicate for the test context throw an exception '{ex.GetType().Name}' with message '{ex.Message}'");
_capturedException = ex;
}
}
}

void HandleTimeout(object state)
{
if (_disposed)
return;

lock (_completionSouce)
{
if (_disposed)
return;

_logger.LogDebug(new EventId(5, nameof(HandleTimeout)), $"The state wait helper for the test context timed out");

var error = new WaitForStateFailedException(WaitForStateFailedException.TIMEOUT_BEFORE_PASS, _capturedException);
_completionSouce.TrySetException(error);

Dispose();
}
}

/// <summary>
/// Disposes the wait helper and sets the <see cref="WaitTask"/> to canceled, if it is not
/// already in one of the other completed states.
/// </summary>
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_testContext.OnAfterRender -= TryPredicate;
_timer.Dispose();
_logger.LogDebug(new EventId(6, nameof(Dispose)), $"The state wait helper for the test context disposed");
_completionSouce.TrySetCanceled();
}

private static ILogger<T> GetLogger<T>(IServiceProvider services)
{
var loggerFactory = services.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
return loggerFactory.CreateLogger<T>();
}
}
}
Loading

0 comments on commit 46176c3

Please sign in to comment.