From 8a74c81329a2bd9a0bcef393ae18c31cf0b904ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 18:45:21 +0200 Subject: [PATCH 01/23] docs: document params/returns on generated CreateMock surface Generated CreateMock, Wrapping, and Initialize overloads previously emitted only a `` tag - hovering over their parameter list in the IDE showed an empty parameter-info popup. Add ``, ``, and `` tags plus overload-specific remarks so IntelliSense differentiates between the `setup`/`mockBehavior`/`constructorParameters` overloads without opening the docs. Also strengthen the fallback stub `CreateMock` in `Sources.Mock.cs`: it throws at runtime, and the existing summary claimed it created a mock. The new summary, remarks, and `` tag make it clear the overload only exists so the IDE has something to bind to when the generator did not run. Adds `AppendXmlParam`, `AppendXmlTypeParam`, and `AppendXmlReturns` helpers alongside the existing `AppendXmlSummary` / `AppendXmlRemarks` so follow-up passes on the `Setup.*` / `Verify.*` / `Raise.*` generator emissions can reuse them. --- .../Sources/Sources.Mock.cs | 25 +++++++-- .../Sources/Sources.MockClass.cs | 52 ++++++++++++++----- .../Sources/Sources.cs | 20 +++++++ 3 files changed, 81 insertions(+), 16 deletions(-) diff --git a/Source/Mockolate.SourceGenerators/Sources/Sources.Mock.cs b/Source/Mockolate.SourceGenerators/Sources/Sources.Mock.cs index ce787bfe..a0d9cb45 100644 --- a/Source/Mockolate.SourceGenerators/Sources/Sources.Mock.cs +++ b/Source/Mockolate.SourceGenerators/Sources/Sources.Mock.cs @@ -49,22 +49,37 @@ internal interface IMockGenerationDidNotRun {} extension(T _) { /// - /// Create a new mock of with the given . + /// Fallback CreateMock that is only resolved when the Mockolate source generator did not run or when + /// is not mockable. Calling it always throws a . /// + /// Type to mock, which can be an interface or a class. + /// Ignored; reserved for the generator-emitted overload. + /// This method never returns - it always throws. /// - /// Any interface type can be used for mocking, but for classes, only abstract and virtual members can be mocked. + /// The source generator emits a concrete CreateMock overload per mockable type with the same shape. + /// If you see this fallback resolved in your IDE, the generator did not run for ; + /// run a clean build (for example dotnet clean && dotnet build) and verify that the type is mockable. /// + /// Always thrown: the source generator did not run or is not mockable. public static global::Mockolate.Mock.IMockGenerationDidNotRun CreateMock(global::Mockolate.MockBehavior? mockBehavior = null) { throw new global::Mockolate.Exceptions.MockException($"This method should not be called directly. Either '{typeof(T)}' is not mockable or the source generator did not run correctly."); } /// - /// Create a new mock of using the with the given . + /// Fallback CreateMock that is only resolved when the Mockolate source generator did not run or when + /// is not mockable. Calling it always throws a . /// + /// Type to mock, which can be an interface or a class. + /// Ignored; reserved for the generator-emitted overload. + /// Ignored; reserved for the generator-emitted overload. + /// This method never returns - it always throws. /// - /// Any interface type can be used for mocking, but for classes, only abstract and virtual members can be mocked. + /// The source generator emits a concrete CreateMock overload per mockable type with the same shape. + /// If you see this fallback resolved in your IDE, the generator did not run for ; + /// run a clean build (for example dotnet clean && dotnet build) and verify that the type is mockable. /// + /// Always thrown: the source generator did not run or is not mockable. public static global::Mockolate.Mock.IMockGenerationDidNotRun CreateMock(object?[]? constructorParameters, global::Mockolate.MockBehavior? mockBehavior = null) { throw new global::Mockolate.Exceptions.MockException($"This method should not be called directly. Either '{typeof(T)}' is not mockable or the source generator did not run correctly."); @@ -76,6 +91,8 @@ internal interface IMockGenerationDidNotRun {} /// /// Add an interface that the mock also implements. /// + /// Additional interface the mock should implement. + /// The same mock, typed so that members are accessible. public global::Mockolate.Mock.IMockGenerationDidNotRun Implementing() where TInterface : class { throw new global::Mockolate.Exceptions.MockException($"This method should not be called directly. Either '{typeof(TInterface)}' is not mockable or the source generator did not run correctly."); diff --git a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs index 1a7bd8c3..c1678b09 100644 --- a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs +++ b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs @@ -77,15 +77,21 @@ public static string MockClass(string name, Class @class, bool hasOverloadResolu #region CreateMock + string createMockReturns = + $"A new mock instance of ."; + sb.AppendXmlSummary( $"Create a new mock of with the default ."); + sb.AppendXmlReturns(createMockReturns); sb.Append("\t\tpublic static ").Append(@class.ClassFullName).Append(" CreateMock()").AppendLine(); sb.Append("\t\t\t=> CreateMock(null, null, null);").AppendLine(); sb.AppendLine(); sb.AppendXmlSummary( - $"Create a new mock of with the default ."); - sb.AppendXmlRemarks("The provided is immediately applied to the mock."); + $"Create a new mock of with the default , applying the given immediately."); + sb.AppendXmlRemarks("The provided is immediately applied to the mock. Use this overload when you want setups to cover virtual interactions triggered inside the constructor."); + sb.AppendXmlParam("setup", "Callback that receives the mock's setup surface and registers initial setups before the mock is returned."); + sb.AppendXmlReturns(createMockReturns); sb.Append("\t\tpublic static ").Append(@class.ClassFullName).Append(" CreateMock(global::System.Action<") .Append(setupType).Append("> setup)").AppendLine(); sb.Append("\t\t\t=> CreateMock(null, null, setup);").AppendLine(); @@ -93,14 +99,19 @@ public static string MockClass(string name, Class @class, bool hasOverloadResolu sb.AppendXmlSummary( $"Create a new mock of with the given ."); + sb.AppendXmlParam("mockBehavior", "Controls how the mock responds when members are invoked without a matching setup; see ."); + sb.AppendXmlReturns(createMockReturns); sb.Append("\t\tpublic static ").Append(@class.ClassFullName) .Append(" CreateMock(global::Mockolate.MockBehavior mockBehavior)").AppendLine(); sb.Append("\t\t\t=> CreateMock(null, mockBehavior, null);").AppendLine(); sb.AppendLine(); sb.AppendXmlSummary( - $"Create a new mock of with the given ."); - sb.AppendXmlRemarks("The provided is immediately applied to the mock."); + $"Create a new mock of with the given , applying the given immediately."); + sb.AppendXmlRemarks("The provided is immediately applied to the mock. Use this overload when you want setups to cover virtual interactions triggered inside the constructor."); + sb.AppendXmlParam("mockBehavior", "Controls how the mock responds when members are invoked without a matching setup; see ."); + sb.AppendXmlParam("setup", "Callback that receives the mock's setup surface and registers initial setups before the mock is returned."); + sb.AppendXmlReturns(createMockReturns); sb.Append("\t\tpublic static ").Append(@class.ClassFullName) .Append(" CreateMock(global::Mockolate.MockBehavior mockBehavior, global::System.Action<").Append(setupType) .Append("> setup)").AppendLine(); @@ -110,14 +121,19 @@ public static string MockClass(string name, Class @class, bool hasOverloadResolu if (!@class.IsInterface) { sb.AppendXmlSummary( - $"Create a new mock of using the ."); + $"Create a new mock of using the given to invoke the base-class constructor."); + sb.AppendXmlParam("constructorParameters", "Values forwarded to a matching base-class constructor. Required when no parameterless constructor exists."); + sb.AppendXmlReturns(createMockReturns); sb.Append("\t\tpublic static ").Append(@class.ClassFullName) .Append(" CreateMock(object?[] constructorParameters)").AppendLine(); sb.Append("\t\t\t=> CreateMock(constructorParameters, null, null);").AppendLine(); sb.AppendLine(); sb.AppendXmlSummary( - $"Create a new mock of using the with the given ."); + $"Create a new mock of using the given and ."); + sb.AppendXmlParam("constructorParameters", "Values forwarded to a matching base-class constructor. Required when no parameterless constructor exists."); + sb.AppendXmlParam("mockBehavior", "Controls how the mock responds when members are invoked without a matching setup; see ."); + sb.AppendXmlReturns(createMockReturns); sb.Append("\t\tpublic static ").Append(@class.ClassFullName) .Append(" CreateMock(object?[] constructorParameters, global::Mockolate.MockBehavior mockBehavior)") .AppendLine(); @@ -125,8 +141,11 @@ public static string MockClass(string name, Class @class, bool hasOverloadResolu sb.AppendLine(); sb.AppendXmlSummary( - $"Create a new mock of using the ."); - sb.AppendXmlRemarks("The provided is immediately applied to the mock."); + $"Create a new mock of using the given , applying the given immediately."); + sb.AppendXmlRemarks("The provided is immediately applied to the mock. Use this overload when you want setups to cover virtual interactions triggered inside the constructor."); + sb.AppendXmlParam("constructorParameters", "Values forwarded to a matching base-class constructor. Required when no parameterless constructor exists."); + sb.AppendXmlParam("setup", "Callback that receives the mock's setup surface and registers initial setups before the mock is returned."); + sb.AppendXmlReturns(createMockReturns); sb.Append("\t\tpublic static ").Append(@class.ClassFullName) .Append(" CreateMock(object?[] constructorParameters, global::System.Action<").Append(setupType) .Append("> setup)").AppendLine(); @@ -135,8 +154,12 @@ public static string MockClass(string name, Class @class, bool hasOverloadResolu } sb.AppendXmlSummary( - $"Create a new mock of using the with the given ."); - sb.AppendXmlRemarks("The provided is immediately applied to the mock."); + $"Create a new mock of using the given and , applying the given immediately."); + sb.AppendXmlRemarks("The provided is immediately applied to the mock. Use this overload when you want setups to cover virtual interactions triggered inside the constructor."); + sb.AppendXmlParam("constructorParameters", "Values forwarded to a matching base-class constructor, or to use the parameterless constructor."); + sb.AppendXmlParam("mockBehavior", "Controls how the mock responds when members are invoked without a matching setup, or for ."); + sb.AppendXmlParam("setup", "Callback that receives the mock's setup surface and registers initial setups before the mock is returned, or to skip."); + sb.AppendXmlReturns(createMockReturns); sb.Append("\t\t").Append(@class.IsInterface ? "private" : "public").Append(" static ") .Append(@class.ClassFullName) .Append( @@ -364,7 +387,9 @@ static bool TryCastWithDefaultValue(object?[] values, int index, TValue #endregion CreateMock sb.AppendXmlSummary("Create a mock that wraps the given ."); - sb.AppendXmlRemarks("All interactions are forwarded to the ."); + sb.AppendXmlRemarks("Public members on the mock forward to unless overridden by a setup; protected members still go through the base-class implementation. All forwarded interactions are recorded and can be verified the same as on a plain mock."); + sb.AppendXmlParam("instance", "The real object whose calls should be forwarded. Must not be ."); + sb.AppendXmlReturns($"A new mock of that delegates to ."); sb.Append("\t\tpublic ").Append(@class.ClassFullName).Append(" Wrapping(").Append(@class.ClassFullName) .Append(" instance)").AppendLine(); sb.Append("\t\t{").AppendLine(); @@ -394,7 +419,10 @@ static bool TryCastWithDefaultValue(object?[] values, int index, TValue sb.AppendXmlSummary( "Initialize mocks of type with the given ."); sb.AppendXmlRemarks( - "The is applied to the mock before the constructor is executed."); + "The is applied to the mock before the constructor is executed. Calling Initialize again overlays additional setups on top of any previously registered ones."); + sb.AppendXmlTypeParam("T", $"The mockable type derived from that this setup should apply to."); + sb.AppendXmlParam("setup", $"Callback invoked when a new mock of is created."); + sb.AppendXmlReturns("A new with the registered initializer. The original instance is unchanged."); sb.Append("\t\tpublic global::Mockolate.MockBehavior Initialize(global::System.Action<").Append(setupType) .Append("> setup)").AppendLine(); sb.Append("\t\t\twhere T : ").Append(@class.ClassFullName).AppendLine(); diff --git a/Source/Mockolate.SourceGenerators/Sources/Sources.cs b/Source/Mockolate.SourceGenerators/Sources/Sources.cs index aae54fcc..ac4ad082 100644 --- a/Source/Mockolate.SourceGenerators/Sources/Sources.cs +++ b/Source/Mockolate.SourceGenerators/Sources/Sources.cs @@ -376,6 +376,26 @@ private void AppendXmlRemarks(string remarksText, string indent = "\t\t") sb.Append(indent).Append("/// ").Append(remarksText).AppendLine(); sb.Append(indent).Append("/// ").AppendLine(); } + + /// + /// Appends an XML documentation <param> tag with the given name and description. + /// + private void AppendXmlParam(string name, string description, string indent = "\t\t") + => sb.Append(indent).Append("/// ") + .Append(description).Append("").AppendLine(); + + /// + /// Appends an XML documentation <typeparam> tag with the given name and description. + /// + private void AppendXmlTypeParam(string name, string description, string indent = "\t\t") + => sb.Append(indent).Append("/// ") + .Append(description).Append("").AppendLine(); + + /// + /// Appends an XML documentation <returns> tag with the given text. + /// + private void AppendXmlReturns(string returnsText, string indent = "\t\t") + => sb.Append(indent).Append("/// ").Append(returnsText).Append("").AppendLine(); } } #pragma warning restore S3776 // Cognitive Complexity of methods should not be too high From 919a658d01d2dda03d6e89927fadc686811f8258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 18:53:22 +0200 Subject: [PATCH 02/23] docs: differentiate generated Setup/Verify overload summaries A method with 3-5 overloads on the mock's Setup and Verify surface (matcher, direct-value, direct-value-all, mixed, and IParameters) previously emitted the same one-line summary for every overload, giving IntelliSense no way to tell them apart. Emit an overload-specific tag that names the strategy: "takes It.* matchers", "accepts a direct value for ", "accepts direct values for every parameter", or "matches via a custom Match.* predicate", and, on the Verify.IgnoreParameters overload, points at .AnyParameters() as the escape hatch. Covers method setups, method verifications, indexer setups, and indexer verifications. Event and property overloads do not have the same overload explosion and are left alone. --- .../Sources/Sources.MockClass.cs | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs index c1678b09..dcbb8aef 100644 --- a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs +++ b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs @@ -2670,6 +2670,57 @@ bool MethodPredicate(Method method) #endregion } + private static void AppendOverloadDifferentiatorRemark(StringBuilder sb, + IReadOnlyList parameterNames, bool useParameters, bool[]? valueFlags, + bool isVerify = false) + { + if (parameterNames.Count == 0) + { + return; + } + + string text; + if (useParameters) + { + text = isVerify + ? "This overload matches invocations via a custom predicate (for example or ) rather than per-parameter matchers." + : "This overload configures the setup via a custom predicate (for example or ) rather than per-parameter matchers."; + } + else if (valueFlags is null) + { + text = "This overload takes argument matchers (e.g. It.IsAny<T>(), It.Is<T>(value)) for every parameter."; + } + else if (valueFlags.All(x => x)) + { + text = isVerify + ? "This overload accepts direct values for every parameter and returns a whose drops per-parameter matching entirely." + : "This overload accepts direct values for every parameter; each is treated as It.Is<T>(value)."; + } + else + { + List valueParams = []; + List matcherParams = []; + for (int i = 0; i < parameterNames.Count && i < valueFlags.Length; i++) + { + if (valueFlags[i]) + { + valueParams.Add($""); + } + else + { + matcherParams.Add($""); + } + } + + string directPart = string.Join(", ", valueParams); + string matcherPart = string.Join(", ", matcherParams); + text = + $"This overload accepts a direct value for {directPart} (equivalent to It.Is<T>(value)) and an matcher for {matcherPart}."; + } + + sb.AppendXmlRemarks(text); + } + private static void AppendMethodSetupDefinition(StringBuilder sb, Class @class, Method method, bool useParameters, string? methodNameOverride = null, bool[]? valueFlags = null, bool hasOverloadResolutionPriority = false) @@ -2722,6 +2773,7 @@ private static void AppendMethodSetupDefinition(StringBuilder sb, Class @class, sb.Append(".").AppendLine(); sb.Append("\t\t/// ").AppendLine(); + AppendOverloadDifferentiatorRemark(sb, method.Parameters.Select(p => p.Name).ToArray(), useParameters, valueFlags); if (method.ReturnType != Type.Void) { if (valueFlags?.All(x => x) == true) @@ -3337,6 +3389,9 @@ private static void AppendIndexerSetupDefinition(StringBuilder sb, Property inde sb.AppendXmlSummary( $"Setup for the {indexer.Type.Fullname.EscapeForXmlDoc()} indexer p.RefKind.GetString() + p.Type.Fullname.EscapeForXmlDoc()))}]\" />"); + string[] indexerNames = Enumerable.Range(1, indexer.IndexerParameters!.Value.Count) + .Select(i => $"parameter{i}").ToArray(); + AppendOverloadDifferentiatorRemark(sb, indexerNames, useParameters: false, valueFlags); if (hasOverloadResolutionPriority) { sb.Append("\t\t[global::System.Runtime.CompilerServices.OverloadResolutionPriority(") @@ -3774,6 +3829,9 @@ private static void AppendIndexerVerifyDefinition(StringBuilder sb, Property ind sb.AppendXmlSummary( $"Verify interactions with the {indexer.Type.Fullname.EscapeForXmlDoc()} indexer p.RefKind.GetString() + p.Type.Fullname.EscapeForXmlDoc()))}]\" />."); + AppendOverloadDifferentiatorRemark(sb, + indexer.IndexerParameters!.Value.Select(p => p.Name).ToArray(), + useParameters: false, valueFlags, isVerify: true); if (hasOverloadResolutionPriority) { sb.Append("\t\t[global::System.Runtime.CompilerServices.OverloadResolutionPriority(") @@ -4218,6 +4276,7 @@ private static void AppendMethodVerifyDefinition(StringBuilder sb, Method method sb.Append(".").AppendLine(); sb.Append("\t\t/// ").AppendLine(); + AppendOverloadDifferentiatorRemark(sb, method.Parameters.Select(p => p.Name).ToArray(), useParameters, valueFlags, isVerify: true); if (valueFlags?.All(x => x) == true) { sb.Append("\t\tglobal::Mockolate.Verify.VerificationResult<").Append(verifyName) From f45e712642ff96fe9f5414574ce2a38815b55436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 19:00:20 +0200 Subject: [PATCH 03/23] docs: add exception tags to verification terminators The count-assertion terminators on a VerificationResult (Once, Exactly, Never, AtLeast, AtLeastOnce, AtLeastTwice, AtMost, AtMostOnce, AtMostTwice, Between, Twice, Times, Then) throw MockVerificationException when the assertion fails, but none of them document that. Users discover the throw behavior the first time a verification fails unexpectedly. Add tags to every terminator so the throw is visible in IntelliSense. Also document the ArgumentOutOfRangeException that Between raises for a negative minimum or a maximum below the minimum, add docs on Times (clarifying that the int is the actual interaction count) and Then (clarifying the ordering semantic), and document the compiler-populated doNotPopulateThisValue parameter on Times. --- .../Verify/VerificationResultExtensions.cs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/Source/Mockolate/Verify/VerificationResultExtensions.cs b/Source/Mockolate/Verify/VerificationResultExtensions.cs index 31c05f1e..a59257df 100644 --- a/Source/Mockolate/Verify/VerificationResultExtensions.cs +++ b/Source/Mockolate/Verify/VerificationResultExtensions.cs @@ -33,6 +33,10 @@ private static string ToTimes(this int amount, string verb = "") /// /// …at least the expected number of . /// + /// + /// Thrown when the verified interaction occurred fewer than times, + /// or when a timeout elapses first. + /// public void AtLeast(int times) { IVerificationResult result = verificationResult; @@ -59,6 +63,10 @@ public void AtLeast(int times) /// /// …at least once. /// + /// + /// Thrown when the verified interaction did not occur, + /// or when a timeout elapses first. + /// public void AtLeastOnce() { IVerificationResult result = verificationResult; @@ -85,6 +93,10 @@ public void AtLeastOnce() /// /// …at least twice. /// + /// + /// Thrown when the verified interaction occurred fewer than two times, + /// or when a timeout elapses first. + /// public void AtLeastTwice() { IVerificationResult result = verificationResult; @@ -111,6 +123,9 @@ public void AtLeastTwice() /// /// …at most the expected number of . /// + /// + /// Thrown when the verified interaction occurred more than times. + /// public void AtMost(int times) { IVerificationResult result = verificationResult; @@ -138,6 +153,14 @@ public void AtMost(int times) /// Verifies that the mock was invoked between and times /// (inclusive). /// + /// + /// Thrown when the verified interaction count falls outside the + /// - range, + /// or when a timeout elapses first. + /// + /// + /// Thrown when is negative or is less than . + /// public void Between(int minimum, int maximum) { if (minimum < 0) @@ -175,6 +198,9 @@ public void Between(int minimum, int maximum) /// /// …at most once. /// + /// + /// Thrown when the verified interaction occurred more than once. + /// public void AtMostOnce() { IVerificationResult result = verificationResult; @@ -201,6 +227,9 @@ public void AtMostOnce() /// /// …at most twice. /// + /// + /// Thrown when the verified interaction occurred more than two times. + /// public void AtMostTwice() { IVerificationResult result = verificationResult; @@ -227,6 +256,10 @@ public void AtMostTwice() /// /// …exactly the expected number of . /// + /// + /// Thrown when the verified interaction count is not equal to , + /// or when a timeout elapses first. + /// public void Exactly(int times) { IVerificationResult result = verificationResult; @@ -253,6 +286,9 @@ public void Exactly(int times) /// /// …never. /// + /// + /// Thrown when the verified interaction occurred at least once. + /// public void Never() { IVerificationResult result = verificationResult; @@ -279,6 +315,10 @@ public void Never() /// /// …exactly once. /// + /// + /// Thrown when the verified interaction did not occur exactly once, + /// or when a timeout elapses first. + /// public void Once() { IVerificationResult result = verificationResult; @@ -305,6 +345,10 @@ public void Once() /// /// …exactly twice. /// + /// + /// Thrown when the verified interaction did not occur exactly two times, + /// or when a timeout elapses first. + /// public void Twice() { IVerificationResult result = verificationResult; @@ -331,6 +375,17 @@ public void Twice() /// /// Verifies that the mock was invoked according to the . /// + /// + /// Receives the actual number of matching interactions and returns if that count is acceptable. + /// + /// + /// Populated by the compiler via + /// to include the source expression of in failure messages. + /// + /// + /// Thrown when returns for the observed count, + /// or when a timeout elapses first. + /// public void Times(Func predicate, [CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") @@ -359,6 +414,14 @@ public void Times(Func predicate, /// /// Supports fluent chaining of verifications in a given order. /// + /// + /// Each callback returns a follow-up ; that interaction must have been + /// recorded strictly after the previous verification in the chain for the assertion to pass. + /// + /// + /// Thrown when the expected interactions did not occur in the given order + /// (for example, a later interaction was recorded before an earlier one, or was missing). + /// public void Then(params Func>[] orderedChecks) { string? error = null; From e644a6b4cd63f74e774ca23bcdd8a22c45303af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 19:03:22 +0200 Subject: [PATCH 04/23] docs: rewrite verification assertion summaries as whole sentences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The count-assertion terminators (AtLeast, AtLeastOnce, AtLeastTwice, AtMost, AtMostOnce, AtMostTwice, Between, Exactly, Never, Once, Twice) used summaries that started with an ellipsis and read like fragments of a chained call ("…at least once."). In IntelliSense the prefix is gone and readers see a mid-sentence fragment with no subject - useless as a one-line explanation. Rewrite each one as a complete sentence of the form "Asserts that the verified interaction occurred ...". Also rewrite Then to state the actual guarantee (order of observed interactions) instead of "Supports fluent chaining". --- .../Verify/VerificationResultExtensions.cs | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/Source/Mockolate/Verify/VerificationResultExtensions.cs b/Source/Mockolate/Verify/VerificationResultExtensions.cs index a59257df..8210536f 100644 --- a/Source/Mockolate/Verify/VerificationResultExtensions.cs +++ b/Source/Mockolate/Verify/VerificationResultExtensions.cs @@ -31,7 +31,7 @@ private static string ToTimes(this int amount, string verb = "") extension(VerificationResult verificationResult) { /// - /// …at least the expected number of . + /// Asserts that the verified interaction occurred at least times. /// /// /// Thrown when the verified interaction occurred fewer than times, @@ -61,7 +61,7 @@ public void AtLeast(int times) } /// - /// …at least once. + /// Asserts that the verified interaction occurred at least once. /// /// /// Thrown when the verified interaction did not occur, @@ -91,7 +91,7 @@ public void AtLeastOnce() } /// - /// …at least twice. + /// Asserts that the verified interaction occurred at least twice. /// /// /// Thrown when the verified interaction occurred fewer than two times, @@ -121,7 +121,7 @@ public void AtLeastTwice() } /// - /// …at most the expected number of . + /// Asserts that the verified interaction occurred at most times. /// /// /// Thrown when the verified interaction occurred more than times. @@ -150,8 +150,8 @@ public void AtMost(int times) } /// - /// Verifies that the mock was invoked between and times - /// (inclusive). + /// Asserts that the verified interaction occurred between and + /// times, inclusive. /// /// /// Thrown when the verified interaction count falls outside the @@ -196,7 +196,7 @@ public void Between(int minimum, int maximum) } /// - /// …at most once. + /// Asserts that the verified interaction occurred at most once. /// /// /// Thrown when the verified interaction occurred more than once. @@ -225,7 +225,7 @@ public void AtMostOnce() } /// - /// …at most twice. + /// Asserts that the verified interaction occurred at most twice. /// /// /// Thrown when the verified interaction occurred more than two times. @@ -254,7 +254,7 @@ public void AtMostTwice() } /// - /// …exactly the expected number of . + /// Asserts that the verified interaction occurred exactly times. /// /// /// Thrown when the verified interaction count is not equal to , @@ -284,7 +284,7 @@ public void Exactly(int times) } /// - /// …never. + /// Asserts that the verified interaction never occurred. /// /// /// Thrown when the verified interaction occurred at least once. @@ -313,7 +313,7 @@ public void Never() } /// - /// …exactly once. + /// Asserts that the verified interaction occurred exactly once. /// /// /// Thrown when the verified interaction did not occur exactly once, @@ -343,7 +343,7 @@ public void Once() } /// - /// …exactly twice. + /// Asserts that the verified interaction occurred exactly twice. /// /// /// Thrown when the verified interaction did not occur exactly two times, @@ -412,7 +412,8 @@ public void Times(Func predicate, } /// - /// Supports fluent chaining of verifications in a given order. + /// Asserts that the current verification and each of the occurred in the + /// specified order. /// /// /// Each callback returns a follow-up ; that interaction must have been From bdc40421655ab776406b64e978cdfa30af4daca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 19:04:17 +0200 Subject: [PATCH 05/23] docs: correct VerificationResultExtensions class summary The class summary claimed the type "contains the matching interactions for verification" - it does not. This is a static class of count-assertion extension methods (Once, Exactly, Between, Then, ...) that act as terminators on a VerificationResult. Replace with a summary that names the actual role, and add a remarks block pointing at Within/WithCancellation for async scenarios so readers can discover the full pipeline from IntelliSense. --- Source/Mockolate/Verify/VerificationResultExtensions.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Source/Mockolate/Verify/VerificationResultExtensions.cs b/Source/Mockolate/Verify/VerificationResultExtensions.cs index 8210536f..7ffae187 100644 --- a/Source/Mockolate/Verify/VerificationResultExtensions.cs +++ b/Source/Mockolate/Verify/VerificationResultExtensions.cs @@ -8,8 +8,15 @@ namespace Mockolate.Verify; /// -/// The expectation contains the matching interactions for verification. +/// Count-assertion extensions on that turn a recorded interaction set +/// into a pass/fail check (for example .Once(), .AtLeast(3), .Then(...)). /// +/// +/// These methods are the terminators of a verification chain: each one either returns normally when the observed +/// interactions match the expectation, or throws a . Use Within or +/// WithCancellation on the before a terminator to wait for +/// interactions produced on a background thread. +/// #if !DEBUG [System.Diagnostics.DebuggerNonUserCode] #endif From 212fb182f710b6cb316d3fc1a6d9d07e438201c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 19:05:12 +0200 Subject: [PATCH 06/23] docs: clarify Match class summary and contrast with It The previous summary ("Specify a matching condition for a list of parameter.") was ungrammatical and did not explain when Match is the right choice over the per-parameter It matchers. Rewrite the summary to describe the role, add a remarks block noting the unambiguous-method-name restriction the generator enforces, and point at AnyParameters() and Parameters(predicate) as the two entry points users actually call. --- Source/Mockolate/Match.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Source/Mockolate/Match.cs b/Source/Mockolate/Match.cs index 3d2dd986..ad95db6f 100644 --- a/Source/Mockolate/Match.cs +++ b/Source/Mockolate/Match.cs @@ -4,8 +4,18 @@ namespace Mockolate; #pragma warning disable S3453 // This class can't be instantiated; make its constructor 'public'. /// -/// Specify a matching condition for a list of parameter. +/// Specifies a matching condition across the full parameter list of a method or indexer. Complement to +/// , which targets a single parameter at a time. /// +/// +/// Reach for when you want to match by a predicate over all parameters at once, or when +/// per-parameter matchers would be too verbose. Use to accept any arguments, or +/// Match.Parameters(predicate) to match against a custom predicate. +/// +/// Because the generator cannot disambiguate which overload a -based setup targets, +/// overloads are only emitted on unambiguous method names. For overloaded methods, use +/// matchers per parameter instead. +/// #if !DEBUG [System.Diagnostics.DebuggerNonUserCode] #endif From 82c59ff204e5d327c90d044619fb8998f43b5d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 19:06:28 +0200 Subject: [PATCH 07/23] docs: document Match.Parameters/AnyParameters restrictions and args Match.Parameters had a one-line summary and no /, so readers had to open the README to learn what the predicate's object?[] argument actually contains or that the generator only emits these overloads for methods with a unique name (no overloads). Document the predicate shape, the compiler-populated CallerArgumentExpression, and the overload-uniqueness requirement on both Match.Parameters and Match.AnyParameters, with a cross-reference to It.IsAny{T} as the fallback for overloaded methods. --- Source/Mockolate/Match.AnyParameters.cs | 10 +++++++++- Source/Mockolate/Match.Parameters.cs | 22 +++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/Source/Mockolate/Match.AnyParameters.cs b/Source/Mockolate/Match.AnyParameters.cs index 62a6e4d2..a6c722fe 100644 --- a/Source/Mockolate/Match.AnyParameters.cs +++ b/Source/Mockolate/Match.AnyParameters.cs @@ -6,8 +6,16 @@ namespace Mockolate; public partial class Match { /// - /// Matches any parameter combination. + /// Matches any argument combination passed to the method. /// + /// + /// An usable in generator-emitted Setup.Method(Match.AnyParameters()) + /// and Verify.Method(Match.AnyParameters()) overloads. + /// + /// + /// Only available on methods whose name is unique on the mocked type (no overloads). For overloaded + /// methods, combine per-parameter matchers instead. + /// public static IParameters AnyParameters() => new AnyParametersMatch(); diff --git a/Source/Mockolate/Match.Parameters.cs b/Source/Mockolate/Match.Parameters.cs index ef391d91..a6f7bf61 100644 --- a/Source/Mockolate/Match.Parameters.cs +++ b/Source/Mockolate/Match.Parameters.cs @@ -8,8 +8,28 @@ namespace Mockolate; public partial class Match { /// - /// Matches the parameters against the . + /// Matches an invocation when the supplied returns + /// for its full argument array. /// + /// + /// Receives every argument of the invocation, in declaration order, as ? values. + /// Ref and out parameters are passed as their current values. Return to accept the + /// invocation. + /// + /// + /// Populated by the compiler via to include the source + /// expression of in the setup's and in + /// verification failure messages. + /// + /// + /// An usable in generator-emitted Setup.Method(Match.Parameters(...)) + /// and Verify.Method(Match.Parameters(...)) overloads. + /// + /// + /// The Mockolate source generator only emits -based Setup/Verify + /// overloads for methods whose name is unique on the mocked type (no overloads). For overloaded methods, + /// use per-parameter matchers instead. + /// public static IParameters Parameters(Func predicate, [CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") From fb3c642410413bb2e11dcddc900da64a93add9e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 19:07:25 +0200 Subject: [PATCH 08/23] docs: expand IMock and IMock.MockRegistry summaries The IMock interface and its MockRegistry property are what users land on via the typed sut.Mock accessor. The previous one-liners ("The mock interface gives access to the mock registry" / "to store setups and interactions") told readers nothing about the fluent entry points they are about to encounter. Expand the interface-level remarks to name the entry points (Setup, Verify, Raise, InScenario, TransitionTo, Monitor, ClearAllInteractions) and point the MockRegistry property's docs at the typed Mock surface as the preferred API. --- Source/Mockolate/IMock.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Source/Mockolate/IMock.cs b/Source/Mockolate/IMock.cs index 536c0fe0..cab0578f 100644 --- a/Source/Mockolate/IMock.cs +++ b/Source/Mockolate/IMock.cs @@ -1,13 +1,27 @@ namespace Mockolate; /// -/// The mock interface gives access to the mock registry of a mock instance. +/// Non-generic contract exposed by every Mockolate-generated mock instance. Provides access to the +/// that backs the mock. /// +/// +/// Most users reach for the generator-emitted typed Mock property (for example sut.Mock) instead of +/// this interface. The typed view surfaces the fluent entry points Setup, Verify, Raise, +/// InScenario, TransitionTo, Monitor, and ClearAllInteractions. Use +/// directly only for framework code that needs to work against an untyped mock handle. +/// public interface IMock { /// - /// The mock registry to store setups and interactions with the mock. + /// The that backs this mock: it owns the , + /// the registered setups, and the recorded interactions. /// + /// + /// The typed Mock property on a generated mock exposes the fluent surface (Setup, Verify, + /// Raise, InScenario, TransitionTo, Monitor, ClearAllInteractions) and is + /// the preferred entry point; reach for directly only when you need to interact + /// with the underlying state (for example in custom extensions or diagnostic tooling). + /// MockRegistry MockRegistry { get; } /// From a566fc7c99a1bfe1c0cb48f9eac9950e61875c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 19:08:35 +0200 Subject: [PATCH 09/23] docs: differentiate MockRegistry constructor docs All four MockRegistry constructors used , so IntelliSense showed the same class-level text for each one and gave readers no way to pick the right overload. Replace with per-overload summaries plus docs describing what each constructor actually does: 1. (behavior, constructorParameters?) - primary entry point 2. (registry, wraps) - wrap a real instance 3. (registry, constructorParameters) - rebind ctor parameters 4. (registry, interactions) - scoped interaction recording --- Source/Mockolate/MockRegistry.cs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/Source/Mockolate/MockRegistry.cs b/Source/Mockolate/MockRegistry.cs index dfe69f96..249175d2 100644 --- a/Source/Mockolate/MockRegistry.cs +++ b/Source/Mockolate/MockRegistry.cs @@ -19,7 +19,12 @@ public partial class MockRegistry [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly ScenarioState _scenarioState; - /// + /// + /// Creates a new with the given and, optionally, + /// for a class mock's base-class constructor. + /// + /// The that governs how the mock responds without a matching setup. + /// Values forwarded to the base-class constructor, or if no base constructor call is needed. public MockRegistry(MockBehavior behavior, object?[]? constructorParameters = null) { Behavior = behavior; @@ -30,7 +35,12 @@ public MockRegistry(MockBehavior behavior, object?[]? constructorParameters = nu Wraps = null; } - /// + /// + /// Creates a that shares setup and scenario state with + /// but records interactions on a fresh bucket and forwards calls to . + /// + /// The source registry whose , setups, and scenario state are reused. + /// The real instance that the mock should delegate calls to. See . public MockRegistry(MockRegistry registry, object wraps) { Behavior = registry.Behavior; @@ -41,7 +51,12 @@ public MockRegistry(MockRegistry registry, object wraps) Wraps = wraps; } - /// + /// + /// Creates a that shares all state with but exposes + /// a different set of to the base-class constructor. + /// + /// The source registry whose setups, interactions, scenario state, and wrapped instance are reused. + /// Values forwarded to the base-class constructor of the new mock instance. public MockRegistry(MockRegistry registry, object?[] constructorParameters) { Behavior = registry.Behavior; @@ -52,7 +67,12 @@ public MockRegistry(MockRegistry registry, object?[] constructorParameters) Wraps = registry.Wraps; } - /// + /// + /// Creates a that shares all state with but records + /// into the supplied collection. Used for scoped monitoring. + /// + /// The source registry whose behavior, setups, constructor parameters, scenario state, and wrapped instance are reused. + /// The interaction collection that new invocations should be appended to. public MockRegistry(MockRegistry registry, MockInteractions interactions) { Behavior = registry.Behavior; From abf520c4d65d180f817ad27f80520d5c619732eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 19:10:10 +0200 Subject: [PATCH 10/23] docs: expand MockRegistry.Wraps null/forwarding semantics The property docs said only "The instance the mock wraps.", which did not explain when Wraps is null, which calls it receives, or how it interacts with setups and protected members. Expand the summary to call out the null case (plain mock vs. wrapping mock) and add a remarks block describing the forwarding rules and the fact that forwarded calls still land on Interactions and are verifiable. --- Source/Mockolate/MockRegistry.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Source/Mockolate/MockRegistry.cs b/Source/Mockolate/MockRegistry.cs index 249175d2..5892a320 100644 --- a/Source/Mockolate/MockRegistry.cs +++ b/Source/Mockolate/MockRegistry.cs @@ -99,8 +99,15 @@ public MockRegistry(MockRegistry registry, MockInteractions interactions) public object?[]? ConstructorParameters { get; } /// - /// The instance the mock wraps. + /// The real instance that public, non-protected calls are forwarded to, or when the + /// mock is a plain (non-wrapping) mock. /// + /// + /// Populated by the generator-emitted Wrapping(instance) extension. Public members delegate to + /// unless a matching setup overrides them; protected members still go through the base + /// class implementation. All forwarded calls are recorded on and can be verified + /// like any other interaction. + /// public object? Wraps { get; } /// From 79ffdd3d3cebdc0fd84c76fb2e6cb676e9dfae77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 19:12:18 +0200 Subject: [PATCH 11/23] docs: document scenario resolution on Scenario/TransitionTo The README describes the active-scenario -> default-bucket -> behavior fallback chain, but nothing on Scenario, TransitionTo, or the scenario-aware SetupIndexer/SetupMethod/SetupProperty/SetupEvent overloads said so. Add the resolution order (as a numbered list) to Scenario, point TransitionTo at it, and note on each scoped Setup overload that scenario setups do not leak into the default bucket. Users can now understand the scenario model without leaving IntelliSense. --- Source/Mockolate/MockRegistry.Setup.cs | 8 ++++++++ Source/Mockolate/MockRegistry.cs | 22 ++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Source/Mockolate/MockRegistry.Setup.cs b/Source/Mockolate/MockRegistry.Setup.cs index b8804cb0..a41aa545 100644 --- a/Source/Mockolate/MockRegistry.Setup.cs +++ b/Source/Mockolate/MockRegistry.Setup.cs @@ -17,6 +17,8 @@ public void SetupIndexer(IndexerSetup indexerSetup) /// /// Registers the in the mock for the given . + /// The setup only applies while equals ; it does not leak + /// into the default bucket. /// public void SetupIndexer(string scenario, IndexerSetup indexerSetup) => Setup.GetOrCreateScenario(scenario).Indexers.Add(indexerSetup); @@ -29,6 +31,8 @@ public void SetupMethod(MethodSetup methodSetup) /// /// Registers the in the mock for the given . + /// The setup only applies while equals ; it does not leak + /// into the default bucket. /// public void SetupMethod(string scenario, MethodSetup methodSetup) => Setup.GetOrCreateScenario(scenario).Methods.Add(methodSetup); @@ -41,6 +45,8 @@ public void SetupProperty(PropertySetup propertySetup) /// /// Registers the in the mock for the given . + /// The setup only applies while equals ; it does not leak + /// into the default bucket. /// public void SetupProperty(string scenario, PropertySetup propertySetup) => Setup.GetOrCreateScenario(scenario).Properties.Add(propertySetup); @@ -53,6 +59,8 @@ public void SetupEvent(EventSetup eventSetup) /// /// Registers the in the mock for the given . + /// The setup only applies while equals ; it does not leak + /// into the default bucket. /// public void SetupEvent(string scenario, EventSetup eventSetup) => Setup.GetOrCreateScenario(scenario).Events.Add(eventSetup); diff --git a/Source/Mockolate/MockRegistry.cs b/Source/Mockolate/MockRegistry.cs index 5892a320..f877ffc6 100644 --- a/Source/Mockolate/MockRegistry.cs +++ b/Source/Mockolate/MockRegistry.cs @@ -84,8 +84,20 @@ public MockRegistry(MockRegistry registry, MockInteractions interactions) } /// - /// The current scenario of the mock. Use to change the active scenario. + /// The name of the currently active scenario. Defaults to . Use + /// or the generator-emitted TransitionTo chained onto a setup to + /// change it. /// + /// + /// When a member is invoked, Mockolate resolves a matching setup in this order: + /// + /// the active scenario's bucket, when is not empty; + /// the default bucket (setups registered via sut.Mock.Setup.*); + /// the default response determined by . + /// + /// Scenario setups add to, rather than replace, the default bucket - register catch-alls at the default scope + /// and override specific members per scenario. + /// public string Scenario => _scenarioState.Current; /// @@ -111,9 +123,15 @@ public MockRegistry(MockRegistry registry, MockInteractions interactions) public object? Wraps { get; } /// - /// Transitions the mock to the given . + /// Transitions the mock to the given , so that subsequent member invocations + /// look up setups in that scenario's bucket first, and fall back to the default bucket if nothing matches. /// /// The name of the scenario to activate. Use for the default scope. + /// + /// Transitioning to a scenario name for which no setups were registered via InScenario(name) is + /// legal - resolution will simply fall straight through to the default bucket. See + /// for the full resolution order. + /// public void TransitionTo(string scenario) => _scenarioState.Current = scenario; From 8efe2722fb63cbd05d6a0526b0e61f98920e9232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 19:13:14 +0200 Subject: [PATCH 12/23] docs: annotate Within/WithCancellation blocking behavior The summaries called these methods "awaitable" without clarifying that the count-assertion terminator blocks the calling thread while it waits. Users expect a non-blocking await from anything called "Within", so mention explicitly that the wait is synchronous, that MockVerificationTimeoutException surfaces through MockVerificationException, and seealso the aweXpect.Mockolate extension package's asynchronous variant referenced in the README. --- Source/Mockolate/Verify/VerificationResult.cs | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/Source/Mockolate/Verify/VerificationResult.cs b/Source/Mockolate/Verify/VerificationResult.cs index c745d93e..cfa9f817 100644 --- a/Source/Mockolate/Verify/VerificationResult.cs +++ b/Source/Mockolate/Verify/VerificationResult.cs @@ -71,16 +71,37 @@ private static void ThrowIfRecordingDisabled(MockInteractions interactions) } /// - /// Makes the verification result awaitable, using the specified to wait for the expected - /// interactions to occur. + /// Returns a verification result that, when terminated with a count assertion, waits up to + /// for the expected interactions before throwing. /// + /// How long to wait for interactions before reporting a timeout failure. + /// + /// The wait is synchronous: the terminating count assertion blocks the calling thread until the expectation + /// is satisfied, the timeout elapses, or the token (if any) + /// fires. If a non-blocking wait is needed, use the asynchronous Within(TimeSpan) variant provided by + /// the aweXpect.Mockolate extension package. + /// + /// On timeout, a is raised internally and surfaces as a + /// from the terminator. + /// + /// public virtual VerificationResult Within(TimeSpan timeout) => new Awaitable(this, timeout); /// - /// Makes the verification result awaitable, using the specified to wait for the - /// expected interactions to occur. + /// Returns a verification result that, when terminated with a count assertion, waits for the expected + /// interactions until is cancelled. /// + /// Token that signals when to stop waiting. + /// + /// The wait is synchronous: the terminating count assertion blocks the calling thread. Combine with + /// to apply both a timeout and an external cancellation; whichever fires + /// first wins. + /// + /// For a non-blocking wait, use the asynchronous variant provided by the aweXpect.Mockolate + /// extension package. + /// + /// public virtual VerificationResult WithCancellation(CancellationToken cancellationToken) => new Awaitable(this, cancellationToken); From dcd8dacf284f59cdacec6c5f862dad200c01bfd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 19:14:47 +0200 Subject: [PATCH 13/23] docs: document exception-type constructor parameters All four custom exception types (MockException, MockNotSetupException, MockVerificationException, MockVerificationTimeoutException) used on their constructors. That emits the class-level summary instead of parameter docs, so `message` and `innerException` showed up in IntelliSense with no description. Replace with explicit + docs on each constructor and document MockVerificationTimeoutException's nullable timeout argument (the null case maps to cancellation-token-driven waits). --- Source/Mockolate/Exceptions/MockException.cs | 12 ++++++++++-- Source/Mockolate/Exceptions/MockNotSetupException.cs | 12 ++++++++++-- .../Exceptions/MockVerificationException.cs | 12 ++++++++++-- .../Exceptions/MockVerificationTimeoutException.cs | 7 ++++++- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/Source/Mockolate/Exceptions/MockException.cs b/Source/Mockolate/Exceptions/MockException.cs index d616d679..3b51a323 100644 --- a/Source/Mockolate/Exceptions/MockException.cs +++ b/Source/Mockolate/Exceptions/MockException.cs @@ -7,12 +7,20 @@ namespace Mockolate.Exceptions; /// public class MockException : Exception { - /// + /// + /// Creates a new with the given . + /// + /// Human-readable description of the failure. public MockException(string message) : base(message) { } - /// + /// + /// Creates a new with the given and + /// . + /// + /// Human-readable description of the failure. + /// The underlying exception that triggered this one. public MockException(string message, Exception innerException) : base(message, innerException) { } diff --git a/Source/Mockolate/Exceptions/MockNotSetupException.cs b/Source/Mockolate/Exceptions/MockNotSetupException.cs index 289dad29..de1291a9 100644 --- a/Source/Mockolate/Exceptions/MockNotSetupException.cs +++ b/Source/Mockolate/Exceptions/MockNotSetupException.cs @@ -7,12 +7,20 @@ namespace Mockolate.Exceptions; /// public class MockNotSetupException : MockException { - /// + /// + /// Creates a new with the given . + /// + /// Human-readable description of the missing setup. public MockNotSetupException(string message) : base(message) { } - /// + /// + /// Creates a new with the given and + /// . + /// + /// Human-readable description of the missing setup. + /// The underlying exception that triggered this one. public MockNotSetupException(string message, Exception innerException) : base(message, innerException) { } diff --git a/Source/Mockolate/Exceptions/MockVerificationException.cs b/Source/Mockolate/Exceptions/MockVerificationException.cs index b15bf0af..fbd33a22 100644 --- a/Source/Mockolate/Exceptions/MockVerificationException.cs +++ b/Source/Mockolate/Exceptions/MockVerificationException.cs @@ -7,12 +7,20 @@ namespace Mockolate.Exceptions; /// public class MockVerificationException : MockException { - /// + /// + /// Creates a new with the given . + /// + /// Human-readable description of the verification failure. public MockVerificationException(string message) : base(message) { } - /// + /// + /// Creates a new with the given and + /// . + /// + /// Human-readable description of the verification failure. + /// The underlying exception that triggered this one. public MockVerificationException(string message, Exception innerException) : base(message, innerException) { } diff --git a/Source/Mockolate/Exceptions/MockVerificationTimeoutException.cs b/Source/Mockolate/Exceptions/MockVerificationTimeoutException.cs index 414dfc18..15b3faec 100644 --- a/Source/Mockolate/Exceptions/MockVerificationTimeoutException.cs +++ b/Source/Mockolate/Exceptions/MockVerificationTimeoutException.cs @@ -7,7 +7,12 @@ namespace Mockolate.Exceptions; /// public class MockVerificationTimeoutException : MockVerificationException { - /// + /// + /// Creates a new that records the elapsed + /// and wraps the triggering . + /// + /// The timeout that elapsed, or when the wait was cancelled via a cancellation token. + /// The underlying cancellation or timing exception that triggered this one. public MockVerificationTimeoutException(TimeSpan? timeout, Exception innerException) : base(timeout is null ? "it timed out" : $"it timed out after {timeout.Value}", innerException) { From a071c8e40f9c1f522377891830a1b25abba5f6cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 19:16:42 +0200 Subject: [PATCH 14/23] docs: clarify SetupExtensions Forever/OnlyOnce semantics The 28+ Forever() and OnlyOnce() extension methods used the terse one-liners "Returns/throws forever." / "Uses the return value only once." / "Executes the callback only once." - none said *what* they terminate or how they interact with a multi-entry Returns/Throws/Do sequence. Replace with text that calls out the actual behaviour (terminates the sequence so the last entry repeats rather than cycling back to the first; deactivates a single entry so subsequent invocations fall through to the next entry) and notes the equivalent .For/.Only shortcuts. --- Source/Mockolate/SetupExtensions.cs | 339 +++++++++++++++++++++++----- 1 file changed, 285 insertions(+), 54 deletions(-) diff --git a/Source/Mockolate/SetupExtensions.cs b/Source/Mockolate/SetupExtensions.cs index e5a8f0f1..b6f842af 100644 --- a/Source/Mockolate/SetupExtensions.cs +++ b/Source/Mockolate/SetupExtensions.cs @@ -16,14 +16,23 @@ public static class SetupExtensions extension(IPropertySetupReturnWhenBuilder setup) { /// - /// Returns/throws forever. + /// Terminates the return/throw sequence by repeating the preceding entry forever instead of cycling + /// back to the first entry once the end is reached. /// + /// + /// Equivalent to .For(int.MaxValue). Applies only to the preceding Returns(...)/Throws(...) + /// entry; earlier entries in the sequence still run once each in order. + /// public void Forever() => setup.For(int.MaxValue); /// - /// Uses the return value only once. + /// Deactivates the preceding Returns(...)/Throws(...) entry after a single invocation, + /// so subsequent invocations fall through to the next sequence entry (or to the mock's default behaviour). /// + /// + /// Equivalent to .Only(1). + /// public IPropertySetup OnlyOnce() => setup.Only(1); } @@ -34,8 +43,12 @@ public IPropertySetup OnlyOnce() extension(IPropertyGetterSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IPropertySetup OnlyOnce() => setup.Only(1); } @@ -46,8 +59,12 @@ public IPropertySetup OnlyOnce() extension(IPropertySetterSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IPropertySetup OnlyOnce() => setup.Only(1); } @@ -58,14 +75,22 @@ public IPropertySetup OnlyOnce() extension(IEventSubscriptionSetupCallbackWhenBuilder setup) { /// - /// Repeats the callback forever. + /// Terminates the callback sequence by repeating the preceding Do(...) entry forever instead of + /// cycling back to the first entry once the end is reached. /// + /// + /// Equivalent to .For(int.MaxValue). Applies only to the preceding Do(...) entry. + /// public void Forever() => setup.For(int.MaxValue); /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IEventSetup OnlyOnce() => setup.Only(1); } @@ -76,14 +101,22 @@ public IEventSetup OnlyOnce() extension(IEventUnsubscriptionSetupCallbackWhenBuilder setup) { /// - /// Repeats the callback forever. + /// Terminates the callback sequence by repeating the preceding Do(...) entry forever instead of + /// cycling back to the first entry once the end is reached. /// + /// + /// Equivalent to .For(int.MaxValue). Applies only to the preceding Do(...) entry. + /// public void Forever() => setup.For(int.MaxValue); /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IEventSetup OnlyOnce() => setup.Only(1); } @@ -94,14 +127,23 @@ public IEventSetup OnlyOnce() extension(IIndexerSetupReturnWhenBuilder setup) { /// - /// Returns/throws forever. + /// Terminates the return/throw sequence by repeating the preceding entry forever instead of cycling + /// back to the first entry once the end is reached. /// + /// + /// Equivalent to .For(int.MaxValue). Applies only to the preceding Returns(...)/Throws(...) + /// entry; earlier entries in the sequence still run once each in order. + /// public void Forever() => setup.For(int.MaxValue); /// - /// Uses the return value only once. + /// Deactivates the preceding Returns(...)/Throws(...) entry after a single invocation, + /// so subsequent invocations fall through to the next sequence entry (or to the mock's default behaviour). /// + /// + /// Equivalent to .Only(1). + /// public IIndexerSetup OnlyOnce() => setup.Only(1); } @@ -112,8 +154,12 @@ public IIndexerSetup OnlyOnce() extension(IIndexerGetterSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IIndexerSetup OnlyOnce() => setup.Only(1); } @@ -124,8 +170,12 @@ public IIndexerSetup OnlyOnce() extension(IIndexerSetterSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IIndexerSetup OnlyOnce() => setup.Only(1); } @@ -136,14 +186,23 @@ public IIndexerSetup OnlyOnce() extension(IIndexerSetupReturnWhenBuilder setup) { /// - /// Returns/throws forever. + /// Terminates the return/throw sequence by repeating the preceding entry forever instead of cycling + /// back to the first entry once the end is reached. /// + /// + /// Equivalent to .For(int.MaxValue). Applies only to the preceding Returns(...)/Throws(...) + /// entry; earlier entries in the sequence still run once each in order. + /// public void Forever() => setup.For(int.MaxValue); /// - /// Uses the return value only once. + /// Deactivates the preceding Returns(...)/Throws(...) entry after a single invocation, + /// so subsequent invocations fall through to the next sequence entry (or to the mock's default behaviour). /// + /// + /// Equivalent to .Only(1). + /// public IIndexerSetup OnlyOnce() => setup.Only(1); } @@ -154,8 +213,12 @@ public IIndexerSetup OnlyOnce() extension(IIndexerGetterSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IIndexerSetup OnlyOnce() => setup.Only(1); } @@ -166,8 +229,12 @@ public IIndexerSetup OnlyOnce() extension(IIndexerSetterSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IIndexerSetup OnlyOnce() => setup.Only(1); } @@ -178,14 +245,23 @@ public IIndexerSetup OnlyOnce() extension(IIndexerSetupReturnWhenBuilder setup) { /// - /// Returns/throws forever. + /// Terminates the return/throw sequence by repeating the preceding entry forever instead of cycling + /// back to the first entry once the end is reached. /// + /// + /// Equivalent to .For(int.MaxValue). Applies only to the preceding Returns(...)/Throws(...) + /// entry; earlier entries in the sequence still run once each in order. + /// public void Forever() => setup.For(int.MaxValue); /// - /// Uses the return value only once. + /// Deactivates the preceding Returns(...)/Throws(...) entry after a single invocation, + /// so subsequent invocations fall through to the next sequence entry (or to the mock's default behaviour). /// + /// + /// Equivalent to .Only(1). + /// public IIndexerSetup OnlyOnce() => setup.Only(1); } @@ -196,8 +272,12 @@ public IIndexerSetup OnlyOnce() extension(IIndexerGetterSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IIndexerSetup OnlyOnce() => setup.Only(1); } @@ -208,8 +288,12 @@ public IIndexerSetup OnlyOnce() extension(IIndexerSetterSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IIndexerSetup OnlyOnce() => setup.Only(1); } @@ -220,14 +304,23 @@ public IIndexerSetup OnlyOnce() extension(IIndexerSetupReturnWhenBuilder setup) { /// - /// Returns/throws forever. + /// Terminates the return/throw sequence by repeating the preceding entry forever instead of cycling + /// back to the first entry once the end is reached. /// + /// + /// Equivalent to .For(int.MaxValue). Applies only to the preceding Returns(...)/Throws(...) + /// entry; earlier entries in the sequence still run once each in order. + /// public void Forever() => setup.For(int.MaxValue); /// - /// Uses the return value only once. + /// Deactivates the preceding Returns(...)/Throws(...) entry after a single invocation, + /// so subsequent invocations fall through to the next sequence entry (or to the mock's default behaviour). /// + /// + /// Equivalent to .Only(1). + /// public IIndexerSetup OnlyOnce() => setup.Only(1); } @@ -238,8 +331,12 @@ public IIndexerSetup OnlyOnce() extension(IIndexerGetterSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IIndexerSetup OnlyOnce() => setup.Only(1); } @@ -250,8 +347,12 @@ public IIndexerSetup OnlyOnce() extension(IIndexerSetterSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IIndexerSetup OnlyOnce() => setup.Only(1); } @@ -262,14 +363,23 @@ public IIndexerSetup OnlyOnce() extension(IReturnMethodSetupReturnWhenBuilder setup) { /// - /// Returns/throws forever. + /// Terminates the return/throw sequence by repeating the preceding entry forever instead of cycling + /// back to the first entry once the end is reached. /// + /// + /// Equivalent to .For(int.MaxValue). Applies only to the preceding Returns(...)/Throws(...) + /// entry; earlier entries in the sequence still run once each in order. + /// public void Forever() => setup.For(int.MaxValue); /// - /// Uses the return value only once. + /// Deactivates the preceding Returns(...)/Throws(...) entry after a single invocation, + /// so subsequent invocations fall through to the next sequence entry (or to the mock's default behaviour). /// + /// + /// Equivalent to .Only(1). + /// public IReturnMethodSetup OnlyOnce() => setup.Only(1); } @@ -280,8 +390,12 @@ public IReturnMethodSetup OnlyOnce() extension(IReturnMethodSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IReturnMethodSetup OnlyOnce() => setup.Only(1); } @@ -292,14 +406,23 @@ public IReturnMethodSetup OnlyOnce() extension(IReturnMethodSetupReturnWhenBuilder setup) { /// - /// Returns/throws forever. + /// Terminates the return/throw sequence by repeating the preceding entry forever instead of cycling + /// back to the first entry once the end is reached. /// + /// + /// Equivalent to .For(int.MaxValue). Applies only to the preceding Returns(...)/Throws(...) + /// entry; earlier entries in the sequence still run once each in order. + /// public void Forever() => setup.For(int.MaxValue); /// - /// Uses the return value only once. + /// Deactivates the preceding Returns(...)/Throws(...) entry after a single invocation, + /// so subsequent invocations fall through to the next sequence entry (or to the mock's default behaviour). /// + /// + /// Equivalent to .Only(1). + /// public IReturnMethodSetup OnlyOnce() => setup.Only(1); } @@ -310,8 +433,12 @@ public IReturnMethodSetup OnlyOnce() extension(IReturnMethodSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IReturnMethodSetup OnlyOnce() => setup.Only(1); } @@ -322,14 +449,23 @@ public IReturnMethodSetup OnlyOnce() extension(IReturnMethodSetupReturnWhenBuilder setup) { /// - /// Returns/throws forever. + /// Terminates the return/throw sequence by repeating the preceding entry forever instead of cycling + /// back to the first entry once the end is reached. /// + /// + /// Equivalent to .For(int.MaxValue). Applies only to the preceding Returns(...)/Throws(...) + /// entry; earlier entries in the sequence still run once each in order. + /// public void Forever() => setup.For(int.MaxValue); /// - /// Uses the return value only once. + /// Deactivates the preceding Returns(...)/Throws(...) entry after a single invocation, + /// so subsequent invocations fall through to the next sequence entry (or to the mock's default behaviour). /// + /// + /// Equivalent to .Only(1). + /// public IReturnMethodSetup OnlyOnce() => setup.Only(1); } @@ -340,8 +476,12 @@ public IReturnMethodSetup OnlyOnce() extension(IReturnMethodSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IReturnMethodSetup OnlyOnce() => setup.Only(1); } @@ -352,14 +492,23 @@ public IReturnMethodSetup OnlyOnce() extension(IReturnMethodSetupReturnWhenBuilder setup) { /// - /// Returns/throws forever. + /// Terminates the return/throw sequence by repeating the preceding entry forever instead of cycling + /// back to the first entry once the end is reached. /// + /// + /// Equivalent to .For(int.MaxValue). Applies only to the preceding Returns(...)/Throws(...) + /// entry; earlier entries in the sequence still run once each in order. + /// public void Forever() => setup.For(int.MaxValue); /// - /// Uses the return value only once. + /// Deactivates the preceding Returns(...)/Throws(...) entry after a single invocation, + /// so subsequent invocations fall through to the next sequence entry (or to the mock's default behaviour). /// + /// + /// Equivalent to .Only(1). + /// public IReturnMethodSetup OnlyOnce() => setup.Only(1); } @@ -370,8 +519,12 @@ public IReturnMethodSetup OnlyOnce() extension(IReturnMethodSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IReturnMethodSetup OnlyOnce() => setup.Only(1); } @@ -382,14 +535,23 @@ public IReturnMethodSetup OnlyOnce() extension(IReturnMethodSetupReturnWhenBuilder setup) { /// - /// Returns/throws forever. + /// Terminates the return/throw sequence by repeating the preceding entry forever instead of cycling + /// back to the first entry once the end is reached. /// + /// + /// Equivalent to .For(int.MaxValue). Applies only to the preceding Returns(...)/Throws(...) + /// entry; earlier entries in the sequence still run once each in order. + /// public void Forever() => setup.For(int.MaxValue); /// - /// Uses the return value only once. + /// Deactivates the preceding Returns(...)/Throws(...) entry after a single invocation, + /// so subsequent invocations fall through to the next sequence entry (or to the mock's default behaviour). /// + /// + /// Equivalent to .Only(1). + /// public IReturnMethodSetup OnlyOnce() => setup.Only(1); } @@ -400,8 +562,12 @@ public IReturnMethodSetup OnlyOnce() extension(IReturnMethodSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IReturnMethodSetup OnlyOnce() => setup.Only(1); } @@ -412,14 +578,23 @@ public IReturnMethodSetup OnlyOnce() extension(IVoidMethodSetupReturnWhenBuilder setup) { /// - /// Returns/throws forever. + /// Terminates the return/throw sequence by repeating the preceding entry forever instead of cycling + /// back to the first entry once the end is reached. /// + /// + /// Equivalent to .For(int.MaxValue). Applies only to the preceding Returns(...)/Throws(...) + /// entry; earlier entries in the sequence still run once each in order. + /// public void Forever() => setup.For(int.MaxValue); /// - /// Uses the return value only once. + /// Deactivates the preceding Returns(...)/Throws(...) entry after a single invocation, + /// so subsequent invocations fall through to the next sequence entry (or to the mock's default behaviour). /// + /// + /// Equivalent to .Only(1). + /// public IVoidMethodSetup OnlyOnce() => setup.Only(1); } @@ -430,8 +605,12 @@ public IVoidMethodSetup OnlyOnce() extension(IVoidMethodSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IVoidMethodSetup OnlyOnce() => setup.Only(1); } @@ -442,14 +621,23 @@ public IVoidMethodSetup OnlyOnce() extension(IVoidMethodSetupReturnWhenBuilder setup) { /// - /// Returns/throws forever. + /// Terminates the return/throw sequence by repeating the preceding entry forever instead of cycling + /// back to the first entry once the end is reached. /// + /// + /// Equivalent to .For(int.MaxValue). Applies only to the preceding Returns(...)/Throws(...) + /// entry; earlier entries in the sequence still run once each in order. + /// public void Forever() => setup.For(int.MaxValue); /// - /// Uses the return value only once. + /// Deactivates the preceding Returns(...)/Throws(...) entry after a single invocation, + /// so subsequent invocations fall through to the next sequence entry (or to the mock's default behaviour). /// + /// + /// Equivalent to .Only(1). + /// public IVoidMethodSetup OnlyOnce() => setup.Only(1); } @@ -460,8 +648,12 @@ public IVoidMethodSetup OnlyOnce() extension(IVoidMethodSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IVoidMethodSetup OnlyOnce() => setup.Only(1); } @@ -472,14 +664,23 @@ public IVoidMethodSetup OnlyOnce() extension(IVoidMethodSetupReturnWhenBuilder setup) { /// - /// Returns/throws forever. + /// Terminates the return/throw sequence by repeating the preceding entry forever instead of cycling + /// back to the first entry once the end is reached. /// + /// + /// Equivalent to .For(int.MaxValue). Applies only to the preceding Returns(...)/Throws(...) + /// entry; earlier entries in the sequence still run once each in order. + /// public void Forever() => setup.For(int.MaxValue); /// - /// Uses the return value only once. + /// Deactivates the preceding Returns(...)/Throws(...) entry after a single invocation, + /// so subsequent invocations fall through to the next sequence entry (or to the mock's default behaviour). /// + /// + /// Equivalent to .Only(1). + /// public IVoidMethodSetup OnlyOnce() => setup.Only(1); } @@ -490,8 +691,12 @@ public IVoidMethodSetup OnlyOnce() extension(IVoidMethodSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IVoidMethodSetup OnlyOnce() => setup.Only(1); } @@ -502,14 +707,23 @@ public IVoidMethodSetup OnlyOnce() extension(IVoidMethodSetupReturnWhenBuilder setup) { /// - /// Returns/throws forever. + /// Terminates the return/throw sequence by repeating the preceding entry forever instead of cycling + /// back to the first entry once the end is reached. /// + /// + /// Equivalent to .For(int.MaxValue). Applies only to the preceding Returns(...)/Throws(...) + /// entry; earlier entries in the sequence still run once each in order. + /// public void Forever() => setup.For(int.MaxValue); /// - /// Uses the return value only once. + /// Deactivates the preceding Returns(...)/Throws(...) entry after a single invocation, + /// so subsequent invocations fall through to the next sequence entry (or to the mock's default behaviour). /// + /// + /// Equivalent to .Only(1). + /// public IVoidMethodSetup OnlyOnce() => setup.Only(1); } @@ -520,8 +734,12 @@ public IVoidMethodSetup OnlyOnce() extension(IVoidMethodSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IVoidMethodSetup OnlyOnce() => setup.Only(1); } @@ -532,14 +750,23 @@ public IVoidMethodSetup OnlyOnce() extension(IVoidMethodSetupReturnWhenBuilder setup) { /// - /// Returns/throws forever. + /// Terminates the return/throw sequence by repeating the preceding entry forever instead of cycling + /// back to the first entry once the end is reached. /// + /// + /// Equivalent to .For(int.MaxValue). Applies only to the preceding Returns(...)/Throws(...) + /// entry; earlier entries in the sequence still run once each in order. + /// public void Forever() => setup.For(int.MaxValue); /// - /// Uses the return value only once. + /// Deactivates the preceding Returns(...)/Throws(...) entry after a single invocation, + /// so subsequent invocations fall through to the next sequence entry (or to the mock's default behaviour). /// + /// + /// Equivalent to .Only(1). + /// public IVoidMethodSetup OnlyOnce() => setup.Only(1); } @@ -550,8 +777,12 @@ public IVoidMethodSetup OnlyOnce() extension(IVoidMethodSetupCallbackWhenBuilder setup) { /// - /// Executes the callback only once. + /// Deactivates the preceding Do(...) callback after a single invocation, so subsequent invocations + /// fall through to the next callback in the sequence (or are skipped). /// + /// + /// Equivalent to .Only(1). + /// public IVoidMethodSetup OnlyOnce() => setup.Only(1); } From cdcd4b9d90b29b2f74571f50830229e7daf4201c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 19:19:14 +0200 Subject: [PATCH 15/23] docs: clarify IDefaultValueGenerator.GenerateValue parameters The params object?[] parameters argument was documented only as "for context", which left readers unable to know what to expect there or to what extent they can rely on it when implementing IDefaultValueGenerator. Add / docs explaining that the library itself does not populate parameters, that the array is forwarded from DefaultValueFactory's Generate(...) helper when the user's factory uses it, and what the null-return fallback semantic is. Mirror the docs on DefaultValueFactory's override. --- Source/Mockolate/DefaultValueFactory.cs | 9 +++++++-- Source/Mockolate/IDefaultValueGenerator.cs | 18 +++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/Source/Mockolate/DefaultValueFactory.cs b/Source/Mockolate/DefaultValueFactory.cs index c63290aa..d1a1b802 100644 --- a/Source/Mockolate/DefaultValueFactory.cs +++ b/Source/Mockolate/DefaultValueFactory.cs @@ -34,9 +34,14 @@ public virtual bool CanGenerateValue(Type type) => _predicate?.Invoke(type) ?? false; /// - /// Generates a default value of the specified , with - /// the for context. + /// Generates a default value for using the registered generator delegate. /// + /// The runtime type to produce a default value for. + /// + /// Optional context forwarded from the caller; typically empty. See + /// for details. + /// + /// The generated value, or when no generator has been configured. public virtual object? GenerateValue(Type type, params object?[] parameters) => _generator?.Invoke(type, parameters); } diff --git a/Source/Mockolate/IDefaultValueGenerator.cs b/Source/Mockolate/IDefaultValueGenerator.cs index 27c040a1..9a13b7f8 100644 --- a/Source/Mockolate/IDefaultValueGenerator.cs +++ b/Source/Mockolate/IDefaultValueGenerator.cs @@ -3,14 +3,26 @@ namespace Mockolate; /// -/// Defines a mechanism for generating default values of a specified type. +/// Defines a mechanism for generating default values of a specified type. Populated on +/// and consulted whenever a mock needs a return value without a +/// matching setup. /// public interface IDefaultValueGenerator { /// - /// Generates a default value of the specified , with - /// the for context. + /// Generates a default value for . /// + /// The runtime type to produce a default value for. + /// + /// Optional context passed through from a caller of a 's + /// Generate<T>(T nullValue, params object?[] parameters) helper. The library itself does + /// not populate this array; treat it as /empty unless your factory specifically + /// relies on user-supplied context. + /// + /// + /// A default value assignable to , or if no value can be + /// produced. The caller will fall back to the language default if the returned value is not assignment-compatible. + /// object? GenerateValue(Type type, params object?[] parameters); } From 8ad9486847c0d0559ceca4301c0f4671267f4b67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 19:26:27 +0200 Subject: [PATCH 16/23] docs: correct IDefaultValueGenerator.GenerateValue parameters description Previous commit incorrectly stated the library does not populate the `parameters` array. It does: the generator forwards the mocked invocation's runtime arguments into `IDefaultValueGenerator.GenerateValue` (and the `DefaultValueFactory.Generate` extension) so factories can react to context. The built-in `CancellableTaskFactory` / `CancellableValueTaskFactory` specifically scan the array for a cancelled `CancellationToken` to return `Task.FromCanceled` instead of `Task.CompletedTask`. Rewrite the docs on both the interface and `DefaultValueFactory.GenerateValue` to describe this correctly. --- Source/Mockolate/DefaultValueFactory.cs | 4 +++- Source/Mockolate/IDefaultValueGenerator.cs | 16 ++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Source/Mockolate/DefaultValueFactory.cs b/Source/Mockolate/DefaultValueFactory.cs index d1a1b802..34217068 100644 --- a/Source/Mockolate/DefaultValueFactory.cs +++ b/Source/Mockolate/DefaultValueFactory.cs @@ -38,7 +38,9 @@ public virtual bool CanGenerateValue(Type type) /// /// The runtime type to produce a default value for. /// - /// Optional context forwarded from the caller; typically empty. See + /// The runtime arguments of the mocked invocation, forwarded so the factory can vary its result by + /// context (for example, returning a cancelled task when a cancelled + /// is present). See /// for details. /// /// The generated value, or when no generator has been configured. diff --git a/Source/Mockolate/IDefaultValueGenerator.cs b/Source/Mockolate/IDefaultValueGenerator.cs index 9a13b7f8..a247b089 100644 --- a/Source/Mockolate/IDefaultValueGenerator.cs +++ b/Source/Mockolate/IDefaultValueGenerator.cs @@ -10,18 +10,22 @@ namespace Mockolate; public interface IDefaultValueGenerator { /// - /// Generates a default value for . + /// Generates a default value for , optionally using the invocation + /// for context. /// /// The runtime type to produce a default value for. /// - /// Optional context passed through from a caller of a 's - /// Generate<T>(T nullValue, params object?[] parameters) helper. The library itself does - /// not populate this array; treat it as /empty unless your factory specifically - /// relies on user-supplied context. + /// The runtime arguments of the mocked invocation, in declaration order (for tuple defaults, nested + /// Func<T> factories for each tuple element are appended). Implementations can inspect this + /// array to return a more appropriate default - for example, the built-in cancellable-task factory scans it + /// for a cancelled and returns + /// instead of + /// . /// /// /// A default value assignable to , or if no value can be - /// produced. The caller will fall back to the language default if the returned value is not assignment-compatible. + /// produced. The caller will fall back to the language default if the returned value is not + /// assignment-compatible. /// object? GenerateValue(Type type, params object?[] parameters); } From 9dd85de9ee698f73fa2c559a4202f661ebfffa60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 21:38:07 +0200 Subject: [PATCH 17/23] Update Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs index dcbb8aef..b98fe8ae 100644 --- a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs +++ b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs @@ -2693,7 +2693,7 @@ private static void AppendOverloadDifferentiatorRemark(StringBuilder sb, else if (valueFlags.All(x => x)) { text = isVerify - ? "This overload accepts direct values for every parameter and returns a whose drops per-parameter matching entirely." + ? "This overload accepts direct values for every parameter and returns a whose drops per-parameter matching entirely." : "This overload accepts direct values for every parameter; each is treated as It.Is<T>(value)."; } else From c416ddbd1c4efc31c9942136b0535cfd21f32f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 21:38:19 +0200 Subject: [PATCH 18/23] Update Source/Mockolate/Verify/VerificationResultExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Source/Mockolate/Verify/VerificationResultExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Mockolate/Verify/VerificationResultExtensions.cs b/Source/Mockolate/Verify/VerificationResultExtensions.cs index 7ffae187..431f06fe 100644 --- a/Source/Mockolate/Verify/VerificationResultExtensions.cs +++ b/Source/Mockolate/Verify/VerificationResultExtensions.cs @@ -131,7 +131,8 @@ public void AtLeastTwice() /// Asserts that the verified interaction occurred at most times. /// /// - /// Thrown when the verified interaction occurred more than times. + /// Thrown when the verified interaction occurred more than times, + /// or when a timeout elapses first. /// public void AtMost(int times) { From 0eb30d922011c1a322999a3fd817f5b0f165b103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 21:38:28 +0200 Subject: [PATCH 19/23] Update Source/Mockolate/Verify/VerificationResultExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Source/Mockolate/Verify/VerificationResultExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Mockolate/Verify/VerificationResultExtensions.cs b/Source/Mockolate/Verify/VerificationResultExtensions.cs index 431f06fe..1e6e716d 100644 --- a/Source/Mockolate/Verify/VerificationResultExtensions.cs +++ b/Source/Mockolate/Verify/VerificationResultExtensions.cs @@ -207,7 +207,8 @@ public void Between(int minimum, int maximum) /// Asserts that the verified interaction occurred at most once. /// /// - /// Thrown when the verified interaction occurred more than once. + /// Thrown when the verified interaction occurred more than once, or when a preceding Within or + /// WithCancellation causes verification to time out or be cancelled. /// public void AtMostOnce() { From 016fde73e302a8eed9a2fa4a6bc0beb7024486b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 21:38:39 +0200 Subject: [PATCH 20/23] Update Source/Mockolate/Verify/VerificationResultExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Source/Mockolate/Verify/VerificationResultExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Mockolate/Verify/VerificationResultExtensions.cs b/Source/Mockolate/Verify/VerificationResultExtensions.cs index 1e6e716d..7dc51f8b 100644 --- a/Source/Mockolate/Verify/VerificationResultExtensions.cs +++ b/Source/Mockolate/Verify/VerificationResultExtensions.cs @@ -237,7 +237,8 @@ public void AtMostOnce() /// Asserts that the verified interaction occurred at most twice. /// /// - /// Thrown when the verified interaction occurred more than two times. + /// Thrown when the verified interaction occurred more than two times, or when verification fails because a + /// configured wait or cancellation condition times out or is cancelled. /// public void AtMostTwice() { From c85587b039210cacd18fcb23c1fa7679b8cad28c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 21:38:48 +0200 Subject: [PATCH 21/23] Update Source/Mockolate/Verify/VerificationResultExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Source/Mockolate/Verify/VerificationResultExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Mockolate/Verify/VerificationResultExtensions.cs b/Source/Mockolate/Verify/VerificationResultExtensions.cs index 7dc51f8b..a12012f2 100644 --- a/Source/Mockolate/Verify/VerificationResultExtensions.cs +++ b/Source/Mockolate/Verify/VerificationResultExtensions.cs @@ -297,7 +297,8 @@ public void Exactly(int times) /// Asserts that the verified interaction never occurred. /// /// - /// Thrown when the verified interaction occurred at least once. + /// Thrown when the verified interaction occurred at least once, + /// or when a timeout elapses first. /// public void Never() { From ad825e080c4985e0e3c55f9294fc701b671e639a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 21:38:58 +0200 Subject: [PATCH 22/23] Update Source/Mockolate/Match.Parameters.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Source/Mockolate/Match.Parameters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Mockolate/Match.Parameters.cs b/Source/Mockolate/Match.Parameters.cs index a6f7bf61..7d6593c4 100644 --- a/Source/Mockolate/Match.Parameters.cs +++ b/Source/Mockolate/Match.Parameters.cs @@ -18,7 +18,7 @@ public partial class Match /// /// /// Populated by the compiler via to include the source - /// expression of in the setup's and in + /// expression of in the setup's and in /// verification failure messages. /// /// From 885c5ac51a06476bf28c7c047531b6c67e75df21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 19 Apr 2026 21:37:15 +0200 Subject: [PATCH 23/23] Fix review issue --- .../Sources/Sources.Mock.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Source/Mockolate.SourceGenerators/Sources/Sources.Mock.cs b/Source/Mockolate.SourceGenerators/Sources/Sources.Mock.cs index a0d9cb45..6373e9a1 100644 --- a/Source/Mockolate.SourceGenerators/Sources/Sources.Mock.cs +++ b/Source/Mockolate.SourceGenerators/Sources/Sources.Mock.cs @@ -89,10 +89,17 @@ internal interface IMockGenerationDidNotRun {} extension(global::Mockolate.Mock.IMockGenerationDidNotRun _) { /// - /// Add an interface that the mock also implements. + /// Fallback Implementing that is only resolved when the Mockolate source generator did not run or when + /// is not mockable. Calling it always throws a . /// /// Additional interface the mock should implement. - /// The same mock, typed so that members are accessible. + /// This method never returns - it always throws. + /// + /// The source generator emits a concrete Implementing overload per mockable type with the same shape. + /// If you see this fallback resolved in your IDE, the generator did not run for ; + /// run a clean build (for example dotnet clean && dotnet build) and verify that the type is mockable. + /// + /// Always thrown: the source generator did not run or is not mockable. public global::Mockolate.Mock.IMockGenerationDidNotRun Implementing() where TInterface : class { throw new global::Mockolate.Exceptions.MockException($"This method should not be called directly. Either '{typeof(TInterface)}' is not mockable or the source generator did not run correctly.");