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.**