Skip to content

Commit a7d234f

Browse files
committed
Improve generic method matching in RuntimeProxy<T>
Enhanced `_setupInfo` to include `signatureKey` for precise method signature matching, especially for generic methods. Introduced `SignatureKey` to generate unique keys for signatures, accounting for generic parameters. Refactored `FindMatchingBehaviorWithIsAny` to use `signatureKey` and removed the redundant `FindMatchingBehavior` method. Updated `Setup` and `CreateArgumentKey` to support signature-based matching. Simplified `Task` and `ValueTask` handling syntax. Added a new test for complex argument matching with `It.IsAny` markers and introduced the `ITestQueryService` interface for validation. These changes improve accuracy, reduce false positives, and streamline the codebase.
1 parent cf82c39 commit a7d234f

File tree

2 files changed

+39
-43
lines changed

2 files changed

+39
-43
lines changed

src/MockLite.Core/RuntimeProxy.cs

Lines changed: 15 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ internal class RuntimeProxy<T> : DispatchProxy where T : class
3535
/// <summary>
3636
/// Stores setup information including whether parameters use It.IsAny
3737
/// </summary>
38-
private readonly Dictionary<string, (Delegate behavior, bool[] isAnyMatcher)> _setupInfo = [];
38+
private readonly Dictionary<string, (Delegate behavior, bool[] isAnyMatcher, string signatureKey)> _setupInfo = [];
3939

4040
private static readonly ConcurrentDictionary<Type, object> _anyMatchers = new();
4141

@@ -63,7 +63,7 @@ internal class RuntimeProxy<T> : DispatchProxy where T : class
6363
var anyMatcherBehavior = FindMatchingBehaviorWithIsAny(targetMethod!, args);
6464
if (anyMatcherBehavior != null)
6565
{
66-
return anyMatcherBehavior.Method.GetParameters().Length == 0 ?
66+
return anyMatcherBehavior.Method.GetParameters().Length == 0 ?
6767
anyMatcherBehavior.DynamicInvoke() :
6868
anyMatcherBehavior.DynamicInvoke(args);
6969
}
@@ -82,7 +82,7 @@ internal class RuntimeProxy<T> : DispatchProxy where T : class
8282
var tArg = ret.GenericTypeArguments[0];
8383
return typeof(Task).GetMethod(nameof(Task.FromResult))!
8484
.MakeGenericMethod(tArg)
85-
.Invoke(null, new object?[] { GetDefault(tArg) });
85+
.Invoke(null, [GetDefault(tArg)]);
8686
}
8787
if (ret == typeof(ValueTask)) return default(ValueTask);
8888
if (ret.IsGenericType && ret.GetGenericTypeDefinition() == typeof(ValueTask<>))
@@ -98,14 +98,14 @@ internal class RuntimeProxy<T> : DispatchProxy where T : class
9898
/// </summary>
9999
private Delegate? FindMatchingBehaviorWithIsAny(MethodInfo method, object?[] args)
100100
{
101+
var key = SignatureKey(method);
101102
// Check all setup information entries for this method looking for IsAny matches
102103
foreach (var kvp in _setupInfo)
103104
{
104-
if (!kvp.Key.StartsWith(method.Name + "("))
105-
continue;
106-
107-
var (behavior, isAnyMatcher) = kvp.Value;
105+
var (behavior, isAnyMatcher, signatureKey) = kvp.Value;
108106

107+
if (!signatureKey.Equals(key))
108+
continue;
109109
// Only check setups that have at least one IsAny
110110
if (!isAnyMatcher.Any(x => x))
111111
continue;
@@ -120,39 +120,6 @@ internal class RuntimeProxy<T> : DispatchProxy where T : class
120120
return null;
121121
}
122122

123-
/// <summary>
124-
/// Finds a matching behavior for the given method and arguments,
125-
/// considering It.IsAny matchers in the setup.
126-
/// </summary>
127-
private Delegate? FindMatchingBehavior(MethodInfo method, object?[] args)
128-
{
129-
var methodParams = method.GetParameters();
130-
Delegate? anyMatcherBehavior = null;
131-
132-
// Check all setup information entries for this method
133-
foreach (var kvp in _setupInfo)
134-
{
135-
if (!kvp.Key.StartsWith(method.Name + "("))
136-
continue;
137-
138-
var (behavior, isAnyMatcher) = kvp.Value;
139-
140-
// Check if this setup matches the current invocation
141-
if (DoesSetupMatch(args, isAnyMatcher))
142-
{
143-
// If all parameters are specific (not IsAny), this is an exact match - return immediately
144-
if (!isAnyMatcher.Any(x => x))
145-
return behavior;
146-
147-
// If has IsAny matchers, save it as fallback
148-
anyMatcherBehavior = behavior;
149-
}
150-
}
151-
152-
// Return the exact match if found, otherwise return the IsAny match
153-
return anyMatcherBehavior;
154-
}
155-
156123
/// <summary>
157124
/// Determines if a setup matches the current invocation arguments.
158125
/// </summary>
@@ -174,6 +141,7 @@ private static bool DoesSetupMatch(object?[] invocationArgs, bool[] isAnyMatcher
174141
// The actual exact matching is done via the argument key comparison in _behaviors dictionary
175142
}
176143

144+
177145
return true;
178146
}
179147

@@ -195,11 +163,12 @@ public void Setup(MethodInfo method, Delegate behavior)
195163
public void Setup(MethodInfo method, object?[] args, Delegate behavior)
196164
{
197165
var argKey = CreateArgumentKey(method, args);
166+
var signatureKey = SignatureKey(method);
198167
_behaviors[argKey] = behavior;
199168

200169
// Store setup info to track which arguments are IsAny matchers
201170
var isAnyMatcher = args.Select(arg => IsAnyMatcherInstance(arg)).ToArray();
202-
_setupInfo[argKey] = (behavior, isAnyMatcher);
171+
_setupInfo[argKey] = (behavior, isAnyMatcher, signatureKey);
203172
}
204173

205174
/// <summary>
@@ -264,12 +233,15 @@ private void ExecuteCallbacks(MethodInfo method, object?[] args)
264233

265234
private static string SignatureKey(MethodInfo mi)
266235
{
236+
// take into account generic parameters
237+
var genericPart = mi.IsGenericMethod ? $"<{string.Join(",", mi.GetGenericArguments().Select(ga => ga.FullName))}>" : "";
267238
var pars = string.Join(",", mi.GetParameters().Select(p => p.ParameterType.FullName));
268-
return $"{mi.Name}({pars})";
239+
return $"{mi.Name}({pars}){genericPart}";
269240
}
270241

271242
private static string CreateArgumentKey(MethodInfo mi, object?[] args)
272243
{
244+
var genericPart = mi.IsGenericMethod ? $"<{string.Join(",", mi.GetGenericArguments().Select(ga => ga.FullName))}>" : "";
273245
var pars = string.Join(",", mi.GetParameters().Select(p => p.ParameterType.FullName));
274246
var argValues = string.Join(",", args.Select(a =>
275247
{
@@ -278,7 +250,7 @@ private static string CreateArgumentKey(MethodInfo mi, object?[] args)
278250
return "IsAny";
279251
return a?.ToString() ?? "null";
280252
}));
281-
return $"{mi.Name}({pars})[{argValues}]";
253+
return $"{mi.Name}({pars})[{argValues}]{genericPart}";
282254
}
283255

284256
private static object? GetDefault(Type t) => t.IsValueType ? Activator.CreateInstance(t) : null;

tests/MockLite.Core.Tests/MockCallbackTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,4 +623,28 @@ public void Test_Callback_SideEffectTracking()
623623
Assert.Equal("Property 'Name' was set to 'Alice'", auditLog[1]);
624624
Assert.Equal("Property 'Name' was read", auditLog[2]);
625625
}
626+
627+
[Fact]
628+
public void Test_Callback_ComplexArgumentMatching()
629+
{
630+
// Arrange
631+
632+
var builder = Mock.Create<ITestQueryService>()
633+
.Setup(x => x.Get<IEnumerable<int>>(It.IsAny<string>()), () => [1, 2, 3])
634+
.Setup(x => x.Get<IEnumerable<string>>(It.IsAny<string>()), () => ["one", "two", "three"]);
635+
636+
var mock = builder.Object;
637+
// Act
638+
var nums = mock.Get<IEnumerable<int>>("my-key-123");
639+
var strs = mock.Get<IEnumerable<string>>("anotherkey");
640+
// Assert
641+
642+
Assert.Contains(2, nums);
643+
Assert.Contains("three", strs);
644+
}
645+
646+
private interface ITestQueryService
647+
{
648+
T Get<T>(string key);
649+
}
626650
}

0 commit comments

Comments
 (0)