From ce655eea20aa084aba2820970a1dd13c0d0ae9a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 4 Apr 2026 10:01:00 +0200 Subject: [PATCH 1/2] feat: add event setup with callbacks for subscribe/unsubscribe --- .../Sources/Sources.MockClass.cs | 37 +++ Source/Mockolate/MockRegistry.Interactions.cs | 8 + Source/Mockolate/MockRegistry.Setup.cs | 6 + Source/Mockolate/Setup/EventSetup.cs | 183 +++++++++++++ .../Mockolate/Setup/Interfaces.EventSetup.cs | 101 +++++++ Source/Mockolate/Setup/MockSetups.Events.cs | 65 +++++ Source/Mockolate/Setup/MockSetups.cs | 8 +- Source/Mockolate/SetupExtensions.cs | 18 ++ .../Expected/Mockolate_net10.0.txt | 40 +++ .../Expected/Mockolate_net8.0.txt | 40 +++ .../Expected/Mockolate_netstandard2.0.txt | 40 +++ .../MockSetupsTests.cs | 35 ++- .../MockTests.ClassTests.EventsTests.cs | 45 ++++ .../MockEvents/SetupEventTests.cs | 248 ++++++++++++++++++ 14 files changed, 864 insertions(+), 10 deletions(-) create mode 100644 Source/Mockolate/Setup/EventSetup.cs create mode 100644 Source/Mockolate/Setup/Interfaces.EventSetup.cs create mode 100644 Source/Mockolate/Setup/MockSetups.Events.cs create mode 100644 Tests/Mockolate.Tests/MockEvents/SetupEventTests.cs diff --git a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs index 1ad3a3fd..34a29cdb 100644 --- a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs +++ b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs @@ -1772,6 +1772,19 @@ private static void DefineSetupInterface(StringBuilder sb, Class @class, MemberT #endregion + #region Events + + Func eventPredicate = @event + => @event.ExplicitImplementation is null && @event.MemberType == memberType; + foreach (Event @event in @class.AllEvents().Where(eventPredicate)) + { + sb.AppendXmlSummary($"Setup for the event ."); + sb.Append("\t\tglobal::Mockolate.Setup.EventSetup ").Append(@event.Name).Append(" { get; }").AppendLine(); + sb.AppendLine(); + } + + #endregion + #region Indexers Func indexerPredicate = @@ -2024,6 +2037,30 @@ private static void ImplementSetupInterface(StringBuilder sb, Class @class, stri #endregion + #region Events + + Func eventSetupPredicate = @event + => @event.ExplicitImplementation is null && @event.MemberType == memberType; + foreach (Event @event in @class.AllEvents().Where(eventSetupPredicate)) + { + sb.Append("\t\t/// ").AppendLine(); + sb.Append("\t\t[global::System.Diagnostics.DebuggerBrowsable(global::System.Diagnostics.DebuggerBrowsableState.Never)]").AppendLine(); + sb.Append("\t\tglobal::Mockolate.Setup.EventSetup global::Mockolate.Mock.") + .Append(setupName).Append('.').Append(@event.Name).AppendLine(); + sb.Append("\t\t{").AppendLine(); + sb.Append("\t\t\tget").AppendLine(); + sb.Append("\t\t\t{").AppendLine(); + sb.Append("\t\t\t\tglobal::Mockolate.Setup.EventSetup eventSetup = new global::Mockolate.Setup.EventSetup(") + .Append(@event.GetUniqueNameString()).Append(");").AppendLine(); + sb.Append("\t\t\t\tthis.").Append(mockRegistryName).Append(".SetupEvent(eventSetup);").AppendLine(); + sb.Append("\t\t\t\treturn eventSetup;").AppendLine(); + sb.Append("\t\t\t}").AppendLine(); + sb.Append("\t\t}").AppendLine(); + sb.AppendLine(); + } + + #endregion + #region Indexers Func indexerPredicate = diff --git a/Source/Mockolate/MockRegistry.Interactions.cs b/Source/Mockolate/MockRegistry.Interactions.cs index 859ccd28..3edc076c 100644 --- a/Source/Mockolate/MockRegistry.Interactions.cs +++ b/Source/Mockolate/MockRegistry.Interactions.cs @@ -206,6 +206,10 @@ public void AddEvent(string name, object? target, MethodInfo? method) } ((IMockInteractions)Interactions).RegisterInteraction(new EventSubscription(name, target, method)); + foreach (EventSetup setup in Setup.Events.GetByName(name)) + { + setup.InvokeSubscribed(target, method); + } } /// @@ -220,5 +224,9 @@ public void RemoveEvent(string name, object? target, MethodInfo? method) } ((IMockInteractions)Interactions).RegisterInteraction(new EventUnsubscription(name, target, method)); + foreach (EventSetup setup in Setup.Events.GetByName(name)) + { + setup.InvokeUnsubscribed(target, method); + } } } diff --git a/Source/Mockolate/MockRegistry.Setup.cs b/Source/Mockolate/MockRegistry.Setup.cs index a4dc1082..a6a07c86 100644 --- a/Source/Mockolate/MockRegistry.Setup.cs +++ b/Source/Mockolate/MockRegistry.Setup.cs @@ -50,4 +50,10 @@ private TValue GetIndexerValue(IInteractiveIndexerSetup? setup, Func public void SetupProperty(PropertySetup propertySetup) => Setup.Properties.Add(propertySetup); + + /// + /// Registers the in the mock. + /// + public void SetupEvent(EventSetup eventSetup) + => Setup.Events.Add(eventSetup); } diff --git a/Source/Mockolate/Setup/EventSetup.cs b/Source/Mockolate/Setup/EventSetup.cs new file mode 100644 index 00000000..c09dc01c --- /dev/null +++ b/Source/Mockolate/Setup/EventSetup.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using Mockolate.Internals; + +namespace Mockolate.Setup; + +/// +/// Sets up event subscription and unsubscription callbacks. +/// +[DebuggerDisplay("{ToString()}")] +[DebuggerNonUserCode] +public class EventSetup(string name) : IEventSetup, + IEventSubscriptionSetup, IEventUnsubscriptionSetup, + IEventSetupCallbackBuilder, IEventSetupCallbackWhenBuilder +{ + private Callback? _currentCallback; + private int _currentSubscribedCallbacksIndex; + private int _currentUnsubscribedCallbacksIndex; + private List>>? _subscribedCallbacks; + private List>>? _unsubscribedCallbacks; + + /// + /// The fully-qualified name of the event. + /// + public string Name => name; + + /// + public IEventSubscriptionSetup OnSubscribed => this; + + /// + public IEventUnsubscriptionSetup OnUnsubscribed => this; + + /// + IEventSetupCallbackWhenBuilder IEventSetupCallbackBuilder.When(Func predicate) + { + _currentCallback?.When(predicate); + return this; + } + + /// + IEventSetupCallbackWhenBuilder IEventSetupCallbackBuilder.InParallel() + { + _currentCallback?.InParallel(); + return this; + } + + /// + IEventSetupCallbackWhenBuilder IEventSetupCallbackWhenBuilder.For(int times) + { + _currentCallback?.For(times); + return this; + } + + /// + IEventSetup IEventSetupCallbackWhenBuilder.Only(int times) + { + _currentCallback?.Only(times); + return this; + } + + /// + IEventSetupCallbackBuilder IEventSubscriptionSetup.Do(Action callback) + { + Callback> item = new(Delegate); + _currentCallback = item; + (_subscribedCallbacks ??= []).Add(item); + return this; + + [DebuggerNonUserCode] + void Delegate(int _, object? _target, MethodInfo _method) + { + callback(); + } + } + + /// + IEventSetupCallbackBuilder IEventSubscriptionSetup.Do(Action callback) + { + Callback> item = new(Delegate); + _currentCallback = item; + (_subscribedCallbacks ??= []).Add(item); + return this; + + [DebuggerNonUserCode] + void Delegate(int _, object? target, MethodInfo method) + { + callback(target, method); + } + } + + /// + IEventSetupCallbackBuilder IEventUnsubscriptionSetup.Do(Action callback) + { + Callback> item = new(Delegate); + _currentCallback = item; + (_unsubscribedCallbacks ??= []).Add(item); + return this; + + [DebuggerNonUserCode] + void Delegate(int _, object? _target, MethodInfo _method) + { + callback(); + } + } + + /// + IEventSetupCallbackBuilder IEventUnsubscriptionSetup.Do(Action callback) + { + Callback> item = new(Delegate); + _currentCallback = item; + (_unsubscribedCallbacks ??= []).Add(item); + return this; + + [DebuggerNonUserCode] + void Delegate(int _, object? target, MethodInfo method) + { + callback(target, method); + } + } + + /// + /// Invokes all registered subscription callbacks. + /// + internal void InvokeSubscribed(object? target, MethodInfo method) + { + if (_subscribedCallbacks is null) + { + return; + } + + bool wasInvoked = false; + int currentIndex = _currentSubscribedCallbacksIndex; + for (int i = 0; i < _subscribedCallbacks.Count; i++) + { + Callback> callback = + _subscribedCallbacks[(currentIndex + i) % _subscribedCallbacks.Count]; + if (callback.Invoke(wasInvoked, ref _currentSubscribedCallbacksIndex, Dispatch)) + { + wasInvoked = true; + } + } + + [DebuggerNonUserCode] + void Dispatch(int invocationCount, Action @delegate) + { + @delegate(invocationCount, target, method); + } + } + + /// + /// Invokes all registered unsubscription callbacks. + /// + internal void InvokeUnsubscribed(object? target, MethodInfo method) + { + if (_unsubscribedCallbacks is null) + { + return; + } + + bool wasInvoked = false; + int currentIndex = _currentUnsubscribedCallbacksIndex; + for (int i = 0; i < _unsubscribedCallbacks.Count; i++) + { + Callback> callback = + _unsubscribedCallbacks[(currentIndex + i) % _unsubscribedCallbacks.Count]; + if (callback.Invoke(wasInvoked, ref _currentUnsubscribedCallbacksIndex, Dispatch)) + { + wasInvoked = true; + } + } + + [DebuggerNonUserCode] + void Dispatch(int invocationCount, Action @delegate) + { + @delegate(invocationCount, target, method); + } + } + + /// + public override string ToString() => name.SubstringAfterLast('.'); +} diff --git a/Source/Mockolate/Setup/Interfaces.EventSetup.cs b/Source/Mockolate/Setup/Interfaces.EventSetup.cs new file mode 100644 index 00000000..7e106d7c --- /dev/null +++ b/Source/Mockolate/Setup/Interfaces.EventSetup.cs @@ -0,0 +1,101 @@ +using System; +using System.Reflection; + +namespace Mockolate.Setup; + +/// +/// Interface for setting up an event with fluent syntax. +/// +public interface IEventSetup +{ + /// + /// Sets up callbacks on the event subscription (add accessor). + /// + IEventSubscriptionSetup OnSubscribed { get; } + + /// + /// Sets up callbacks on the event unsubscription (remove accessor). + /// + IEventUnsubscriptionSetup OnUnsubscribed { get; } +} + +/// +/// Interface for setting up an event subscription with fluent syntax. +/// +public interface IEventSubscriptionSetup +{ + /// + /// Registers a callback to be invoked whenever a handler is subscribed to the event. + /// + IEventSetupCallbackBuilder Do(Action callback); + + /// + /// Registers a callback to be invoked whenever a handler is subscribed to the event. + /// + /// + /// The callback receives the target object and method of the subscribed handler. + /// + IEventSetupCallbackBuilder Do(Action callback); +} + +/// +/// Interface for setting up an event unsubscription with fluent syntax. +/// +public interface IEventUnsubscriptionSetup +{ + /// + /// Registers a callback to be invoked whenever a handler is unsubscribed from the event. + /// + IEventSetupCallbackBuilder Do(Action callback); + + /// + /// Registers a callback to be invoked whenever a handler is unsubscribed from the event. + /// + /// + /// The callback receives the target object and method of the unsubscribed handler. + /// + IEventSetupCallbackBuilder Do(Action callback); +} + +/// +/// Interface for setting up an event with fluent syntax. +/// +public interface IEventSetupCallbackBuilder : IEventSetupCallbackWhenBuilder +{ + /// + /// Limits the callback to only execute for event interactions where the predicate returns true. + /// + /// + /// Provides a zero-based counter indicating how many times the event has been interacted with so far. + /// + IEventSetupCallbackWhenBuilder When(Func predicate); + + /// + /// Runs the callback in parallel to the other callbacks. + /// + IEventSetupCallbackWhenBuilder InParallel(); +} + +/// +/// Interface for setting up an event with fluent syntax. +/// +public interface IEventSetupCallbackWhenBuilder : IEventSetup +{ + /// + /// Repeats the callback for the given number of . + /// + /// + /// The number of times is only counted for actual executions ( + /// evaluates to ). + /// + IEventSetupCallbackWhenBuilder For(int times); + + /// + /// Deactivates the callback after the given number of . + /// + /// + /// The number of times is only counted for actual executions ( + /// evaluates to ). + /// + IEventSetup Only(int times); +} diff --git a/Source/Mockolate/Setup/MockSetups.Events.cs b/Source/Mockolate/Setup/MockSetups.Events.cs new file mode 100644 index 00000000..a94e5c89 --- /dev/null +++ b/Source/Mockolate/Setup/MockSetups.Events.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; + +namespace Mockolate.Setup; + +internal partial class MockSetups +{ + internal EventSetups Events { get; } = new(); + + [DebuggerDisplay("{ToString()}")] + [DebuggerNonUserCode] + internal sealed class EventSetups + { + private int _count; + private List? _storage; + + // ReSharper disable once InconsistentlySynchronizedField + public int Count => Volatile.Read(ref _count); + + private List GetOrCreateStorage() + { + if (_storage is null) + { + Interlocked.CompareExchange(ref _storage, [], null); + } + + return _storage!; + } + + public void Add(EventSetup setup) + { + List storage = GetOrCreateStorage(); + lock (storage) + { + storage.Add(setup); + Volatile.Write(ref _count, _count + 1); + } + } + + public List GetByName(string name) + { + List result = []; + if (_storage is null) + { + return result; + } + + List storage = _storage; + lock (storage) + { + foreach (EventSetup setup in storage) + { + if (string.Equals(setup.Name, name, StringComparison.Ordinal)) + { + result.Add(setup); + } + } + } + + return result; + } + } +} diff --git a/Source/Mockolate/Setup/MockSetups.cs b/Source/Mockolate/Setup/MockSetups.cs index 809abf63..2cdb94d9 100644 --- a/Source/Mockolate/Setup/MockSetups.cs +++ b/Source/Mockolate/Setup/MockSetups.cs @@ -30,7 +30,13 @@ public override string ToString() { sb.Append(indexerCount).Append(indexerCount == 1 ? " indexer, " : " indexers, "); } - + + int eventCount = Events.Count; + if (eventCount > 0) + { + sb.Append(eventCount).Append(eventCount == 1 ? " event, " : " events, "); + } + if (sb.Length == 0) { return "no setups"; diff --git a/Source/Mockolate/SetupExtensions.cs b/Source/Mockolate/SetupExtensions.cs index d0a2801c..8092cbc0 100644 --- a/Source/Mockolate/SetupExtensions.cs +++ b/Source/Mockolate/SetupExtensions.cs @@ -39,6 +39,24 @@ public IPropertySetup OnlyOnce() => setup.Only(1); } + /// + /// Extensions for event callback setups. + /// + extension(IEventSetupCallbackWhenBuilder setup) + { + /// + /// Repeats the callback forever. + /// + public void Forever() + => setup.For(int.MaxValue); + + /// + /// Executes the callback only once. + /// + public IEventSetup OnlyOnce() + => setup.Only(1); + } + /// /// Extensions for indexer setups with one parameter. /// diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt index 373f8c52..01c5c239 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt @@ -188,6 +188,7 @@ namespace Mockolate public void RemoveEvent(string name, object? target, System.Reflection.MethodInfo? method) { } public bool SetIndexer(TResult value, params Mockolate.Parameters.NamedParameterValue[] parameters) { } public bool SetProperty(string propertyName, object? value) { } + public void SetupEvent(Mockolate.Setup.EventSetup eventSetup) { } public void SetupIndexer(Mockolate.Setup.IndexerSetup indexerSetup) { } public void SetupMethod(Mockolate.Setup.MethodSetup methodSetup) { } public void SetupProperty(Mockolate.Setup.PropertySetup propertySetup) { } @@ -273,6 +274,11 @@ namespace Mockolate { public Mockolate.Setup.IPropertySetup OnlyOnce() { } } + extension(Mockolate.Setup.IEventSetupCallbackWhenBuilder setup) + { + public void Forever() { } + public Mockolate.Setup.IEventSetup OnlyOnce() { } + } extension(Mockolate.Setup.IIndexerSetupReturnWhenBuilder setup) where TValue : notnull where T1 : notnull @@ -679,6 +685,40 @@ namespace Mockolate.Setup public bool Invoke(bool wasInvoked, ref int index, System.Action callback) { } public bool Invoke(ref int index, System.Func callback, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out TReturn? returnValue) { } } + [System.Diagnostics.DebuggerDisplay("{ToString()}")] + public class EventSetup : Mockolate.Setup.IEventSetup, Mockolate.Setup.IEventSetupCallbackBuilder, Mockolate.Setup.IEventSetupCallbackWhenBuilder, Mockolate.Setup.IEventSubscriptionSetup, Mockolate.Setup.IEventUnsubscriptionSetup + { + public EventSetup(string name) { } + public string Name { get; } + public Mockolate.Setup.IEventSubscriptionSetup OnSubscribed { get; } + public Mockolate.Setup.IEventUnsubscriptionSetup OnUnsubscribed { get; } + public override string ToString() { } + } + public interface IEventSetup + { + Mockolate.Setup.IEventSubscriptionSetup OnSubscribed { get; } + Mockolate.Setup.IEventUnsubscriptionSetup OnUnsubscribed { get; } + } + public interface IEventSetupCallbackBuilder : Mockolate.Setup.IEventSetup, Mockolate.Setup.IEventSetupCallbackWhenBuilder + { + Mockolate.Setup.IEventSetupCallbackWhenBuilder InParallel(); + Mockolate.Setup.IEventSetupCallbackWhenBuilder When(System.Func predicate); + } + public interface IEventSetupCallbackWhenBuilder : Mockolate.Setup.IEventSetup + { + Mockolate.Setup.IEventSetupCallbackWhenBuilder For(int times); + Mockolate.Setup.IEventSetup Only(int times); + } + public interface IEventSubscriptionSetup + { + Mockolate.Setup.IEventSetupCallbackBuilder Do(System.Action callback); + Mockolate.Setup.IEventSetupCallbackBuilder Do(System.Action callback); + } + public interface IEventUnsubscriptionSetup + { + Mockolate.Setup.IEventSetupCallbackBuilder Do(System.Action callback); + Mockolate.Setup.IEventSetupCallbackBuilder Do(System.Action callback); + } public interface IIndexerGetterSetup { Mockolate.Setup.IIndexerSetupCallbackBuilder Do(System.Action callback); diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt index 8f9ed0c0..8986f028 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt @@ -187,6 +187,7 @@ namespace Mockolate public void RemoveEvent(string name, object? target, System.Reflection.MethodInfo? method) { } public bool SetIndexer(TResult value, params Mockolate.Parameters.NamedParameterValue[] parameters) { } public bool SetProperty(string propertyName, object? value) { } + public void SetupEvent(Mockolate.Setup.EventSetup eventSetup) { } public void SetupIndexer(Mockolate.Setup.IndexerSetup indexerSetup) { } public void SetupMethod(Mockolate.Setup.MethodSetup methodSetup) { } public void SetupProperty(Mockolate.Setup.PropertySetup propertySetup) { } @@ -272,6 +273,11 @@ namespace Mockolate { public Mockolate.Setup.IPropertySetup OnlyOnce() { } } + extension(Mockolate.Setup.IEventSetupCallbackWhenBuilder setup) + { + public void Forever() { } + public Mockolate.Setup.IEventSetup OnlyOnce() { } + } extension(Mockolate.Setup.IIndexerSetupReturnWhenBuilder setup) where TValue : notnull where T1 : notnull @@ -678,6 +684,40 @@ namespace Mockolate.Setup public bool Invoke(bool wasInvoked, ref int index, System.Action callback) { } public bool Invoke(ref int index, System.Func callback, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out TReturn? returnValue) { } } + [System.Diagnostics.DebuggerDisplay("{ToString()}")] + public class EventSetup : Mockolate.Setup.IEventSetup, Mockolate.Setup.IEventSetupCallbackBuilder, Mockolate.Setup.IEventSetupCallbackWhenBuilder, Mockolate.Setup.IEventSubscriptionSetup, Mockolate.Setup.IEventUnsubscriptionSetup + { + public EventSetup(string name) { } + public string Name { get; } + public Mockolate.Setup.IEventSubscriptionSetup OnSubscribed { get; } + public Mockolate.Setup.IEventUnsubscriptionSetup OnUnsubscribed { get; } + public override string ToString() { } + } + public interface IEventSetup + { + Mockolate.Setup.IEventSubscriptionSetup OnSubscribed { get; } + Mockolate.Setup.IEventUnsubscriptionSetup OnUnsubscribed { get; } + } + public interface IEventSetupCallbackBuilder : Mockolate.Setup.IEventSetup, Mockolate.Setup.IEventSetupCallbackWhenBuilder + { + Mockolate.Setup.IEventSetupCallbackWhenBuilder InParallel(); + Mockolate.Setup.IEventSetupCallbackWhenBuilder When(System.Func predicate); + } + public interface IEventSetupCallbackWhenBuilder : Mockolate.Setup.IEventSetup + { + Mockolate.Setup.IEventSetupCallbackWhenBuilder For(int times); + Mockolate.Setup.IEventSetup Only(int times); + } + public interface IEventSubscriptionSetup + { + Mockolate.Setup.IEventSetupCallbackBuilder Do(System.Action callback); + Mockolate.Setup.IEventSetupCallbackBuilder Do(System.Action callback); + } + public interface IEventUnsubscriptionSetup + { + Mockolate.Setup.IEventSetupCallbackBuilder Do(System.Action callback); + Mockolate.Setup.IEventSetupCallbackBuilder Do(System.Action callback); + } public interface IIndexerGetterSetup { Mockolate.Setup.IIndexerSetupCallbackBuilder Do(System.Action callback); diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt index 33c3a009..8bdc0d25 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt @@ -174,6 +174,7 @@ namespace Mockolate public void RemoveEvent(string name, object? target, System.Reflection.MethodInfo? method) { } public bool SetIndexer(TResult value, params Mockolate.Parameters.NamedParameterValue[] parameters) { } public bool SetProperty(string propertyName, object? value) { } + public void SetupEvent(Mockolate.Setup.EventSetup eventSetup) { } public void SetupIndexer(Mockolate.Setup.IndexerSetup indexerSetup) { } public void SetupMethod(Mockolate.Setup.MethodSetup methodSetup) { } public void SetupProperty(Mockolate.Setup.PropertySetup propertySetup) { } @@ -231,6 +232,11 @@ namespace Mockolate { public Mockolate.Setup.IPropertySetup OnlyOnce() { } } + extension(Mockolate.Setup.IEventSetupCallbackWhenBuilder setup) + { + public void Forever() { } + public Mockolate.Setup.IEventSetup OnlyOnce() { } + } extension(Mockolate.Setup.IIndexerSetupReturnWhenBuilder setup) where TValue : notnull where T1 : notnull @@ -633,6 +639,40 @@ namespace Mockolate.Setup public bool Invoke(bool wasInvoked, ref int index, System.Action callback) { } public bool Invoke(ref int index, System.Func callback, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out TReturn? returnValue) { } } + [System.Diagnostics.DebuggerDisplay("{ToString()}")] + public class EventSetup : Mockolate.Setup.IEventSetup, Mockolate.Setup.IEventSetupCallbackBuilder, Mockolate.Setup.IEventSetupCallbackWhenBuilder, Mockolate.Setup.IEventSubscriptionSetup, Mockolate.Setup.IEventUnsubscriptionSetup + { + public EventSetup(string name) { } + public string Name { get; } + public Mockolate.Setup.IEventSubscriptionSetup OnSubscribed { get; } + public Mockolate.Setup.IEventUnsubscriptionSetup OnUnsubscribed { get; } + public override string ToString() { } + } + public interface IEventSetup + { + Mockolate.Setup.IEventSubscriptionSetup OnSubscribed { get; } + Mockolate.Setup.IEventUnsubscriptionSetup OnUnsubscribed { get; } + } + public interface IEventSetupCallbackBuilder : Mockolate.Setup.IEventSetup, Mockolate.Setup.IEventSetupCallbackWhenBuilder + { + Mockolate.Setup.IEventSetupCallbackWhenBuilder InParallel(); + Mockolate.Setup.IEventSetupCallbackWhenBuilder When(System.Func predicate); + } + public interface IEventSetupCallbackWhenBuilder : Mockolate.Setup.IEventSetup + { + Mockolate.Setup.IEventSetupCallbackWhenBuilder For(int times); + Mockolate.Setup.IEventSetup Only(int times); + } + public interface IEventSubscriptionSetup + { + Mockolate.Setup.IEventSetupCallbackBuilder Do(System.Action callback); + Mockolate.Setup.IEventSetupCallbackBuilder Do(System.Action callback); + } + public interface IEventUnsubscriptionSetup + { + Mockolate.Setup.IEventSetupCallbackBuilder Do(System.Action callback); + Mockolate.Setup.IEventSetupCallbackBuilder Do(System.Action callback); + } public interface IIndexerGetterSetup { Mockolate.Setup.IIndexerSetupCallbackBuilder Do(System.Action callback); diff --git a/Tests/Mockolate.Internal.Tests/MockSetupsTests.cs b/Tests/Mockolate.Internal.Tests/MockSetupsTests.cs index dfae763c..6b9d05e5 100644 --- a/Tests/Mockolate.Internal.Tests/MockSetupsTests.cs +++ b/Tests/Mockolate.Internal.Tests/MockSetupsTests.cs @@ -6,17 +6,29 @@ namespace Mockolate.Internal.Tests; public partial class MockSetupsTests { + [Fact] + public async Task EventSetup_ToString_ShouldReturnEventName() + { + EventSetup setup = new("global::MyCode.IMyService.SomeEvent"); + + string result = setup.ToString(); + + await That(result).IsEqualTo("SomeEvent"); + } + [Theory] - [InlineData(0, 0, 0, "no setups")] - [InlineData(1, 0, 0, "1 method")] - [InlineData(2, 0, 0, "2 methods")] - [InlineData(0, 1, 0, "1 property")] - [InlineData(0, 2, 0, "2 properties")] - [InlineData(0, 0, 1, "1 indexer")] - [InlineData(0, 0, 2, "2 indexers")] - [InlineData(3, 5, 2, "3 methods, 5 properties, 2 indexers")] + [InlineData(0, 0, 0, 0, "no setups")] + [InlineData(1, 0, 0, 0, "1 method")] + [InlineData(2, 0, 0, 0, "2 methods")] + [InlineData(0, 1, 0, 0, "1 property")] + [InlineData(0, 2, 0, 0, "2 properties")] + [InlineData(0, 0, 1, 0, "1 indexer")] + [InlineData(0, 0, 2, 0, "2 indexers")] + [InlineData(0, 0, 0, 1, "1 event")] + [InlineData(0, 0, 0, 2, "2 events")] + [InlineData(3, 5, 2, 1, "3 methods, 5 properties, 2 indexers, 1 event")] public async Task ToString_ShouldReturnExpectedValue( - int methodCount, int propertyCount, int indexerCount, string expected) + int methodCount, int propertyCount, int indexerCount, int eventCount, string expected) { IMyService sut = IMyService.CreateMock(); IMock mock = (IMock)sut; @@ -37,6 +49,11 @@ public async Task ToString_ShouldReturnExpectedValue( new NamedParameter("index1", (IParameter)It.IsAny()))); } + for (int i = 0; i < eventCount; i++) + { + mock.MockRegistry.SetupEvent(new EventSetup($"my.event{i}")); + } + string result = mock.MockRegistry.Setup.ToString(); await That(result).IsEqualTo(expected); diff --git a/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.EventsTests.cs b/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.EventsTests.cs index a690298c..f1cd3a23 100644 --- a/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.EventsTests.cs +++ b/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.EventsTests.cs @@ -45,6 +45,51 @@ await That(result.Sources).ContainsKey("Mock.IMyService__IMyServiceBase1__IMySer .Contains("event global::System.EventHandler? global::MyCode.IMyServiceBase2.SomeEvent").Once(); } + [Fact] + public async Task ShouldGenerateEventSetupInSetupInterface() + { + GeneratorResult result = Generator + .Run(""" + using System; + using Mockolate; + + namespace MyCode; + public class Program + { + public static void Main(string[] args) + { + _ = IMyService.CreateMock(); + } + } + + public interface IMyService + { + event EventHandler SomeEvent; + } + """); + + await That(result.Sources).ContainsKey("Mock.IMyService.g.cs").WhoseValue + .Contains(""" + /// + /// Setup for the event . + /// + global::Mockolate.Setup.EventSetup SomeEvent { get; } + """).IgnoringNewlineStyle().And + .Contains(""" + /// + [global::System.Diagnostics.DebuggerBrowsable(global::System.Diagnostics.DebuggerBrowsableState.Never)] + global::Mockolate.Setup.EventSetup global::Mockolate.Mock.IMockSetupForIMyService.SomeEvent + { + get + { + global::Mockolate.Setup.EventSetup eventSetup = new global::Mockolate.Setup.EventSetup("global::MyCode.IMyService.SomeEvent"); + this.MockRegistry.SetupEvent(eventSetup); + return eventSetup; + } + } + """).IgnoringNewlineStyle(); + } + [Fact] public async Task ShouldImplementAllEventsFromInterfaces() { diff --git a/Tests/Mockolate.Tests/MockEvents/SetupEventTests.cs b/Tests/Mockolate.Tests/MockEvents/SetupEventTests.cs new file mode 100644 index 00000000..28adea7d --- /dev/null +++ b/Tests/Mockolate.Tests/MockEvents/SetupEventTests.cs @@ -0,0 +1,248 @@ +using System.Collections.Generic; +using System.Reflection; +using Mockolate.Tests.TestHelpers; + +namespace Mockolate.Tests.MockEvents; + +public sealed class SetupEventTests +{ + [Fact] + public async Task Forever_Extension_RepeatsIndefinitely() + { + int callCount = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed + .OnSubscribed.Do(() => { callCount++; }).For(1).Forever(); + + for (int i = 0; i < 10; i++) + { + sut.ChocolateDispensed += Handler; + } + + await That(callCount).IsEqualTo(10); + + void Handler(string type, int amount) { } + } + + [Fact] + public async Task MultipleSetups_AreAllInvoked() + { + int callCount1 = 0; + int callCount2 = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed.OnSubscribed.Do(() => { callCount1++; }); + sut.Mock.Setup.ChocolateDispensed.OnSubscribed.Do(() => { callCount2++; }); + + sut.ChocolateDispensed += Handler; + + await That(callCount1).IsEqualTo(1); + await That(callCount2).IsEqualTo(1); + + void Handler(string type, int amount) { } + } + + [Fact] + public async Task OnlyOnce_Extension_StopsAfterSingleInvocation() + { + int callCount = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed.OnSubscribed.Do(() => { callCount++; }).OnlyOnce(); + + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed += Handler; + + await That(callCount).IsEqualTo(1); + + void Handler(string type, int amount) { } + } + + [Fact] + public async Task OnSubscribed_Do_Action_IsInvokedOnSubscribe() + { + int callCount = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed.OnSubscribed.Do(() => { callCount++; }); + + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed += Handler; + + await That(callCount).IsEqualTo(2); + + void Handler(string type, int amount) { } + } + + [Fact] + public async Task OnSubscribed_Do_TargetMethod_ReceivesCorrectValues() + { + List received = []; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed.OnSubscribed.Do((_, method) => { received.Add(method); }); + + sut.ChocolateDispensed += Handler; + + await That(received).HasCount().EqualTo(1); + await That(received[0].Name).Contains("Handler"); + + void Handler(string type, int amount) { } + } + + [Fact] + public async Task OnSubscribed_DoesNotFireOnUnsubscribe() + { + int callCount = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed.OnSubscribed.Do(() => { callCount++; }); + + void Handler(string type, int amount) { } + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed -= Handler; + + await That(callCount).IsEqualTo(1); + } + + [Fact] + public async Task OnSubscribed_For_RepeatsCallbackNTimes() + { + int count1 = 0; + int count2 = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed + .OnSubscribed.Do(() => { count1++; }).For(2) + .OnSubscribed.Do(() => { count2++; }); + + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed += Handler; + + await That(count1).IsEqualTo(2); + await That(count2).IsEqualTo(1); + + void Handler(string type, int amount) { } + } + + [Fact] + public async Task OnSubscribed_InParallel_RunsAlongsideNextCallback() + { + int count1 = 0; + int count2 = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed + .OnSubscribed.Do(() => { count1++; }).InParallel() + .OnSubscribed.Do(() => { count2++; }); + + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed += Handler; + + await That(count1).IsEqualTo(2); + await That(count2).IsEqualTo(2); + + void Handler(string type, int amount) { } + } + + [Fact] + public async Task OnSubscribed_Only_StopsAfterNInvocations() + { + int callCount = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed.OnSubscribed.Do(() => { callCount++; }).Only(2); + + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed += Handler; + + await That(callCount).IsEqualTo(2); + + void Handler(string type, int amount) { } + } + + [Fact] + public async Task OnSubscribed_When_OnlyFires_WhenPredicateMatches() + { + int callCount = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed.OnSubscribed.Do(() => { callCount++; }).When(n => n % 2 == 0); + + for (int i = 0; i < 6; i++) + { + sut.ChocolateDispensed += Handler; + } + + // subscriptions at index 0, 2, 4 pass the predicate + await That(callCount).IsEqualTo(3); + + void Handler(string type, int amount) { } + } + + [Fact] + public async Task OnUnsubscribed_Do_Action_IsInvokedOnUnsubscribe() + { + int callCount = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed.OnUnsubscribed.Do(() => { callCount++; }); + + void Handler(string type, int amount) { } + + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed -= Handler; + sut.ChocolateDispensed -= Handler; + + await That(callCount).IsEqualTo(2); + } + + [Fact] + public async Task OnUnsubscribed_Do_TargetMethod_ReceivesCorrectValues() + { + List received = []; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed.OnUnsubscribed.Do((_, method) => { received.Add(method); }); + + void Handler(string type, int amount) { } + + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed -= Handler; + + await That(received).HasCount().EqualTo(1); + await That(received[0].Name).Contains("Handler"); + } + + [Fact] + public async Task OnUnsubscribed_DoesNotFireOnSubscribe() + { + int callCount = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed.OnUnsubscribed.Do(() => { callCount++; }); + + void Handler(string type, int amount) { } + sut.ChocolateDispensed += Handler; + + await That(callCount).IsEqualTo(0); + } + + [Fact] + public async Task SetupDoesNotInterfereWithVerification() + { + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed.OnSubscribed.Do(() => { }); + + void Handler(string type, int amount) { } + sut.ChocolateDispensed += Handler; + + await That(sut.Mock.Verify.ChocolateDispensed.Subscribed()).Once(); + } +} From 62537d66127c77a87f76b3659c54f75b996676ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 4 Apr 2026 13:26:38 +0200 Subject: [PATCH 2/2] Fix review issue --- .../MockEvents/SetupEventTests.cs | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/Tests/Mockolate.Tests/MockEvents/SetupEventTests.cs b/Tests/Mockolate.Tests/MockEvents/SetupEventTests.cs index 28adea7d..8621422f 100644 --- a/Tests/Mockolate.Tests/MockEvents/SetupEventTests.cs +++ b/Tests/Mockolate.Tests/MockEvents/SetupEventTests.cs @@ -233,6 +233,120 @@ void Handler(string type, int amount) { } await That(callCount).IsEqualTo(0); } + [Fact] + public async Task OnUnsubscribed_For_RepeatsCallbackNTimes() + { + int count1 = 0; + int count2 = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed + .OnUnsubscribed.Do(() => { count1++; }).For(2) + .OnUnsubscribed.Do(() => { count2++; }); + + void Handler(string type, int amount) { } + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed -= Handler; + sut.ChocolateDispensed -= Handler; + sut.ChocolateDispensed -= Handler; + + await That(count1).IsEqualTo(2); + await That(count2).IsEqualTo(1); + } + + [Fact] + public async Task OnUnsubscribed_Forever_Extension_RepeatsIndefinitely() + { + int callCount = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed + .OnUnsubscribed.Do(() => { callCount++; }).For(1).Forever(); + + void Handler(string type, int amount) { } + sut.ChocolateDispensed += Handler; + for (int i = 0; i < 10; i++) + { + sut.ChocolateDispensed -= Handler; + } + + await That(callCount).IsEqualTo(10); + } + + [Fact] + public async Task OnUnsubscribed_InParallel_RunsAlongsideNextCallback() + { + int count1 = 0; + int count2 = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed + .OnUnsubscribed.Do(() => { count1++; }).InParallel() + .OnUnsubscribed.Do(() => { count2++; }); + + void Handler(string type, int amount) { } + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed -= Handler; + sut.ChocolateDispensed -= Handler; + + await That(count1).IsEqualTo(2); + await That(count2).IsEqualTo(2); + } + + [Fact] + public async Task OnUnsubscribed_Only_StopsAfterNInvocations() + { + int callCount = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed.OnUnsubscribed.Do(() => { callCount++; }).Only(2); + + void Handler(string type, int amount) { } + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed -= Handler; + sut.ChocolateDispensed -= Handler; + sut.ChocolateDispensed -= Handler; + sut.ChocolateDispensed -= Handler; + + await That(callCount).IsEqualTo(2); + } + + [Fact] + public async Task OnUnsubscribed_OnlyOnce_Extension_StopsAfterSingleInvocation() + { + int callCount = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed.OnUnsubscribed.Do(() => { callCount++; }).OnlyOnce(); + + void Handler(string type, int amount) { } + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed -= Handler; + sut.ChocolateDispensed -= Handler; + sut.ChocolateDispensed -= Handler; + + await That(callCount).IsEqualTo(1); + } + + [Fact] + public async Task OnUnsubscribed_When_OnlyFires_WhenPredicateMatches() + { + int callCount = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed.OnUnsubscribed.Do(() => { callCount++; }).When(n => n % 2 == 0); + + void Handler(string type, int amount) { } + sut.ChocolateDispensed += Handler; + for (int i = 0; i < 6; i++) + { + sut.ChocolateDispensed -= Handler; + } + + // unsubscriptions at index 0, 2, 4 pass the predicate + await That(callCount).IsEqualTo(3); + } + [Fact] public async Task SetupDoesNotInterfereWithVerification() {