Skip to content

Commit

Permalink
xunit/xunit#2900: Add Assert.Raises overloads for Action
Browse files Browse the repository at this point in the history
  • Loading branch information
bradwilson committed Mar 21, 2024
1 parent 9fa0ed4 commit adcd7cb
Show file tree
Hide file tree
Showing 2 changed files with 224 additions and 2 deletions.
217 changes: 216 additions & 1 deletion EventAsserts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#pragma warning disable CS8600
#pragma warning disable CS8603
#pragma warning disable CS8622
#pragma warning disable CS8625
#endif

using System;
Expand All @@ -20,6 +21,47 @@ namespace Xunit
#endif
partial class Assert
{
/// <summary>
/// Verifies that an event is raised.
/// </summary>
/// <param name="attach">Code to attach the event handler</param>
/// <param name="detach">Code to detach the event handler</param>
/// <param name="testCode">A delegate to the code to be tested</param>
/// <exception cref="RaisesException">Thrown when the expected event was not raised.</exception>
public static void Raises(
Action<Action> attach,
Action<Action> detach,
Action testCode)
{
if (!RaisesInternal(attach, detach, testCode))
throw RaisesException.ForNoEvent();
}

/// <summary>
/// Verifies that an event with the exact event args is raised.
/// </summary>
/// <typeparam name="T">The type of the event arguments to expect</typeparam>
/// <param name="attach">Code to attach the event handler</param>
/// <param name="detach">Code to detach the event handler</param>
/// <param name="testCode">A delegate to the code to be tested</param>
/// <returns>The event sender and arguments wrapped in an object</returns>
/// <exception cref="RaisesException">Thrown when the expected event was not raised.</exception>
public static RaisedEvent<T> Raises<T>(
Action<Action<T>> attach,
Action<Action<T>> detach,
Action testCode)
{
var raisedEvent = RaisesInternal(attach, detach, testCode);

if (raisedEvent == null)
throw RaisesException.ForNoEvent(typeof(T));

if (raisedEvent.Arguments != null && !raisedEvent.Arguments.GetType().Equals(typeof(T)))
throw RaisesException.ForIncorrectType(typeof(T), raisedEvent.Arguments.GetType());

return raisedEvent;
}

/// <summary>
/// Verifies that an event with the exact event args is raised.
/// </summary>
Expand Down Expand Up @@ -97,6 +139,28 @@ partial class Assert
return raisedEvent;
}

/// <summary>
/// Verifies that an event with the exact or a derived event args is raised.
/// </summary>
/// <typeparam name="T">The type of the event arguments to expect</typeparam>
/// <param name="attach">Code to attach the event handler</param>
/// <param name="detach">Code to detach the event handler</param>
/// <param name="testCode">A delegate to the code to be tested</param>
/// <returns>The event sender and arguments wrapped in an object</returns>
/// <exception cref="RaisesException">Thrown when the expected event was not raised.</exception>
public static RaisedEvent<T> RaisesAny<T>(
Action<Action<T>> attach,
Action<Action<T>> detach,
Action testCode)
{
var raisedEvent = RaisesInternal(attach, detach, testCode);

if (raisedEvent == null)
throw RaisesAnyException.ForNoEvent(typeof(T));

return raisedEvent;
}

/// <summary>
/// Verifies that an event with the exact or a derived event args is raised.
/// </summary>
Expand Down Expand Up @@ -140,6 +204,28 @@ partial class Assert
return raisedEvent;
}

/// <summary>
/// Verifies that an event with the exact or a derived event args is raised.
/// </summary>
/// <typeparam name="T">The type of the event arguments to expect</typeparam>
/// <param name="attach">Code to attach the event handler</param>
/// <param name="detach">Code to detach the event handler</param>
/// <param name="testCode">A delegate to the code to be tested</param>
/// <returns>The event sender and arguments wrapped in an object</returns>
/// <exception cref="RaisesException">Thrown when the expected event was not raised.</exception>
public static async Task<RaisedEvent<T>> RaisesAnyAsync<T>(
Action<Action<T>> attach,
Action<Action<T>> detach,
Func<Task> testCode)
{
var raisedEvent = await RaisesAsyncInternal(attach, detach, testCode);

if (raisedEvent == null)
throw RaisesAnyException.ForNoEvent(typeof(T));

return raisedEvent;
}

/// <summary>
/// Verifies that an event with the exact or a derived event args is raised.
/// </summary>
Expand All @@ -162,6 +248,48 @@ partial class Assert
return raisedEvent;
}

/// <summary>
/// Verifies that an event is raised.
/// </summary>
/// <param name="attach">Code to attach the event handler</param>
/// <param name="detach">Code to detach the event handler</param>
/// <param name="testCode">A delegate to the code to be tested</param>
/// <returns>The event sender and arguments wrapped in an object</returns>
/// <exception cref="RaisesException">Thrown when the expected event was not raised.</exception>
public static async Task RaisesAsync(
Action<Action> attach,
Action<Action> detach,
Func<Task> testCode)
{
if (!await RaisesAsyncInternal(attach, detach, testCode))
throw RaisesException.ForNoEvent();
}

