Skip to content

Instantly share code, notes, and snippets.

@olivier-spinelli
Created October 1, 2023 07:52
Show Gist options
  • Save olivier-spinelli/c075b672c26a3ba26801e55be3d3e5d9 to your computer and use it in GitHub Desktop.
Save olivier-spinelli/c075b672c26a3ba26801e55be3d3e5d9 to your computer and use it in GitHub Desktop.
A "breakpoint" that let other threads run.
// Implemented as an extension method (on IMonitorTestHelper that provides logging) but this
// dependency can easily ve removed.
public static class MonitorTestHelperExtension
{
sealed class Resumer
{
internal readonly TaskCompletionSource _tcs;
readonly Timer _timer;
readonly Func<bool, bool> _resume;
bool _reentrant;
internal Resumer( Func<bool, bool> resume )
{
_timer = new Timer( OnTimer, null, 1000, 1000 );
_tcs = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously );
_resume = resume;
}
void OnTimer( object? _ )
{
if( _reentrant ) return;
_reentrant = true;
if( _resume( false ) )
{
_tcs.SetResult();
_timer.Dispose();
}
_reentrant = false;
}
}
/// <summary>
/// Asynchronously blocks until true is returned from the callback (the callback is called every second).
/// This can be used only when <see cref="Debugger.IsAttached"/> is true: this is ignored otherwise.
/// <para>
/// This is intended to let context alive for an undetermined delay, this can be seen as an interruptible
/// <c>await Task.Delay( Timeout.Infinite );</c> or a breakpoint that suspends the current task but let
/// all the other tasks and threads run.
/// </para>
/// <para>
/// Usage: set a breakpoint in the callback and set the resume variable to true (typically via the watch window)
/// to continue the execution.
/// <code>
/// Put a breakpoint here
/// |
/// await TestHelper.SuspendAsync( resume => resume );
/// </code>
/// </para>
/// </summary>
/// <param name="resume">callback always called with false that completes the returned task when true is returned.</param>
/// <returns>The task to await.</returns>
public static Task SuspendAsync( this Testing.IMonitorTestHelper @this,
Func<bool, bool> resume,
[CallerMemberName] string? testName = null,
[CallerLineNumber] int lineNumber = 0,
[CallerFilePath] string? fileName = null )
{
Throw.CheckNotNullArgument( resume );
if( !Debugger.IsAttached )
{
@this.Monitor.Warn( $"TestHelper.SuspendAsync called from '{testName}' method while no debugger is attached. Ignoring it.", lineNumber, fileName );
return Task.CompletedTask;
}
@this.Monitor.Info( $"TestHelper.SuspendAsync called from '{testName}' method.", lineNumber, fileName );
return new Resumer( resume )._tcs.Task;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment