From f7df75b3c150b09f156f0c89154d175999030aea Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Mon, 17 Aug 2026 21:29:52 +1000 Subject: [PATCH] Match MSTest overloads against the test data FindMethod returned the first method whose name matched, so with overloaded test methods every case resolved to the same MethodInfo. Two ways that goes wrong: * The parameter count guard in BuildVerifier compares the data length against that MethodInfo, so it mismatches for the other overload and SetParameters is silently skipped, dropping the parameters from the file name. * The parameter names handed to InnerVerifier come from that MethodInfo, so a same arity overload is labeled with the other overload's parameter names. The data the test was invoked with is all there is to tell overloads apart, so candidates are now narrowed by parameter count and then by the types of that data. It still falls back to the first match when that does not narrow to one. --- ...sts.Overload_first=1_second=2.verified.txt | 1 + ...oadTests.Overload_value=Value.verified.txt | 1 + ...ts.SameArityOverload_number=1.verified.txt | 1 + ....SameArityOverload_text=Value.verified.txt | 1 + src/Verify.MSTest.Tests/OverloadTests.cs | 26 ++++ src/Verify.MSTest/TestExecutionContext.cs | 114 +++++++++++++++++- src/todo.md | 2 +- 7 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 src/Verify.MSTest.Tests/OverloadTests.Overload_first=1_second=2.verified.txt create mode 100644 src/Verify.MSTest.Tests/OverloadTests.Overload_value=Value.verified.txt create mode 100644 src/Verify.MSTest.Tests/OverloadTests.SameArityOverload_number=1.verified.txt create mode 100644 src/Verify.MSTest.Tests/OverloadTests.SameArityOverload_text=Value.verified.txt create mode 100644 src/Verify.MSTest.Tests/OverloadTests.cs diff --git a/src/Verify.MSTest.Tests/OverloadTests.Overload_first=1_second=2.verified.txt b/src/Verify.MSTest.Tests/OverloadTests.Overload_first=1_second=2.verified.txt new file mode 100644 index 0000000000..e9143f5c2c --- /dev/null +++ b/src/Verify.MSTest.Tests/OverloadTests.Overload_first=1_second=2.verified.txt @@ -0,0 +1 @@ +1 2 \ No newline at end of file diff --git a/src/Verify.MSTest.Tests/OverloadTests.Overload_value=Value.verified.txt b/src/Verify.MSTest.Tests/OverloadTests.Overload_value=Value.verified.txt new file mode 100644 index 0000000000..3ee291f7bd --- /dev/null +++ b/src/Verify.MSTest.Tests/OverloadTests.Overload_value=Value.verified.txt @@ -0,0 +1 @@ +Value \ No newline at end of file diff --git a/src/Verify.MSTest.Tests/OverloadTests.SameArityOverload_number=1.verified.txt b/src/Verify.MSTest.Tests/OverloadTests.SameArityOverload_number=1.verified.txt new file mode 100644 index 0000000000..24e9e841a3 --- /dev/null +++ b/src/Verify.MSTest.Tests/OverloadTests.SameArityOverload_number=1.verified.txt @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/src/Verify.MSTest.Tests/OverloadTests.SameArityOverload_text=Value.verified.txt b/src/Verify.MSTest.Tests/OverloadTests.SameArityOverload_text=Value.verified.txt new file mode 100644 index 0000000000..3ee291f7bd --- /dev/null +++ b/src/Verify.MSTest.Tests/OverloadTests.SameArityOverload_text=Value.verified.txt @@ -0,0 +1 @@ +Value \ No newline at end of file diff --git a/src/Verify.MSTest.Tests/OverloadTests.cs b/src/Verify.MSTest.Tests/OverloadTests.cs new file mode 100644 index 0000000000..e5c2b097df --- /dev/null +++ b/src/Verify.MSTest.Tests/OverloadTests.cs @@ -0,0 +1,26 @@ +// Overloads share a name, so the data the test was invoked with is all there is +// to tell the MethodInfo of one from the other +[TestClass] +public partial class OverloadTests +{ + [TestMethod] + [DataRow("Value")] + public Task Overload(string value) => + Verify(value); + + [TestMethod] + [DataRow(1, 2)] + public Task Overload(int first, int second) => + Verify($"{first} {second}"); + + // same parameter count, so only the types of the data tell these apart + [TestMethod] + [DataRow("Value")] + public Task SameArityOverload(string text) => + Verify(text); + + [TestMethod] + [DataRow(1)] + public Task SameArityOverload(int number) => + Verify(number); +} diff --git a/src/Verify.MSTest/TestExecutionContext.cs b/src/Verify.MSTest/TestExecutionContext.cs index 6ef42c6de4..c28fad8400 100644 --- a/src/Verify.MSTest/TestExecutionContext.cs +++ b/src/Verify.MSTest/TestExecutionContext.cs @@ -21,14 +21,122 @@ static MethodInfo FindMethod(Type type, TestContext context) var span = testName.AsSpan(); + MethodInfo? first = null; + List? overloads = null; foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Public)) { - if (span.SequenceEqual(method.Name)) + if (!span.SequenceEqual(method.Name)) { - return method; + continue; + } + + if (first is null) + { + first = method; + continue; + } + + overloads ??= [first]; + overloads.Add(method); + } + + if (first is null) + { + throw new($"Could not find method `{type.Name}.{testName}`."); + } + + if (overloads is null) + { + return first; + } + + // Overloads share a name, so the data the test was invoked with is all there is + // to tell them apart. Falls back to the first when it does not narrow to one. + return FindOverload(overloads, context.TestData) ?? first; + } + + static MethodInfo? FindOverload(List overloads, object?[]? data) + { + data ??= []; + + List byCount = []; + foreach (var overload in overloads) + { + if (MatchesCount(overload, data.Length)) + { + byCount.Add(overload); + } + } + + if (byCount.Count <= 1) + { + return byCount.FirstOrDefault(); + } + + MethodInfo? byType = null; + foreach (var overload in byCount) + { + if (!MatchesTypes(overload, data)) + { + continue; + } + + if (byType is not null) + { + return null; + } + + byType = overload; + } + + return byType; + } + + static bool MatchesCount(MethodInfo method, int dataLength) + { + var parameters = method.GetParameters(); + if (parameters.Length == dataLength) + { + return true; + } + + // A params array DataRow exposes raw pre-binding data, so its length only has + // to cover the parameters that precede the array + return parameters.Length > 0 && + parameters[^1].IsDefined(typeof(ParamArrayAttribute)) && + dataLength >= parameters.Length - 1; + } + + static bool MatchesTypes(MethodInfo method, object?[] data) + { + var parameters = method.GetParameters(); + if (parameters.Length != data.Length) + { + return false; + } + + for (var index = 0; index < parameters.Length; index++) + { + var type = parameters[index].ParameterType; + var value = data[index]; + + if (value is null) + { + if (type.IsValueType && + Nullable.GetUnderlyingType(type) is null) + { + return false; + } + + continue; + } + + if (!type.IsInstanceOfType(value)) + { + return false; } } - throw new($"Could not find method `{type.Name}.{testName}`."); + return true; } } \ No newline at end of file diff --git a/src/todo.md b/src/todo.md index 03784f69b0..c1e2690803 100644 --- a/src/todo.md +++ b/src/todo.md @@ -50,7 +50,7 @@ All six resolved 2026-08-16 (five fixed here; the inline item resolved as by-des - [ ] **Combinations name cache collapses distinct keys.** `Verify/Combinations/CombinationResultsConverter.cs:32-54` — `Dictionary` keyed on the boxed value: `DateTime.Equals` ignores `Kind`, `DateTimeOffset.Equals` compares only the instant, while the rendered names include Kind/offset. Inputs `2000-01-01 Utc` and `2000-01-01 Local` both get labeled `2000-01-01Utc`. -- [ ] **MSTest overloaded test methods resolve to the wrong `MethodInfo`.** +- [x] **MSTest overloaded test methods resolve to the wrong `MethodInfo`.** `Verify.MSTest/TestExecutionContext.cs:24-30` — `FindMethod` returns the first name match, ignoring parameters. With two `[DataRow]` overloads of one name, the parameter-count guard in `Verifier.BuildVerifier` mismatches for one of them → `SetParameters` silently skipped → both overloads collide on one snapshot prefix. - [ ] **`Delete:` section drops subdirectories, breaking the parse round-trip.**