/// <summary>
/// Verifies that an event with the exact event args (and not a derived type) is raised.
/// </summary>
/// <typeparam name="T">The type of the event arguments to expect</typeparam>
/// <param name="attach">Code to attach the event handler</param>
/// <param name="detach">Code to detach the event handler</param>
/// <param name="testCode">A delegate to the code to be tested</param>
/// <returns>The event sender and arguments wrapped in an object</returns>
/// <exception cref="RaisesException">Thrown when the expected event was not raised.</exception>
public static async Task<RaisedEvent<T>> RaisesAsync<T>(
Action<Action<T>> attach,
Action<Action<T>> detach,
Func<Task> testCode)
{
var raisedEvent = await RaisesAsyncInternal(attach, detach, testCode);

if (raisedEvent == null)
throw RaisesException.ForNoEvent(typeof(T));

if (raisedEvent.Arguments != null && !raisedEvent.Arguments.GetType().Equals(typeof(T)))
throw RaisesException.ForIncorrectType(typeof(T), raisedEvent.Arguments.GetType());

return raisedEvent;
}

/// <summary>
/// Verifies that an event with the exact event args (and not a derived type) is raised.
/// </summary>
Expand Down Expand Up @@ -189,6 +317,24 @@ partial class Assert

// Helpers

static bool RaisesInternal(
Action<Action> attach,
Action<Action> detach,
Action testCode)
{
GuardArgumentNotNull(nameof(attach), attach);
GuardArgumentNotNull(nameof(detach), detach);
GuardArgumentNotNull(nameof(testCode), testCode);

var result = false;
Action handler = () => result = true;

attach(handler);
testCode();
detach(handler);
return result;
}

#if XUNIT_NULLABLE
static RaisedEvent<EventArgs>? RaisesInternal(
#else
Expand All @@ -214,6 +360,25 @@ partial class Assert
return raisedEvent;
}

#if XUNIT_NULLABLE
static RaisedEvent<T>? RaisesInternal<T>(
#else
static RaisedEvent<T> RaisesInternal<T>(
#endif
Action<Action<T>> attach,
Action<Action<T>> detach,
Action testCode)
{
var raisedEvent = default(RaisedEvent<T>);
Action<T> handler = (T args) => raisedEvent = new RaisedEvent<T>(args);

return RaisesInternal(
() => raisedEvent,
() => attach(handler),
() => detach(handler),
testCode);
}

#if XUNIT_NULLABLE
static RaisedEvent<T>? RaisesInternal<T>(
#else
Expand Down Expand Up @@ -258,6 +423,24 @@ partial class Assert
return handler();
}

static async Task<bool> RaisesAsyncInternal(
Action<Action> attach,
Action<Action> detach,
Func<Task> testCode)
{
GuardArgumentNotNull(nameof(attach), attach);
GuardArgumentNotNull(nameof(detach), detach);
GuardArgumentNotNull(nameof(testCode), testCode);

var result = false;
Action handler = () => result = true;

attach(handler);
await testCode();
detach(handler);
return result;
}

#if XUNIT_NULLABLE
static async Task<RaisedEvent<EventArgs>?> RaisesAsyncInternal(
#else
Expand All @@ -283,6 +466,28 @@ partial class Assert
return raisedEvent;
}

#if XUNIT_NULLABLE
static async Task<RaisedEvent<T>?> RaisesAsyncInternal<T>(
#else
static async Task<RaisedEvent<T>> RaisesAsyncInternal<T>(
#endif
Action<Action<T>> attach,
Action<Action<T>> detach,
Func<Task> testCode)
{
GuardArgumentNotNull(nameof(attach), attach);
GuardArgumentNotNull(nameof(detach), detach);
GuardArgumentNotNull(nameof(testCode), testCode);

var raisedEvent = default(RaisedEvent<T>);
Action<T> handler = (T args) => raisedEvent = new RaisedEvent<T>(args);

attach(handler);
await testCode();
detach(handler);
return raisedEvent;
}

#if XUNIT_NULLABLE
static async Task<RaisedEvent<T>?> RaisesAsyncInternal<T>(
#else
Expand Down Expand Up @@ -315,7 +520,9 @@ partial class Assert
public class RaisedEvent<T>
{
/// <summary>
/// The sender of the event.
/// The sender of the event. When the event is recorded via <see cref="Action{T}"/> rather
/// than <see cref="EventHandler{TEventArgs}"/>, this value will always be <c>null</c>,
/// since there is no sender value when using actions.
/// </summary>
#if XUNIT_NULLABLE
public object? Sender { get; }
Expand All @@ -328,6 +535,14 @@ public class RaisedEvent<T>
/// </summary>
public T Arguments { get; }

/// <summary>
/// Creates a new instance of the <see cref="RaisedEvent{T}" /> class.
/// </summary>
/// <param name="args">The event arguments</param>
public RaisedEvent(T args) :
this(null, args)
{ }

/// <summary>
/// Creates a new instance of the <see cref="RaisedEvent{T}" /> class.
/// </summary>
Expand Down
9 changes: 8 additions & 1 deletion Sdk/Exceptions/RaisesException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,14 @@ partial class RaisesException : XunitException

/// <summary>
/// Creates a new instance of the <see cref="RaisesException" /> class to be thrown when
/// no event was raised.
/// no event (without data) was raised.
/// </summary>
public static RaisesException ForNoEvent() =>
new RaisesException("Assert.Raises() Failure: No event was raised");

/// <summary>
/// Creates a new instance of the <see cref="RaisesException" /> class to be thrown when
/// no event (with data) was raised.
/// </summary>
/// <param name="expected">The type of the event args that was expected</param>
public static RaisesException ForNoEvent(Type expected) =>
Expand Down

0 comments on commit adcd7cb

Please sign in to comment.