Skip to content

Commit

Permalink
OccurredEvent ordering on monitored object is now done via thread-s…
Browse files Browse the repository at this point in the history
…afe counter (#1773)

Added EventRaisedOrder to correctly track order of raised events in Monitor
  • Loading branch information
MullerWasHere committed Jan 17, 2022
1 parent 834a2db commit 8165b67
Show file tree
Hide file tree
Showing 11 changed files with 92 additions and 6 deletions.
6 changes: 4 additions & 2 deletions Src/FluentAssertions/Events/EventMonitor.cs
Expand Up @@ -30,6 +30,8 @@ public EventMonitor(object eventSource, Func<DateTime> utcNow)

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

private readonly ThreadSafeSequenceGenerator threadSafeSequenceGenerator = new();

public EventMetadata[] MonitoredEvents
{
get
Expand All @@ -49,7 +51,7 @@ public OccurredEvent[] OccurredEvents
from eventName in recorderMap.Keys
let recording = GetRecordingFor(eventName)
from @event in recording
orderby @event.TimestampUtc
orderby @event.Sequence
select @event;

return query.ToArray();
Expand Down Expand Up @@ -125,7 +127,7 @@ private void AttachEventHandler(EventInfo eventInfo, Func<DateTime> utcNow)
{
if (!recorderMap.TryGetValue(eventInfo.Name, out _))
{
var recorder = new EventRecorder(subject.Target, eventInfo.Name, utcNow);
var recorder = new EventRecorder(subject.Target, eventInfo.Name, utcNow, threadSafeSequenceGenerator);
if (recorderMap.TryAdd(eventInfo.Name, recorder))
{
recorder.Attach(subject, eventInfo);
Expand Down
11 changes: 8 additions & 3 deletions Src/FluentAssertions/Events/EventRecorder.cs
Expand Up @@ -25,11 +25,13 @@ internal sealed class EventRecorder : IEventRecording, IDisposable
/// <param name="eventRaiser">The object events are recorded from</param>
/// <param name="eventName">The name of the event that's recorded</param>
/// <param name="utcNow">A delegate to get the current date and time in UTC format.</param>
public EventRecorder(object eventRaiser, string eventName, Func<DateTime> utcNow)
/// <param name="sequenceGenerator">Class used to generate a sequence in a thread-safe manner.</param>
public EventRecorder(object eventRaiser, string eventName, Func<DateTime> utcNow, ThreadSafeSequenceGenerator sequenceGenerator)
{
this.utcNow = utcNow;
EventObject = eventRaiser;
EventName = eventName;
this.sequenceGenerator = sequenceGenerator;
}

/// <summary>
Expand All @@ -40,6 +42,8 @@ public EventRecorder(object eventRaiser, string eventName, Func<DateTime> utcNow
/// <inheritdoc />
public string EventName { get; }

private readonly ThreadSafeSequenceGenerator sequenceGenerator;

public Type EventHandlerType { get; private set; }

public void Attach(WeakReference subject, EventInfo eventInfo)
Expand Down Expand Up @@ -77,7 +81,7 @@ public void RecordEvent(params object[] parameters)
{
lock (lockable)
{
raisedEvents.Add(new RecordedEvent(utcNow(), parameters));
raisedEvents.Add(new RecordedEvent(utcNow(), sequenceGenerator.Increment(), parameters));
}
}

Expand Down Expand Up @@ -105,7 +109,8 @@ public IEnumerator<OccurredEvent> GetEnumerator()
{
EventName = EventName,
Parameters = @event.Parameters,
TimestampUtc = @event.TimestampUtc
TimestampUtc = @event.TimestampUtc,
Sequence = @event.Sequence
};
}
}
Expand Down
5 changes: 5 additions & 0 deletions Src/FluentAssertions/Events/OccurredEvent.cs
Expand Up @@ -21,5 +21,10 @@ public class OccurredEvent
/// The exact date and time of the occurrence in <see cref="DateTimeKind.Local"/>.
/// </summary>
public DateTime TimestampUtc { get; set; }

/// <summary>
/// The order in which this event was raised on the monitored object.
/// </summary>
public int Sequence { get; set; }
}
}
8 changes: 7 additions & 1 deletion Src/FluentAssertions/Events/RecordedEvent.cs
Expand Up @@ -12,10 +12,11 @@ internal class RecordedEvent
/// <summary>
/// Default constructor stores the parameters the event was raised with
/// </summary>
public RecordedEvent(DateTime utcNow, params object[] parameters)
public RecordedEvent(DateTime utcNow, int sequence, params object[] parameters)
{
Parameters = parameters;
TimestampUtc = utcNow;
Sequence = sequence;
}

/// <summary>
Expand All @@ -27,5 +28,10 @@ public RecordedEvent(DateTime utcNow, params object[] parameters)
/// Parameters for the event
/// </summary>
public object[] Parameters { get; }

/// <summary>
/// The order in which this event was invoked on the monitored object.
/// </summary>
public int Sequence { get; }
}
}
20 changes: 20 additions & 0 deletions Src/FluentAssertions/Events/ThreadSafeSequenceGenerator.cs
@@ -0,0 +1,20 @@
using System.Threading;

