diff --git a/Src/FluentAssertions/AssertionExtensions.cs b/Src/FluentAssertions/AssertionExtensions.cs index d16c01d5d7..e832f6127f 100644 --- a/Src/FluentAssertions/AssertionExtensions.cs +++ b/Src/FluentAssertions/AssertionExtensions.cs @@ -917,7 +917,31 @@ public static TaskCompletionSourceAssertions Should(this TaskCompletionSou /// is . public static IMonitor Monitor(this T eventSource, Func utcNow = null) { - return new EventMonitor(eventSource, utcNow ?? (() => DateTime.UtcNow)); + var options = new EventMonitorOptions(); + + if (utcNow is not null) + { + options.ConfigureTimestampProvider(utcNow); + } + + return new EventMonitor(eventSource, options); + } + + /// + /// Starts monitoring for its events. + /// + /// The object for which to monitor the events. + /// + /// An optional delegate that can be used to configure the used to monitor the events. + /// + /// is . + public static IMonitor Monitor(this T eventSource, Action configureOptions) + { + var options = new EventMonitorOptions(); + + configureOptions(options); + + return new EventMonitor(eventSource, options); } #endif diff --git a/Src/FluentAssertions/Events/EventMonitor.cs b/Src/FluentAssertions/Events/EventMonitor.cs index a0bcf681ef..26ebe0ad29 100644 --- a/Src/FluentAssertions/Events/EventMonitor.cs +++ b/Src/FluentAssertions/Events/EventMonitor.cs @@ -18,29 +18,28 @@ internal sealed class EventMonitor : IMonitor private readonly ConcurrentDictionary recorderMap = new(); - public EventMonitor(object eventSource, Func utcNow) + public EventMonitor(object eventSource, EventMonitorOptions options) { Guard.ThrowIfArgumentIsNull(eventSource, nameof(eventSource), "Cannot monitor the events of a 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 { @@ -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 utcNow) { if (!recorderMap.TryGetValue(eventInfo.Name, out _)) @@ -131,7 +142,22 @@ private void AttachEventHandler(EventInfo eventInfo, Func 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 _); } } } diff --git a/Src/FluentAssertions/Events/EventMonitorOptions.cs b/Src/FluentAssertions/Events/EventMonitorOptions.cs new file mode 100644 index 0000000000..4ece4efd71 --- /dev/null +++ b/Src/FluentAssertions/Events/EventMonitorOptions.cs @@ -0,0 +1,59 @@ +using System; + +namespace FluentAssertions.Events; + +/// +/// Settings for the EventMonitor. +/// +public class EventMonitorOptions +{ + /// + /// Will ignore the events, if they throw an exception on any custom event accessor implementation. default: false. + /// + internal bool ShouldIgnoreEventAccessorExceptions { get; private set; } + + /// + /// This will record the event, even if the event accessor add event threw an exception. To ignore exceptions in the event add accessor, call property to set it to true. default: false. + /// + internal bool ShouldRecordEventsWithBrokenAccessor { get; private set; } + + /// + /// Func used to generate the timestamp. + /// + internal Func TimestampProvider { get; private set; } = () => DateTime.UtcNow; + + /// + /// When called it will ignore event accessor Exceptions. + /// + /// The options instance for method stacking. + public EventMonitorOptions IgnoreEventAccessorExceptions() + { + ShouldIgnoreEventAccessorExceptions = true; + return this; + } + + /// + /// When called it will record the event even when the accessor threw an exception. + /// + /// The options instance for method stacking. + public EventMonitorOptions RecordEventsWithBrokenAccessor() + { + ShouldRecordEventsWithBrokenAccessor = true; + return this; + } + + /// + /// Sets the timestamp provider. By default it is . + /// + /// The timestamp provider. + /// The options instance for method stacking. + internal EventMonitorOptions ConfigureTimestampProvider(Func timestampProvider) + { + if (timestampProvider != null) + { + TimestampProvider = timestampProvider; + } + + return this; + } +} diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt index 377122900a..c74d34819d 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt @@ -37,6 +37,7 @@ namespace FluentAssertions public static FluentAssertions.Specialized.MemberExecutionTime ExecutionTimeOf(this T subject, System.Linq.Expressions.Expression> action, FluentAssertions.Common.StartTimer createTimer = null) { } public static System.Action Invoking(this T subject, System.Action action) { } public static System.Func Invoking(this T subject, System.Func action) { } + public static FluentAssertions.Events.IMonitor Monitor(this T eventSource, System.Action configureOptions) { } public static FluentAssertions.Events.IMonitor Monitor(this T eventSource, System.Func utcNow = null) { } public static FluentAssertions.Specialized.ExecutionTimeAssertions Should(this FluentAssertions.Specialized.ExecutionTime executionTime) { } [System.Obsolete("You are asserting the \'AndConstraint\' itself. Remove the \'Should()\' method direct" + @@ -1272,6 +1273,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, System.Collections.IEnumerable { System.Type EventHandlerType { get; } @@ -2801,4 +2808,4 @@ namespace FluentAssertions.Xml public bool CanHandle(object value) { } public void Format(object value, FluentAssertions.Formatting.FormattedObjectGraph formattedGraph, FluentAssertions.Formatting.FormattingContext context, FluentAssertions.Formatting.FormatChild formatChild) { } } -} +} \ No newline at end of file diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt index fa1f44fa66..3583be1fa1 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt @@ -37,6 +37,7 @@ namespace FluentAssertions public static FluentAssertions.Specialized.MemberExecutionTime ExecutionTimeOf(this T subject, System.Linq.Expressions.Expression> action, FluentAssertions.Common.StartTimer createTimer = null) { } public static System.Action Invoking(this T subject, System.Action action) { } public static System.Func Invoking(this T subject, System.Func action) { } + public static FluentAssertions.Events.IMonitor Monitor(this T eventSource, System.Action configureOptions) { } public static FluentAssertions.Events.IMonitor Monitor(this T eventSource, System.Func utcNow = null) { } public static FluentAssertions.Specialized.ExecutionTimeAssertions Should(this FluentAssertions.Specialized.ExecutionTime executionTime) { } [System.Obsolete("You are asserting the \'AndConstraint\' itself. Remove the \'Should()\' method direct" + @@ -1285,6 +1286,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, System.Collections.IEnumerable { System.Type EventHandlerType { get; } @@ -2931,4 +2938,4 @@ namespace FluentAssertions.Xml public bool CanHandle(object value) { } public void Format(object value, FluentAssertions.Formatting.FormattedObjectGraph formattedGraph, FluentAssertions.Formatting.FormattingContext context, FluentAssertions.Formatting.FormatChild formatChild) { } } -} +} \ No newline at end of file diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt index 63d5985ea5..2556d6d3a5 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt @@ -37,6 +37,7 @@ namespace FluentAssertions public static FluentAssertions.Specialized.MemberExecutionTime ExecutionTimeOf(this T subject, System.Linq.Expressions.Expression> action, FluentAssertions.Common.StartTimer createTimer = null) { } public static System.Action Invoking(this T subject, System.Action action) { } public static System.Func Invoking(this T subject, System.Func action) { } + public static FluentAssertions.Events.IMonitor Monitor(this T eventSource, System.Action configureOptions) { } public static FluentAssertions.Events.IMonitor Monitor(this T eventSource, System.Func utcNow = null) { } public static FluentAssertions.Specialized.ExecutionTimeAssertions Should(this FluentAssertions.Specialized.ExecutionTime executionTime) { } [System.Obsolete("You are asserting the \'AndConstraint\' itself. Remove the \'Should()\' method direct" + @@ -1272,6 +1273,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, System.Collections.IEnumerable { System.Type EventHandlerType { get; } @@ -2803,4 +2810,4 @@ namespace FluentAssertions.Xml public bool CanHandle(object value) { } public void Format(object value, FluentAssertions.Formatting.FormattedObjectGraph formattedGraph, FluentAssertions.Formatting.FormattingContext context, FluentAssertions.Formatting.FormatChild formatChild) { } } -} +} \ No newline at end of file diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt index 63d5985ea5..2556d6d3a5 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt @@ -37,6 +37,7 @@ namespace FluentAssertions public static FluentAssertions.Specialized.MemberExecutionTime ExecutionTimeOf(this T subject, System.Linq.Expressions.Expression> action, FluentAssertions.Common.StartTimer createTimer = null) { } public static System.Action Invoking(this T subject, System.Action action) { } public static System.Func Invoking(this T subject, System.Func action) { } + public static FluentAssertions.Events.IMonitor Monitor(this T eventSource, System.Action configureOptions) { } public static FluentAssertions.Events.IMonitor Monitor(this T eventSource, System.Func utcNow = null) { } public static FluentAssertions.Specialized.ExecutionTimeAssertions Should(this FluentAssertions.Specialized.ExecutionTime executionTime) { } [System.Obsolete("You are asserting the \'AndConstraint\' itself. Remove the \'Should()\' method direct" + @@ -1272,6 +1273,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, System.Collections.IEnumerable { System.Type EventHandlerType { get; } @@ -2803,4 +2810,4 @@ namespace FluentAssertions.Xml public bool CanHandle(object value) { } public void Format(object value, FluentAssertions.Formatting.FormattedObjectGraph formattedGraph, FluentAssertions.Formatting.FormattingContext context, FluentAssertions.Formatting.FormatChild formatChild) { } } -} +} \ No newline at end of file diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt index 63d5985ea5..2556d6d3a5 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt @@ -37,6 +37,7 @@ namespace FluentAssertions public static FluentAssertions.Specialized.MemberExecutionTime ExecutionTimeOf(this T subject, System.Linq.Expressions.Expression> action, FluentAssertions.Common.StartTimer createTimer = null) { } public static System.Action Invoking(this T subject, System.Action action) { } public static System.Func Invoking(this T subject, System.Func action) { } + public static FluentAssertions.Events.IMonitor Monitor(this T eventSource, System.Action configureOptions) { } public static FluentAssertions.Events.IMonitor Monitor(this T eventSource, System.Func utcNow = null) { } public static FluentAssertions.Specialized.ExecutionTimeAssertions Should(this FluentAssertions.Specialized.ExecutionTime executionTime) { } [System.Obsolete("You are asserting the \'AndConstraint\' itself. Remove the \'Should()\' method direct" + @@ -1272,6 +1273,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, System.Collections.IEnumerable { System.Type EventHandlerType { get; } @@ -2803,4 +2810,4 @@ namespace FluentAssertions.Xml public bool CanHandle(object value) { } public void Format(object value, FluentAssertions.Formatting.FormattedObjectGraph formattedGraph, FluentAssertions.Formatting.FormattingContext context, FluentAssertions.Formatting.FormatChild formatChild) { } } -} +} \ No newline at end of file diff --git a/Tests/FluentAssertions.Specs/Events/EventAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Events/EventAssertionSpecs.cs index c211c9f0b8..f473e0263b 100644 --- a/Tests/FluentAssertions.Specs/Events/EventAssertionSpecs.cs +++ b/Tests/FluentAssertions.Specs/Events/EventAssertionSpecs.cs @@ -1,6 +1,8 @@ using System; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Reflection; using FluentAssertions.Events; using FluentAssertions.Execution; using FluentAssertions.Extensions; @@ -8,7 +10,6 @@ using Xunit; using Xunit.Sdk; #if NETFRAMEWORK -using System.Reflection; using System.Reflection.Emit; #endif @@ -971,6 +972,148 @@ public void One_matching_argument_type_with_two_or_more_parameters_matching_a_mi } } + public class MonitorDefaultBehavior + { + [Fact] + public void Broken_event_add_accessors_fails() + { + // Arrange + var sut = new TestEventBrokenEventHandlerRaising(); + + // Act / Assert + sut.Invoking(c => + { + using var monitor = c.Monitor(); + }).Should().Throw(); + } + + [Fact] + public void Broken_event_remove_accessors_fails() + { + // Arrange + var sut = new TestEventBrokenEventHandlerRaising(); + + // Act / Assert + sut.Invoking(c => + { + using var monitor = c.Monitor(); + }).Should().Throw(); + } + } + + public class IgnoreMisbehavingEventAccessors + { + [Fact] + public void Monitoring_class_with_broken_event_add_accessor_succeeds() + { + // Arrange + var classToMonitor = new TestEventBrokenEventHandlerRaising(); + + // Act / Assert + classToMonitor.Invoking(c => + { + using var monitor = c.Monitor(opt => opt.IgnoreEventAccessorExceptions()); + }).Should().NotThrow(); + } + + [Fact] + public void Class_with_broken_event_remove_accessor_succeeds() + { + // Arrange + var classToMonitor = new TestEventBrokenEventHandlerRaising(); + + // Act / Assert + classToMonitor.Invoking(c => + { + using var monitor = c.Monitor(opt => opt.IgnoreEventAccessorExceptions()); + }).Should().NotThrow(); + } + + [Fact] + public void Recording_event_with_broken_add_accessor_succeeds() + { + // Arrange + var classToMonitor = new TestEventBrokenEventHandlerRaising(); + + using var monitor = classToMonitor.Monitor(opt => opt.IgnoreEventAccessorExceptions().RecordEventsWithBrokenAccessor()); + + //Act + classToMonitor.RaiseOkEvent(); + + //Assert + monitor.MonitoredEvents.Should().HaveCount(1); + } + + [Fact] + public void Ignoring_broken_event_accessor_should_also_not_record_events() + { + // Arrange + var classToMonitor = new TestEventBrokenEventHandlerRaising(); + + using var monitor = classToMonitor.Monitor(opt => opt.IgnoreEventAccessorExceptions()); + + //Act + classToMonitor.RaiseOkEvent(); + + //Assert + monitor.MonitoredEvents.Should().BeEmpty(); + } + } + + private interface IAddOkEvent + { + event EventHandler OkEvent; + } + + private interface IAddFailingRecordableEvent + { + public event EventHandler AddFailingRecorableEvent; + } + + private interface IAddFailingEvent + { + public event EventHandler AddFailingEvent; + } + + private interface IRemoveFailingEvent + { + public event EventHandler RemoveFailingEvent; + } + + [SuppressMessage("Usage", "CA1801:Check Unused Parameter", Justification = "This is on purpose for testing.")] + private class TestEventBrokenEventHandlerRaising : IAddFailingEvent, IRemoveFailingEvent, IAddOkEvent, IAddFailingRecordableEvent + { + public event EventHandler AddFailingEvent + { + add => throw new InvalidOperationException("Add is failing"); + remove => OkEvent -= value; + } + + public event EventHandler AddFailingRecorableEvent + { + add + { + OkEvent += value; + throw new InvalidOperationException("Add is failing"); + } + + remove => OkEvent -= value; + } + + public event EventHandler OkEvent; + + public event EventHandler RemoveFailingEvent + { + add => OkEvent += value; + remove => throw new InvalidOperationException("Remove is failing"); + } + + public void RaiseOkEvent() + { + OkEvent?.Invoke(this, EventArgs.Empty); + } + } + public class A { #pragma warning disable MA0046 diff --git a/docs/_pages/eventmonitoring.md b/docs/_pages/eventmonitoring.md index 06c462633b..36ee9fbf53 100644 --- a/docs/_pages/eventmonitoring.md +++ b/docs/_pages/eventmonitoring.md @@ -16,10 +16,16 @@ using var monitoredSubject = subject.Monitor(); subject.Foo(); monitoredSubject.Should().Raise("NameChangedEvent"); + +// Or... + +monitoredSubject.Should().NotRaise("SomeOtherEvent"); ``` Notice that Fluent Assertions will keep monitoring the `subject` for as long as the `using` block lasts. +## Filtering asserted events + Assuming that we’re dealing with an MVVM implementation, you might want to verify that it raised its `PropertyChanged` event for a particular property: ```csharp @@ -34,23 +40,22 @@ monitoredSubject This means that event monitoring only works for events that comply with the standard two-argument `sender`/`args` .NET pattern. + +## Monitoring PropertyChanged event + Since verifying for `PropertyChanged` events is so common, we've included a specialized shortcut to the example above: ```csharp -subject.Should().RaisePropertyChangeFor(x => x.SomeProperty); +monitoredSubject.Should().RaisePropertyChangeFor(x => x.SomeProperty); ``` You can also do the opposite; asserting that a particular event was not raised. ```csharp -subject.Should().NotRaisePropertyChangeFor(x => x.SomeProperty); +monitoredSubject.Should().NotRaisePropertyChangeFor(x => x.SomeProperty); ``` -Or... - -```csharp -subject.Should().NotRaise("SomeOtherEvent"); -``` +## Monitoring subset of events `Monitor()` is a generic method, but you will usually have the compiler infer the type. You _can_ specify an explicit type to limit which events you want to listen to: @@ -59,6 +64,8 @@ var subject = new ClassWithManyEvents(); using var monitor = subject.Monitor(); ``` +## Dynamically generated classes + This generic version of `Monitor()` is also very useful if you wish to monitor events of a dynamically generated class using `System.Reflection.Emit`. Since events are dynamically generated and are not present in parent class non-generic version of `Monitor()` will not find the events. This way you can tell the event monitor which interface was implemented in the generated class. ```csharp @@ -68,7 +75,6 @@ using var monitor = subject.Monitor(); // POCO class doesn't have INotifyPropertyChanged implemented monitor.Should().Raise("SomeEvent"); - ``` The `IMonitor` interface returned by `Monitor()` exposes a method named `GetRecordingFor` as well as the properties `MonitoredEvents` and `OccurredEvents` that you can use to directly interact with the monitor, e.g. to create your own extensions. For example: @@ -90,6 +96,28 @@ metadata.Should().BeEquivalentTo(new[] }); ``` +## Configuring the event monitor + +It is also possible to configure the event monitor. To do so, use the overload: + +```csharp +var subject = new EditCustomerViewModel(); +using var monitoredSubject = subject.Monitor(options => ...); +``` + +### Ignore broken event Accessors + +If some event accessors, you are not interested in, should throw exceptions, you can simply ignore all exceptions thrown by those event accessors: + +```csharp +var subject = new EditCustomerViewModel(); +using var monitoredSubject = subject.Monitor(options => options.IgnoreEventAccessorExceptions()); +``` + +The event monitor will now ignore all events that throw exceptions when subscribing (`add`) or unsubscribing (`remove`) them. + +> Note, that if there is an exception in the `add` event accessor, the event cannot be asserted, as it cannot be subscribed successfully. + ## Limitations This feature is not available in .NET Standard 2.0, because [`System.Reflection.Emit.DynamicMethod`](https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.dynamicmethod) is required to generate event handlers dynamically. diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index 11faf69f04..e03be84932 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -7,31 +7,13 @@ sidebar: nav: "sidebar" --- -## 7.0 Alpha 1 +## Unreleased ### What's new - -### Improvements - -### Fixes - -### Breaking Changes (for users) - -### Breaking Changes (for extensions) - -## 6.12.0 - -### What's new -* Added `Be`, `NotBe` and `BeOneOf` for object comparisons with custom comparer - [#2111](https://github.com/fluentassertions/fluentassertions/pull/2111) -* Added `BeSignedWithPublicKey()` and `BeUnsigned()` for assertions on `Assembly` - [#2207](https://github.com/fluentassertions/fluentassertions/pull/2207) -* Added `NotContainItemsAssignableTo` for asserting that a collection does not contain any items assignable to a specific type - [#2266](https://github.com/fluentassertions/fluentassertions/pull/2266) +* Added option for event monitoring to ignore failing event accessors - [#1954](https://github.com/fluentassertions/fluentassertions/pull/1954) ### Fixes * `because` and `becauseArgs` were not included in the error message when collections of enums were not equivalent - [#2214](https://github.com/fluentassertions/fluentassertions/pull/2214) -* Improve caller identification for tests written in Visual Basic - [#2254](https://github.com/fluentassertions/fluentassertions/pull/2254) -* Improved auto conversion to enums for objects of different integral type - [#2261](https://github.com/fluentassertions/fluentassertions/pull/2261) -* Fixed exceptions when trying to auto convert strings or enums of different type to enums- [#2261](https://github.com/fluentassertions/fluentassertions/pull/2261) -* Format records and anonymous objects with their member values instead of the generated `ToString` - [#2144](https://github.com/fluentassertions/fluentassertions/pull/2144) ## 6.11.0 @@ -46,6 +28,7 @@ sidebar: * The maximum depth `BeEquivalentTo` uses for recursive comparisons was 9 instead of the expected 10 - [#2145](https://github.com/fluentassertions/fluentassertions/pull/2145) * Fixed `.Excluding()` and `.For().Exclude()` not working if root is a collection - [#2135](https://github.com/fluentassertions/fluentassertions/pull/2135) * Prevent `InvalidOperationException` when formatting a lambda expression calling a constructor - [#2176](https://github.com/fluentassertions/fluentassertions/pull/2176) +* Format records and anonymous objects with their member values instead of the generated `ToString` - [#2144](https://github.com/fluentassertions/fluentassertions/pull/2144) ## 6.10.0 @@ -302,20 +285,20 @@ sidebar: * Dropped support for .NET Framework 4.5, .NET Standard 1.3 and 1.6 - [#1227](https://github.com/fluentassertions/fluentassertions/pull/1227). * Dropped support for older test frameworks such as MSTest v1, NSpec v1 and v2, XUnit v1, Gallio and MBUnit - [#1227](https://github.com/fluentassertions/fluentassertions/pull/1227). * Removed `[Not]Have{Im,Ex}plictConversionOperator` (they had typos) - [#1221](https://github.com/fluentassertions/fluentassertions/pull/1221). - * Use the equivalent assertions without the typo "plict" instead. + * Use the equivalent assertions without the typo "plict" instead. * Removed `NotBeAscendingInOrder`/`NotBeDescendingInOrder` - [#1221](https://github.com/fluentassertions/fluentassertions/pull/1221). - * Use `NotBeInAscendingOrder`/`NotBeInDescendingOrder` instead. + * Use `NotBeInAscendingOrder`/`NotBeInDescendingOrder` instead. * Removed `HasAttribute`, `HasMatchingAttribute` and `IsDecoratedWith(Type, bool)` `Type` extensions - [#1221](https://github.com/fluentassertions/fluentassertions/pull/1221). - * Use `IsDecoratedWith`/`IsDecoratedWithOrInherits` instead. + * Use `IsDecoratedWith`/`IsDecoratedWithOrInherits` instead. * Made `EquivalencyAssertionOptionsExtentions` `internal` (and fixed a typo in the type name) - [#1221](https://github.com/fluentassertions/fluentassertions/pull/1221). * Changed `ReferenceTypeAssertions.Subject` to be `readonly` - [#1229](https://github.com/fluentassertions/fluentassertions/pull/1229). - * Set the `Subject` through the constructor instead. + * Set the `Subject` through the constructor instead. * Changed `TypeAssertions.HaveAccessModifier` return type from `AndConstraint` to `AndConstraint` - [#1159](https://github.com/fluentassertions/fluentassertions/pull/1159). * Changed `TypeAssertions.NotHaveAccessModifier` return type from `AndConstraint` to `AndConstraint` - [#1159](https://github.com/fluentassertions/fluentassertions/pull/1159). * Changed `AllBeAssignableTo` and `AllBeOfType` return type from `AndConstraint` to `AndWhichConstraint>` - [#1265](https://github.com/fluentassertions/fluentassertions/pull/1265). * The new extension on `TaskCompletionSource` overlays the previously used assertions based on `ObjectAssertions`. * Removed `[Not]BeCloseTo` for `DateTime[Offset]` and `TimeSpan` that took an `int precision` - [#1278](https://github.com/fluentassertions/fluentassertions/pull/1278). - * Use the overloads that take a `TimeSpan precision` instead. + * Use the overloads that take a `TimeSpan precision` instead. * Aligned strings to be compared using `Ordinal[Ignorecase]` - [#1283](https://github.com/fluentassertions/fluentassertions/pull/1283). * Changed `AutoConversion` to convert using `CultureInfo.InvariantCulture` instead of `CultureInfo.CurrentCulture` - [#1283](https://github.com/fluentassertions/fluentassertions/pull/1283). * Renamed `StartWithEquivalent` and `EndWithEquivalent` to `StartWithEquivalentOf` and `EndWithEquivalentOf` to make the API for Equivalent methods consistent - [#1292](https://github.com/fluentassertions/fluentassertions/pull/1292). @@ -343,7 +326,7 @@ sidebar: ### Breaking Changes (Extensibility) * Removed parameterless constructors from: `CollectionAssertions`, `ReferenceTypeAssertions`, `MemberInfoAssertions`, `MethodBaseAssertions` and `MethodInfoAssertions` - [#1229](https://github.com/fluentassertions/fluentassertions/pull/1229). - * Use the constructors taking a `subject` instead. + * Use the constructors taking a `subject` instead. * Restrict generic constraints on `[Nullable]NumericAssertions` to `IComparable` - [#1266](https://github.com/fluentassertions/fluentassertions/pull/1266). * Changed return type of `[Nullable]NumericAssertions.Subject` from `IComparable` to `T?` and `T`, respectively - [#1266](https://github.com/fluentassertions/fluentassertions/pull/1266). * Removed `Succeeded` and `SourceSucceeded` from `Continuation` and `IAssertionScope` - [#1325](https://github.com/fluentassertions/fluentassertions/pull/1325)