diff --git a/tests/Wolfgang.TryPattern.Tests.Unit/DocExampleCompilationTests.cs b/tests/Wolfgang.TryPattern.Tests.Unit/DocExampleCompilationTests.cs index 5e3529c6..81167af4 100644 --- a/tests/Wolfgang.TryPattern.Tests.Unit/DocExampleCompilationTests.cs +++ b/tests/Wolfgang.TryPattern.Tests.Unit/DocExampleCompilationTests.cs @@ -45,14 +45,9 @@ public class DocExampleCompilationTests public static IEnumerable AllDocExamples() { + // A missing XML doc file (GenerateDocumentationFile off) throws + // FileNotFoundException from Load, naming the path. string xmlPath = Path.ChangeExtension(LibraryAssembly.Location, ".xml"); - if (!File.Exists(xmlPath)) - { - // Missing XML doc: repo-level GenerateDocumentationFile is required. - // Test harness will surface this as "no examples found". - yield break; - } - XDocument doc = XDocument.Load(xmlPath); int index = 0; foreach (XElement member in doc.Descendants("member")) @@ -148,14 +143,19 @@ private static void Process(int item) { } .Where(d => d.Severity == DiagnosticSeverity.Error) .ToList(); - if (errors.Count > 0) - { - Assert.Fail(CompileFailureMessage.Format( + // Unconditional so the message builder runs on a pass too (a + // failure-only branch can never reach the 100% test-coverage gate). + Assert.True + ( + errors.Count == 0, + CompileFailureMessage.Format + ( $"XML doc example on `{memberName}` (example index {index}) failed to compile.", errors, snippet, - "Fix the snippet in the source XML doc, or if it needs an additional using, add it inline in the `` block.")); - } + "Fix the snippet in the source XML doc, or if it needs an additional using, add it inline in the `` block." + ) + ); } } diff --git a/tests/Wolfgang.TryPattern.Tests.Unit/PropertyTests.cs b/tests/Wolfgang.TryPattern.Tests.Unit/PropertyTests.cs index 81949270..674f2296 100644 --- a/tests/Wolfgang.TryPattern.Tests.Unit/PropertyTests.cs +++ b/tests/Wolfgang.TryPattern.Tests.Unit/PropertyTests.cs @@ -45,18 +45,13 @@ public Property Try_Run_of_non_throwing_action_always_succeeds() public Property Try_Run_of_throwing_action_carries_message_round_trip(NonEmptyString message) { if (message is null) throw new ArgumentNullException(nameof(message)); - // NonEmptyString excludes only the empty string; Result.Failure - // additionally rejects whitespace-only strings and there's a - // latent bug (#273) that lets a whitespace exception message - // escape from Try.Run. Skip that case so the fast-path - // property is well-formed vs the current API. + // NonEmptyString can still be whitespace-only. Since #273 Try.Run + // reports such a message as the exception's type name, because + // Result.Failure rejects whitespace. string msg = message.Get; - if (string.IsNullOrWhiteSpace(msg)) - { - return true.ToProperty(); - } + string expected = string.IsNullOrWhiteSpace(msg) ? nameof(InvalidOperationException) : msg; Result r = Try.Run(() => throw new InvalidOperationException(msg)); - return (r.Failed && string.Equals(r.ErrorMessage, msg, StringComparison.Ordinal)).ToProperty(); + return (r.Failed && string.Equals(r.ErrorMessage, expected, StringComparison.Ordinal)).ToProperty(); } diff --git a/tests/Wolfgang.TryPattern.Tests.Unit/ReadmeExampleCompilationTests.cs b/tests/Wolfgang.TryPattern.Tests.Unit/ReadmeExampleCompilationTests.cs index 97cb2a80..7cb3cd41 100644 --- a/tests/Wolfgang.TryPattern.Tests.Unit/ReadmeExampleCompilationTests.cs +++ b/tests/Wolfgang.TryPattern.Tests.Unit/ReadmeExampleCompilationTests.cs @@ -67,12 +67,9 @@ public class ReadmeExampleCompilationTests public static IEnumerable AllReadmeFences() { + // LocateReadme returns a non-existent path when nothing is found, + // so ReadAllText throws FileNotFoundException naming it. string readmePath = LocateReadme(); - if (!File.Exists(readmePath)) - { - yield break; - } - string content = File.ReadAllText(readmePath); // Matches ```csharp\n...content...\n``` — the ``` opener/closer // must be at the start of its own line. `Singleline` lets `.` @@ -160,14 +157,19 @@ private static void Process(int item) { } .Where(d => d.Severity == DiagnosticSeverity.Error) .ToList(); - if (errors.Count > 0) - { - Assert.Fail(CompileFailureMessage.Format( + // Unconditional so the message builder runs on a pass too (a + // failure-only branch can never reach the 100% test-coverage gate). + Assert.True + ( + errors.Count == 0, + CompileFailureMessage.Format + ( $"README fence #{index} failed to compile.", errors, snippet, - "Fix the fence in README.md, or if it uses framework types the test project doesn't reference (SqlConnection, ASP.NET Core, EF dbContext), add a marker string to ReadmeExampleCompilationTests.SkipMarkers.")); - } + "Fix the fence in README.md, or if it uses framework types the test project doesn't reference (SqlConnection, ASP.NET Core, EF dbContext), add a marker string to ReadmeExampleCompilationTests.SkipMarkers." + ) + ); } @@ -177,16 +179,11 @@ private static string LocateReadme() // README.md. On CI the test runs from bin/Release/net10.0/, // so the repo root is a few levels up. string? dir = AppContext.BaseDirectory; - for (int depth = 0; depth < 10 && dir is not null; depth++) + while (dir is not null && !File.Exists(Path.Combine(dir, "README.md"))) { - string candidate = Path.Combine(dir, "README.md"); - if (File.Exists(candidate)) - { - return candidate; - } dir = Path.GetDirectoryName(dir); } - return "README.md"; // last-ditch, caller checks Exists + return Path.Combine(dir ?? AppContext.BaseDirectory, "README.md"); } } diff --git a/tests/Wolfgang.TryPattern.Tests.Unit/RunAsyncFuncTests.cs b/tests/Wolfgang.TryPattern.Tests.Unit/RunAsyncFuncTests.cs index 4d681f10..a1c060cb 100644 --- a/tests/Wolfgang.TryPattern.Tests.Unit/RunAsyncFuncTests.cs +++ b/tests/Wolfgang.TryPattern.Tests.Unit/RunAsyncFuncTests.cs @@ -339,11 +339,15 @@ public async Task RunAsync_Func_CancellationToken_int_when_cancellation_is_reque using var cts = new CancellationTokenSource(); var functionStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - async Task Function() + // Never completes on its own: the only way out is the token, so + // there is no unreachable `return` after an infinite delay. + var pending = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var registration = cts.Token.Register(() => pending.TrySetCanceled(cts.Token)); + + Task Function() { functionStarted.TrySetResult(true); - await Task.Delay(Timeout.Infinite, cts.Token); - return 42; + return pending.Task; } var task = Try.RunAsync((Func>)Function, cts.Token); @@ -374,6 +378,11 @@ Task Function() // Act & Assert await Assert.ThrowsAsync(() => Try.RunAsync((Func>)Function, cts.Token)); Assert.False(wasInvoked, "function should not be invoked when the token is already canceled"); + + // Positive control: the probe does flip when the function runs, so + // the assertion above is not vacuous. + await Function(); + Assert.True(wasInvoked); } @@ -395,5 +404,10 @@ public async Task RunAsync_Func_nullable_when_token_is_already_canceled_throws_O // Act & Assert await Assert.ThrowsAsync(() => Try.RunAsync((Func>)Function, cts.Token)); Assert.False(wasInvoked, "function should not be invoked when the token is already canceled"); + + // Positive control: the probe does flip when the function runs, so + // the assertion above is not vacuous. + await Function(); + Assert.True(wasInvoked); } }