namespace FluentAssertions.Events
{
/// <summary>
/// Generates a sequence in a thread-safe manner.
/// </summary>
internal sealed class ThreadSafeSequenceGenerator
{
private int sequence = -1;

/// <summary>
/// Increments the current sequence.
/// </summary>
public int Increment()
{
return Interlocked.Increment(ref sequence);
}
}
}
Expand Up @@ -1221,6 +1221,7 @@ namespace FluentAssertions.Events
public OccurredEvent() { }
public string EventName { get; set; }
public object[] Parameters { get; set; }
public int Sequence { get; set; }
public System.DateTime TimestampUtc { get; set; }
}
}
Expand Down
Expand Up @@ -1221,6 +1221,7 @@ namespace FluentAssertions.Events
public OccurredEvent() { }
public string EventName { get; set; }
public object[] Parameters { get; set; }
public int Sequence { get; set; }
public System.DateTime TimestampUtc { get; set; }
}
}
Expand Down
Expand Up @@ -1221,6 +1221,7 @@ namespace FluentAssertions.Events
public OccurredEvent() { }
public string EventName { get; set; }
public object[] Parameters { get; set; }
public int Sequence { get; set; }
public System.DateTime TimestampUtc { get; set; }
}
}
Expand Down
Expand Up @@ -1221,6 +1221,7 @@ namespace FluentAssertions.Events
public OccurredEvent() { }
public string EventName { get; set; }
public object[] Parameters { get; set; }
public int Sequence { get; set; }
public System.DateTime TimestampUtc { get; set; }
}
}
Expand Down
43 changes: 43 additions & 0 deletions Tests/FluentAssertions.Specs/Events/EventAssertionSpecs.cs
Expand Up @@ -411,6 +411,28 @@ public void When_constraints_are_specified_it_should_filter_the_events_based_on_
.Which.PropertyName.Should().Be("Boo");
}

[Fact]
public void When_events_are_raised_regardless_of_time_tick_it_should_return_by_invokation_order()
{
// Arrange
var observable = new TestEventRaisingInOrder();
var utcNow = 11.January(2022).At(12, 00).AsUtc();
using var monitor = observable.Monitor(() => utcNow);

// Act
observable.RaiseAllEvents();

// Assert
monitor.OccurredEvents[0].EventName.Should().Be(nameof(TestEventRaisingInOrder.InterfaceEvent));
monitor.OccurredEvents[0].Sequence.Should().Be(0);

monitor.OccurredEvents[1].EventName.Should().Be(nameof(TestEventRaisingInOrder.Interface2Event));
monitor.OccurredEvents[1].Sequence.Should().Be(1);

monitor.OccurredEvents[2].EventName.Should().Be(nameof(TestEventRaisingInOrder.Interface3Event));
monitor.OccurredEvents[2].Sequence.Should().Be(2);
}

#endregion

#region Should(Not)RaisePropertyChanged events
Expand Down Expand Up @@ -817,6 +839,22 @@ public void RaiseBothEvents()
}
}

private class TestEventRaisingInOrder : IEventRaisingInterface, IEventRaisingInterface2, IEventRaisingInterface3
{
public event EventHandler Interface3Event;

public event EventHandler Interface2Event;

public event EventHandler InterfaceEvent;

public void RaiseAllEvents()
{
InterfaceEvent?.Invoke(this, EventArgs.Empty);
Interface2Event?.Invoke(this, EventArgs.Empty);
Interface3Event?.Invoke(this, EventArgs.Empty);
}
}

public interface IEventRaisingInterface
{
event EventHandler InterfaceEvent;
Expand All @@ -827,6 +865,11 @@ public interface IEventRaisingInterface2
event EventHandler Interface2Event;
}

public interface IEventRaisingInterface3
{
event EventHandler Interface3Event;
}

public interface IInheritsEventRaisingInterface : IEventRaisingInterface
{
}
Expand Down
1 change: 1 addition & 0 deletions docs/_pages/releases.md
Expand Up @@ -18,6 +18,7 @@ sidebar:
* `ContainItemsAssignableTo` now expects at least one item assignable to `T` - [#1765](https://github.com/fluentassertions/fluentassertions/pull/1765)
* Querying methods on classes, e.g. `typeof(MyController).Methods()`, now also includes static methods - [#1740](https://github.com/fluentassertions/fluentassertions/pull/1740)
* Variable name is not captured after await assertion - [#1770](https://github.com/fluentassertions/fluentassertions/pull/1770)
* `OccurredEvent` ordering on monitored object is now done via thread-safe counter - [#1773](https://github.com/fluentassertions/fluentassertions/pull/1773)
* Avoid a `NullReferenceException` when testing an application compiled with .NET Native - [#1776](https://github.com/fluentassertions/fluentassertions/pull/1776)

## 6.3.0
Expand Down

0 comments on commit 8165b67

Please sign in to comment.