Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/Verify.Tests/ThrowIfVerifyHasBeenRunTests.cs
Original file line number Diff line number Diff line change
@@ -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<Exception>(
() => VerifierSettings.IgnoreMembers<string>("TheMember"));

Assert.Contains("The API 'IgnoreMembers'", exception.Message);
Assert.DoesNotContain(nameof(NamesTheApi), exception.Message);
}
finally
{
InnerVerifier.verifyHasBeenRun = original;
}
}
}
16 changes: 8 additions & 8 deletions src/Verify/Verifier/InnerVerifier.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using StackTrace = System.Diagnostics.StackTrace;

namespace VerifyTests;

public partial class InnerVerifier :
Expand All @@ -24,18 +22,20 @@ public partial class InnerVerifier :
// inline is not compatible with.
string? pathPrefixReceived;

[MethodImpl(MethodImplOptions.NoInlining)]
public static void ThrowIfVerifyHasBeenRun()
/// <param name="api">
/// 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.
/// </param>
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(
Expand Down
2 changes: 1 addition & 1 deletion src/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading