Skip to content

Commit

Permalink
Merge 030821c into 060c463
Browse files Browse the repository at this point in the history
  • Loading branch information
ITaluone committed Apr 22, 2024
2 parents 060c463 + 030821c commit 904cb88
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 35 deletions.
25 changes: 20 additions & 5 deletions Src/FluentAssertions/AssertionExtensions.cs
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
Expand Down Expand Up @@ -873,14 +873,29 @@ public static TaskCompletionSourceAssertions<T> Should<T>(this TaskCompletionSou
/// Starts monitoring <paramref name="eventSource"/> for its events.
/// </summary>
/// <param name="eventSource">The object for which to monitor the events.</param>
/// <param name="utcNow">
/// An optional delegate that returns the current date and time in UTC format.
/// Will revert to <see cref="DateTime.UtcNow"/> if no delegate was provided.
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="eventSource"/> is <see langword="null"/>.</exception>
public static IMonitor<T> Monitor<T>(this T eventSource, Func<DateTime> utcNow = null)
public static IMonitor<T> Monitor<T>(this T eventSource)
{
return new EventMonitor<T>(eventSource, utcNow ?? (() => DateTime.UtcNow));
return new EventMonitor<T>(eventSource, new EventMonitorOptions());
}

/// <summary>
/// Starts monitoring <paramref name="eventSource"/> for its events using the given <paramref name="configureOptions"/>.
/// </summary>
/// <param name="eventSource">The object for which to monitor the events.</param>
/// <param name="configureOptions">
/// Options to configure the EventMonitor.
/// </param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="eventSource"/> is Null.</exception>
public static IMonitor<T> Monitor<T>(this T eventSource, Action<EventMonitorOptions> configureOptions)
{
Guard.ThrowIfArgumentIsNull(configureOptions, nameof(configureOptions));

var options = new EventMonitorOptions();
configureOptions(options);
return new EventMonitor<T>(eventSource, options);
}

#endif
Expand Down
54 changes: 40 additions & 14 deletions Src/FluentAssertions/Events/EventMonitor.cs
Expand Up @@ -18,29 +18,28 @@ internal sealed class EventMonitor<T> : IMonitor<T>

private readonly ConcurrentDictionary<string, EventRecorder> recorderMap = new();

public EventMonitor(object eventSource, Func<DateTime> utcNow)
public EventMonitor(object eventSource, EventMonitorOptions options)
{
Guard.ThrowIfArgumentIsNull(eventSource, nameof(eventSource), "Cannot monitor the events of a <null> object.");
Guard.ThrowIfArgumentIsNull(options, nameof(options), "Event monitor needs configuration.");

this.options = options;

subject = new WeakReference(eventSource);

Attach(typeof(T), utcNow);
Attach(typeof(T), this.options.TimestampProvider);
}

public T Subject => (T)subject.Target;

private readonly ThreadSafeSequenceGenerator threadSafeSequenceGenerator = new();
private readonly EventMonitorOptions options;

public EventMetadata[] MonitoredEvents
{
get
{
return recorderMap
.Values
.Select(recorder => new EventMetadata(recorder.EventName, recorder.EventHandlerType))
.ToArray();
}
}
public EventMetadata[] MonitoredEvents =>
recorderMap
.Values
.Select(recorder => new EventMetadata(recorder.EventName, recorder.EventHandlerType))
.ToArray();

public OccurredEvent[] OccurredEvents
{
Expand Down Expand Up @@ -117,12 +116,24 @@ public void Dispose()
{
foreach (EventRecorder recorder in recorderMap.Values)
{
recorder.Dispose();
DisposeSafeIfRequested(recorder);
}

recorderMap.Clear();
}

private void DisposeSafeIfRequested(IDisposable recorder)
{
try
{
recorder.Dispose();
}
catch when (options.ShouldIgnoreEventAccessorExceptions)
{
// ignore
}
}

private void AttachEventHandler(EventInfo eventInfo, Func<DateTime> utcNow)
{
if (!recorderMap.TryGetValue(eventInfo.Name, out _))
Expand All @@ -131,7 +142,22 @@ private void AttachEventHandler(EventInfo eventInfo, Func<DateTime> utcNow)

if (recorderMap.TryAdd(eventInfo.Name, recorder))
{
recorder.Attach(subject, eventInfo);
AttachEventHandler(eventInfo, recorder);
}
}
}

private void AttachEventHandler(EventInfo eventInfo, EventRecorder recorder)
{
try
{
recorder.Attach(subject, eventInfo);
}
catch when (options.ShouldIgnoreEventAccessorExceptions)
{
if (!options.ShouldRecordEventsWithBrokenAccessor)
{
recorderMap.TryRemove(eventInfo.Name, out _);
}
}
}
Expand Down
59 changes: 59 additions & 0 deletions Src/FluentAssertions/Events/EventMonitorOptions.cs
@@ -0,0 +1,59 @@
using System;

namespace FluentAssertions.Events;

/// <summary>
/// Settings for the EventMonitor.
/// </summary>
public class EventMonitorOptions
{
/// <summary>
/// Will ignore the events, if they throw an exception on any custom event accessor implementation. default: false.
/// </summary>
internal bool ShouldIgnoreEventAccessorExceptions { get; private set; }

/// <summary>
/// This will record the event, even if the event accessor add event threw an exception. To ignore exceptions in the event add accessor, call <see cref="IgnoreEventAccessorExceptions"/> property to set it to true. default: false.
/// </summary>
internal bool ShouldRecordEventsWithBrokenAccessor { get; private set; }

/// <summary>
/// Func used to generate the timestamp.
/// </summary>
internal Func<DateTime> TimestampProvider { get; private set; } = () => DateTime.UtcNow;

/// <summary>
/// When called it will ignore event accessor Exceptions.
/// </summary>
/// <returns>The options instance for method stacking.</returns>
public EventMonitorOptions IgnoreEventAccessorExceptions()
{
ShouldIgnoreEventAccessorExceptions = true;
return this;
}

/// <summary>
/// When called it will record the event even when the accessor threw an exception.
/// </summary>
/// <returns>The options instance for method stacking.</returns>
public EventMonitorOptions RecordEventsWithBrokenAccessor()
{
ShouldRecordEventsWithBrokenAccessor = true;
return this;
}

/// <summary>
/// Sets the timestamp provider. By default it is <see cref="DateTime.UtcNow"/>.
/// </summary>
/// <param name="timestampProvider">The timestamp provider.</param>
/// <returns>The options instance for method stacking.</returns>
internal EventMonitorOptions ConfigureTimestampProvider(Func<DateTime> timestampProvider)
{
if (timestampProvider != null)
{
TimestampProvider = timestampProvider;
}

return this;
}
}
Expand Up @@ -37,7 +37,8 @@ namespace FluentAssertions
public static FluentAssertions.Specialized.MemberExecutionTime<T> ExecutionTimeOf<T>(this T subject, System.Linq.Expressions.Expression<System.Action<T>> action, FluentAssertions.Common.StartTimer createTimer = null) { }
public static System.Action Invoking<T>(this T subject, System.Action<T> action) { }
public static System.Func<TResult> Invoking<T, TResult>(this T subject, System.Func<T, TResult> action) { }
public static FluentAssertions.Events.IMonitor<T> Monitor<T>(this T eventSource, System.Func<System.DateTime> utcNow = null) { }
public static FluentAssertions.Events.IMonitor<T> Monitor<T>(this T eventSource) { }
public static FluentAssertions.Events.IMonitor<T> Monitor<T>(this T eventSource, System.Action<FluentAssertions.Events.EventMonitorOptions> configureOptions) { }
public static FluentAssertions.Specialized.ExecutionTimeAssertions Should(this FluentAssertions.Specialized.ExecutionTime executionTime) { }
[System.Obsolete("You are asserting the \'AndConstraint\' itself. Remove the \'Should()\' method direct" +
"ly following \'And\'", true)]
Expand Down Expand Up @@ -1130,6 +1131,12 @@ namespace FluentAssertions.Events
public string EventName { get; }
public System.Type HandlerType { get; }
}
public class EventMonitorOptions
{
public EventMonitorOptions() { }
public FluentAssertions.Events.EventMonitorOptions IgnoreEventAccessorExceptions() { }
public FluentAssertions.Events.EventMonitorOptions RecordEventsWithBrokenAccessor() { }
}
public interface IEventRecording : System.Collections.Generic.IEnumerable<FluentAssertions.Events.OccurredEvent>, System.Collections.IEnumerable
{
System.Type EventHandlerType { get; }
Expand Down
Expand Up @@ -37,7 +37,8 @@ namespace FluentAssertions
public static FluentAssertions.Specialized.MemberExecutionTime<T> ExecutionTimeOf<T>(this T subject, System.Linq.Expressions.Expression<System.Action<T>> action, FluentAssertions.Common.StartTimer createTimer = null) { }
public static System.Action Invoking<T>(this T subject, System.Action<T> action) { }
public static System.Func<TResult> Invoking<T, TResult>(this T subject, System.Func<T, TResult> action) { }
public static FluentAssertions.Events.IMonitor<T> Monitor<T>(this T eventSource, System.Func<System.DateTime> utcNow = null) { }
public static FluentAssertions.Events.IMonitor<T> Monitor<T>(this T eventSource) { }
public static FluentAssertions.Events.IMonitor<T> Monitor<T>(this T eventSource, System.Action<FluentAssertions.Events.EventMonitorOptions> configureOptions) { }
public static FluentAssertions.Specialized.ExecutionTimeAssertions Should(this FluentAssertions.Specialized.ExecutionTime executionTime) { }
[System.Obsolete("You are asserting the \'AndConstraint\' itself. Remove the \'Should()\' method direct" +
"ly following \'And\'", true)]
Expand Down Expand Up @@ -1143,6 +1144,12 @@ namespace FluentAssertions.Events
public string EventName { get; }
public System.Type HandlerType { get; }
}
public class EventMonitorOptions
{
public EventMonitorOptions() { }
public FluentAssertions.Events.EventMonitorOptions IgnoreEventAccessorExceptions() { }
public FluentAssertions.Events.EventMonitorOptions RecordEventsWithBrokenAccessor() { }
}
public interface IEventRecording : System.Collections.Generic.IEnumerable<FluentAssertions.Events.OccurredEvent>, System.Collections.IEnumerable
{
System.Type EventHandlerType { get; }
Expand Down
Expand Up @@ -37,7 +37,8 @@ namespace FluentAssertions
public static FluentAssertions.Specialized.MemberExecutionTime<T> ExecutionTimeOf<T>(this T subject, System.Linq.Expressions.Expression<System.Action<T>> action, FluentAssertions.Common.StartTimer createTimer = null) { }
public static System.Action Invoking<T>(this T subject, System.Action<T> action) { }
public static System.Func<TResult> Invoking<T, TResult>(this T subject, System.Func<T, TResult> action) { }
public static FluentAssertions.Events.IMonitor<T> Monitor<T>(this T eventSource, System.Func<System.DateTime> utcNow = null) { }
public static FluentAssertions.Events.IMonitor<T> Monitor<T>(this T eventSource) { }
public static FluentAssertions.Events.IMonitor<T> Monitor<T>(this T eventSource, System.Action<FluentAssertions.Events.EventMonitorOptions> configureOptions) { }
public static FluentAssertions.Specialized.ExecutionTimeAssertions Should(this FluentAssertions.Specialized.ExecutionTime executionTime) { }
[System.Obsolete("You are asserting the \'AndConstraint\' itself. Remove the \'Should()\' method direct" +
"ly following \'And\'", true)]
Expand Down Expand Up @@ -1130,6 +1131,12 @@ namespace FluentAssertions.Events
public string EventName { get; }
public System.Type HandlerType { get; }
}
public class EventMonitorOptions
{
public EventMonitorOptions() { }
public FluentAssertions.Events.EventMonitorOptions IgnoreEventAccessorExceptions() { }
public FluentAssertions.Events.EventMonitorOptions RecordEventsWithBrokenAccessor() { }
}
public interface IEventRecording : System.Collections.Generic.IEnumerable<FluentAssertions.Events.OccurredEvent>, System.Collections.IEnumerable
{
System.Type EventHandlerType { get; }
Expand Down

0 comments on commit 904cb88

Please sign in to comment.