diff --git a/src/Verify.Tests/ThrowIfVerifyHasBeenRunTests.cs b/src/Verify.Tests/ThrowIfVerifyHasBeenRunTests.cs new file mode 100644 index 000000000..40e71963e --- /dev/null +++ b/src/Verify.Tests/ThrowIfVerifyHasBeenRunTests.cs @@ -0,0 +1,23 @@ +public class ThrowIfVerifyHasBeenRunTests +{ + // The message points at the API that must move to a module initializer, so it has to + // name that API and not the code that called it + [Fact] + public void NamesTheApi() + { + var original = InnerVerifier.verifyHasBeenRun; + InnerVerifier.verifyHasBeenRun = true; + try + { + var exception = Assert.Throws( + () => VerifierSettings.IgnoreMembers("TheMember")); + + Assert.Contains("The API 'IgnoreMembers'", exception.Message); + Assert.DoesNotContain(nameof(NamesTheApi), exception.Message); + } + finally + { + InnerVerifier.verifyHasBeenRun = original; + } + } +} diff --git a/src/Verify/Verifier/InnerVerifier.cs b/src/Verify/Verifier/InnerVerifier.cs index d901aab41..99b3d209b 100644 --- a/src/Verify/Verifier/InnerVerifier.cs +++ b/src/Verify/Verifier/InnerVerifier.cs @@ -1,5 +1,3 @@ -using StackTrace = System.Diagnostics.StackTrace; - namespace VerifyTests; public partial class InnerVerifier : @@ -24,18 +22,20 @@ public partial class InnerVerifier : // inline is not compatible with. string? pathPrefixReceived; - [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowIfVerifyHasBeenRun() + /// + /// Defaulted from the calling member, so it names the API that has the restriction. + /// Reading it off a StackTrace instead only works while that API has a frame of its + /// own: the JIT is free to inline it into the caller, and does on net48 in release, + /// which put the caller's name in the message. + /// + public static void ThrowIfVerifyHasBeenRun([CallerMemberName] string api = "") { if (!verifyHasBeenRun) { return; } - var stackTrace = new StackTrace(1, false); - var method = stackTrace.GetFrame(1)!.GetMethod()!; - var type = method.DeclaringType; - throw new($"The API '{type}.{method.Name}' must be called prior to any Verify has run. Usually this is done in a [ModuleInitializer]. Verify run by: {verifyHasBeenRunBy}"); + throw new($"The API '{api}' must be called prior to any Verify has run. Usually this is done in a [ModuleInitializer]. Verify run by: {verifyHasBeenRunBy}"); } public InnerVerifier( diff --git a/src/todo.md b/src/todo.md index e40e69c54..b47d61378 100644 --- a/src/todo.md +++ b/src/todo.md @@ -56,7 +56,7 @@ All six resolved 2026-08-16 (five fixed here; the inline item resolved as by-des - [x] **`Delete:` section drops subdirectories, breaking the parse round-trip.** `Verify/Verifier/VerifyExceptionMessageBuilder.cs:62` emits `Path.GetFileName(file)` while the other sections emit directory-relative paths, and `Verify.ExceptionParsing/Parser.cs:109` reconstructs with `Path.Combine(directory, name)`. For `UseUniqueDirectory()`/`VerifyDirectory` tests, a stale `{Directory}\Type.Method\old.verified.txt` parses back as the nonexistent `{Directory}\old.verified.txt`; same-named files in different subdirectories collapse. -- [ ] **`ThrowIfVerifyHasBeenRun` blames the caller instead of the API.** +- [x] **`ThrowIfVerifyHasBeenRun` blames the caller instead of the API.** `Verify/Verifier/InnerVerifier.cs:35-38` — `new StackTrace(1, false)` already skips the guard, so frame 0 is the guarded API; `GetFrame(1)` fetches the API's caller. The message names the user's own method as "The API". Runtime-reproduced (Debug and Release). Fix: `GetFrame(0)`. ## Minor / edge cases