diff --git a/Src/FluentAssertions/Events/EventMonitor.cs b/Src/FluentAssertions/Events/EventMonitor.cs index 132154b758..41cc874c2c 100644 --- a/Src/FluentAssertions/Events/EventMonitor.cs +++ b/Src/FluentAssertions/Events/EventMonitor.cs @@ -30,6 +30,8 @@ public EventMonitor(object eventSource, Func utcNow) public T Subject => (T)subject.Target; + private readonly ThreadSafeSequenceGenerator threadSafeSequenceGenerator = new(); + public EventMetadata[] MonitoredEvents { get @@ -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(); @@ -125,7 +127,7 @@ private void AttachEventHandler(EventInfo eventInfo, Func 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); diff --git a/Src/FluentAssertions/Events/EventRecorder.cs b/Src/FluentAssertions/Events/EventRecorder.cs index 647e216f9e..abea8d3b2b 100644 --- a/Src/FluentAssertions/Events/EventRecorder.cs +++ b/Src/FluentAssertions/Events/EventRecorder.cs @@ -25,11 +25,13 @@ internal sealed class EventRecorder : IEventRecording, IDisposable /// The object events are recorded from /// The name of the event that's recorded /// A delegate to get the current date and time in UTC format. - public EventRecorder(object eventRaiser, string eventName, Func utcNow) + /// Class used to generate a sequence in a thread-safe manner. + public EventRecorder(object eventRaiser, string eventName, Func utcNow, ThreadSafeSequenceGenerator sequenceGenerator) { this.utcNow = utcNow; EventObject = eventRaiser; EventName = eventName; + this.sequenceGenerator = sequenceGenerator; } /// @@ -40,6 +42,8 @@ public EventRecorder(object eventRaiser, string eventName, Func utcNow /// public string EventName { get; } + private readonly ThreadSafeSequenceGenerator sequenceGenerator; + public Type EventHandlerType { get; private set; } public void Attach(WeakReference subject, EventInfo eventInfo) @@ -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)); } } @@ -105,7 +109,8 @@ public IEnumerator GetEnumerator() { EventName = EventName, Parameters = @event.Parameters, - TimestampUtc = @event.TimestampUtc + TimestampUtc = @event.TimestampUtc, + Sequence = @event.Sequence }; } } diff --git a/Src/FluentAssertions/Events/OccurredEvent.cs b/Src/FluentAssertions/Events/OccurredEvent.cs index 937025bfd3..761341ea29 100644 --- a/Src/FluentAssertions/Events/OccurredEvent.cs +++ b/Src/FluentAssertions/Events/OccurredEvent.cs @@ -21,5 +21,10 @@ public class OccurredEvent /// The exact date and time of the occurrence in . /// public DateTime TimestampUtc { get; set; } + + /// + /// The order in which this event was raised on the monitored object. + /// + public int Sequence { get; set; } } } diff --git a/Src/FluentAssertions/Events/RecordedEvent.cs b/Src/FluentAssertions/Events/RecordedEvent.cs index 1930a605ee..69168a7692 100644 --- a/Src/FluentAssertions/Events/RecordedEvent.cs +++ b/Src/FluentAssertions/Events/RecordedEvent.cs @@ -12,10 +12,11 @@ internal class RecordedEvent /// /// Default constructor stores the parameters the event was raised with /// - public RecordedEvent(DateTime utcNow, params object[] parameters) + public RecordedEvent(DateTime utcNow, int sequence, params object[] parameters) { Parameters = parameters; TimestampUtc = utcNow; + Sequence = sequence; } /// @@ -27,5 +28,10 @@ public RecordedEvent(DateTime utcNow, params object[] parameters) /// Parameters for the event /// public object[] Parameters { get; } + + /// + /// The order in which this event was invoked on the monitored object. + /// + public int Sequence { get; } } } diff --git a/Src/FluentAssertions/Events/ThreadSafeSequenceGenerator.cs b/Src/FluentAssertions/Events/ThreadSafeSequenceGenerator.cs new file mode 100644 index 0000000000..4ccf4ad443 --- /dev/null +++ b/Src/FluentAssertions/Events/ThreadSafeSequenceGenerator.cs @@ -0,0 +1,20 @@ +using System.Threading; + +namespace FluentAssertions.Events +{ + /// + /// Generates a sequence in a thread-safe manner. + /// + internal sealed class ThreadSafeSequenceGenerator + { + private int sequence = -1; + + /// + /// Increments the current sequence. + /// + public int Increment() + { + return Interlocked.Increment(ref sequence); + } + } +} diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt index 4f01de4ab7..3abc0f26e9 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt @@ -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; } } } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt index ee1154cbe1..2163da6552 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt @@ -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; } } } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt index 75bcd1c0bf..a97c3e2b3a 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt @@ -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; } } } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt index 7ec995f597..17041a01d3 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt @@ -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; } } } diff --git a/Tests/FluentAssertions.Specs/Events/EventAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Events/EventAssertionSpecs.cs index 432996e070..f07bc28c78 100644 --- a/Tests/FluentAssertions.Specs/Events/EventAssertionSpecs.cs +++ b/Tests/FluentAssertions.Specs/Events/EventAssertionSpecs.cs @@ -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 @@ -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; @@ -827,6 +865,11 @@ public interface IEventRaisingInterface2 event EventHandler Interface2Event; } + public interface IEventRaisingInterface3 + { + event EventHandler Interface3Event; + } + public interface IInheritsEventRaisingInterface : IEventRaisingInterface { } diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index 6e1426a6bb..5b69bb9c9b 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -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