Skip to content

Commit 1e73f28

Browse files
authored
Name the guarded API in ThrowIfVerifyHasBeenRun (#1864)
The message named the calling code as "The API", pointing at the wrong place to move into a module initializer. Reading the name off a StackTrace cannot fix that: whether the guarded API has a frame of its own depends on the JIT, and on net48 in release it is inlined into the caller, so no frame index is right. The name now comes from CallerMemberName, which the compiler fills in at each call site, so it is the same under every runtime and configuration. That drops the declaring type from the message, since only the member name is available.
1 parent 700ddb5 commit 1e73f28

3 files changed

Lines changed: 32 additions & 9 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
public class ThrowIfVerifyHasBeenRunTests
2+
{
3+
// The message points at the API that must move to a module initializer, so it has to
4+
// name that API and not the code that called it
5+
[Fact]
6+
public void NamesTheApi()
7+
{
8+
var original = InnerVerifier.verifyHasBeenRun;
9+
InnerVerifier.verifyHasBeenRun = true;
10+
try
11+
{
12+
var exception = Assert.Throws<Exception>(
13+
() => VerifierSettings.IgnoreMembers<string>("TheMember"));
14+
15+
Assert.Contains("The API 'IgnoreMembers'", exception.Message);
16+
Assert.DoesNotContain(nameof(NamesTheApi), exception.Message);
17+
}
18+
finally
19+
{
20+
InnerVerifier.verifyHasBeenRun = original;
21+
}
22+
}
23+
}

src/Verify/Verifier/InnerVerifier.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using StackTrace = System.Diagnostics.StackTrace;
2-
31
namespace VerifyTests;
42

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

27-
[MethodImpl(MethodImplOptions.NoInlining)]
28-
public static void ThrowIfVerifyHasBeenRun()
25+
/// <param name="api">
26+
/// Defaulted from the calling member, so it names the API that has the restriction.
27+
/// Reading it off a StackTrace instead only works while that API has a frame of its
28+
/// own: the JIT is free to inline it into the caller, and does on net48 in release,
29+
/// which put the caller's name in the message.
30+
/// </param>
31+
public static void ThrowIfVerifyHasBeenRun([CallerMemberName] string api = "")
2932
{
3033
if (!verifyHasBeenRun)
3134
{
3235
return;
3336
}
3437

35-
var stackTrace = new StackTrace(1, false);
36-
var method = stackTrace.GetFrame(1)!.GetMethod()!;
37-
var type = method.DeclaringType;
38-
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}");
38+
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}");
3939
}
4040

4141
public InnerVerifier(

src/todo.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ All six resolved 2026-08-16 (five fixed here; the inline item resolved as by-des
5656
- [x] **`Delete:` section drops subdirectories, breaking the parse round-trip.**
5757
`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.
5858

59-
- [ ] **`ThrowIfVerifyHasBeenRun` blames the caller instead of the API.**
59+
- [x] **`ThrowIfVerifyHasBeenRun` blames the caller instead of the API.**
6060
`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)`.
6161

6262
## Minor / edge cases

0 commit comments

Comments
 (0)