Add support for indexers
This commit is contained in:
Родитель
f39e72bf43
Коммит
b93154c152
|
@ -105,6 +105,28 @@ var stub = new StubIPhoneBook()
|
|||
.MyNumber_Set(value => newNumber = value);
|
||||
```
|
||||
|
||||
## Stubbing indexers
|
||||
```csharp
|
||||
var stub = new StubIGenericContainer<int>();
|
||||
|
||||
// stubbing indexer getter
|
||||
stub.Item_Get(index =>
|
||||
{
|
||||
// we're expecting the code under test to get index 5
|
||||
if (index != 5) throw new IndexOutOfRangeException();
|
||||
return 99;
|
||||
});
|
||||
|
||||
// stubbing indexer setter
|
||||
int res = -1;
|
||||
stub.Item_Set((index, value) =>
|
||||
{
|
||||
// we're expecting the code under test to only set index 7
|
||||
if (index != 7) throw new IndexOutOfRangeException();
|
||||
res = value;
|
||||
});
|
||||
```
|
||||
|
||||
## Stubbing events
|
||||
|
||||
```csharp
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<package >
|
||||
<metadata>
|
||||
<id>Etg.SimpleStubs</id>
|
||||
<version>2.2.0</version>
|
||||
<version>2.3.0</version>
|
||||
<title>SimpleStubs mocking framework</title>
|
||||
<authors>Microsoft Studios (BigPark)</authors>
|
||||
<owners>Microsoft Studios (BigPark)</owners>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Etg.SimpleStubs.CodeGen
|
||||
{
|
||||
internal interface IPropertyStubber
|
||||
{
|
||||
ClassDeclarationSyntax StubProperty(ClassDeclarationSyntax classDclr, IPropertySymbol propertySymbol,
|
||||
INamedTypeSymbol stubbedInterface);
|
||||
}
|
||||
}
|
|
@ -12,9 +12,11 @@ namespace Etg.SimpleStubs.CodeGen
|
|||
internal class InterfaceStubber : IInterfaceStubber
|
||||
{
|
||||
private readonly IEnumerable<IMethodStubber> _methodStubbers;
|
||||
private readonly IEnumerable<IPropertyStubber> _propertyStubbers;
|
||||
|
||||
public InterfaceStubber(IEnumerable<IMethodStubber> methodStubbers)
|
||||
public InterfaceStubber(IEnumerable<IMethodStubber> methodStubbers, IEnumerable<IPropertyStubber> propertyStubbers)
|
||||
{
|
||||
_propertyStubbers = propertyStubbers;
|
||||
_methodStubbers = new List<IMethodStubber>(methodStubbers);
|
||||
}
|
||||
|
||||
|
@ -32,6 +34,7 @@ namespace Etg.SimpleStubs.CodeGen
|
|||
|
||||
classDclr = RoslynUtils.CopyGenericConstraints(interfaceType, classDclr);
|
||||
classDclr = AddStubContainerField(classDclr, stubName);
|
||||
classDclr = StubProperties(interfaceType, classDclr);
|
||||
classDclr = StubMethods(interfaceType, classDclr);
|
||||
|
||||
string fullNameSpace = semanticModel.GetDeclaredSymbol(namespaceNode).ToString();
|
||||
|
@ -42,9 +45,22 @@ namespace Etg.SimpleStubs.CodeGen
|
|||
return cu;
|
||||
}
|
||||
|
||||
private ClassDeclarationSyntax StubProperties(INamedTypeSymbol interfaceType, ClassDeclarationSyntax classDclr)
|
||||
{
|
||||
IEnumerable<IPropertySymbol> propertiesToStub = RoslynUtils.GetAllMembers<IPropertySymbol>(interfaceType);
|
||||
foreach (IPropertySymbol propertySymbol in propertiesToStub)
|
||||
{
|
||||
foreach (IPropertyStubber propertyStubber in _propertyStubbers)
|
||||
{
|
||||
classDclr = propertyStubber.StubProperty(classDclr, propertySymbol, interfaceType);
|
||||
}
|
||||
}
|
||||
return classDclr;
|
||||
}
|
||||
|
||||
private ClassDeclarationSyntax StubMethods(INamedTypeSymbol interfaceType, ClassDeclarationSyntax classDclr)
|
||||
{
|
||||
List<IMethodSymbol> methodsToStub = RoslynUtils.GetAllMethods(interfaceType);
|
||||
IEnumerable<IMethodSymbol> methodsToStub = RoslynUtils.GetAllMembers<IMethodSymbol>(interfaceType);
|
||||
foreach (IMethodSymbol methodSymbol in methodsToStub)
|
||||
{
|
||||
foreach (IMethodStubber methodStubber in _methodStubbers)
|
||||
|
|
|
@ -27,7 +27,7 @@ namespace Etg.SimpleStubs.CodeGen
|
|||
SF.IdentifierName(methodSymbol.GetContainingInterfaceGenericQualifiedName())));
|
||||
|
||||
string delegateTypeName = NamingUtils.GetDelegateTypeName(methodSymbol, stubbedInterface);
|
||||
string parameters = FormatParameters(methodSymbol);
|
||||
string parameters = StubbingUtils.FormatParameters(methodSymbol);
|
||||
|
||||
string callDelegateStmt = StubbingUtils.GenerateInvokeDelegateStmt(delegateTypeName, methodSymbol.GetGenericName(), parameters);
|
||||
if (!methodSymbol.ReturnsVoid)
|
||||
|
@ -40,21 +40,5 @@ namespace Etg.SimpleStubs.CodeGen
|
|||
|
||||
return classDclr;
|
||||
}
|
||||
|
||||
private static string FormatParameters(IMethodSymbol methodSymbol)
|
||||
{
|
||||
return string.Join(", ", methodSymbol.Parameters.Select(p =>
|
||||
{
|
||||
if (p.RefKind == RefKind.Out)
|
||||
{
|
||||
return $"out {p.Name}";
|
||||
}
|
||||
if (p.RefKind == RefKind.Ref)
|
||||
{
|
||||
return $"ref {p.Name}";
|
||||
}
|
||||
return p.Name;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,79 +1,75 @@
|
|||
using System;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Etg.SimpleStubs.CodeGen.Utils;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Etg.SimpleStubs.CodeGen
|
||||
{
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using System.Linq;
|
||||
using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
using SF = SyntaxFactory;
|
||||
|
||||
internal class PropertyStubber : IMethodStubber
|
||||
internal class PropertyStubber : IPropertyStubber
|
||||
{
|
||||
public ClassDeclarationSyntax StubMethod(ClassDeclarationSyntax classDclr, IMethodSymbol methodSymbol,
|
||||
public ClassDeclarationSyntax StubProperty(ClassDeclarationSyntax classDclr, IPropertySymbol propertySymbol,
|
||||
INamedTypeSymbol stubbedInterface)
|
||||
{
|
||||
if (!methodSymbol.IsPropertyAccessor())
|
||||
{
|
||||
return classDclr;
|
||||
}
|
||||
string indexerType = propertySymbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
BasePropertyDeclarationSyntax propDclr = null;
|
||||
|
||||
string delegateTypeName = NamingUtils.GetDelegateTypeName(methodSymbol, stubbedInterface);
|
||||
|
||||
string propName = methodSymbol.AssociatedSymbol.Name;
|
||||
string propType =
|
||||
((IPropertySymbol) methodSymbol.AssociatedSymbol).Type.ToDisplayString(
|
||||
SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
var propDclr = GetPropDclr(classDclr, propName);
|
||||
if (propDclr == null)
|
||||
if (propertySymbol.GetMethod != null)
|
||||
{
|
||||
propDclr = SF.PropertyDeclaration(SF.ParseTypeName(propType), SF.Identifier(propName))
|
||||
.WithExplicitInterfaceSpecifier(SF.ExplicitInterfaceSpecifier(
|
||||
SF.IdentifierName(methodSymbol.GetContainingInterfaceGenericQualifiedName())));
|
||||
}
|
||||
IMethodSymbol getMethodSymbol = propertySymbol.GetMethod;
|
||||
string parameters = StubbingUtils.FormatParameters(getMethodSymbol);
|
||||
|
||||
if (methodSymbol.IsPropertyGetter())
|
||||
{
|
||||
string delegateTypeName = NamingUtils.GetDelegateTypeName(getMethodSymbol, stubbedInterface);
|
||||
var accessorDclr = SF.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, SF.Block(
|
||||
SF.List(new[]
|
||||
{
|
||||
SF.ParseStatement("return " + StubbingUtils.GenerateInvokeDelegateStmt(delegateTypeName, methodSymbol.Name, ""))
|
||||
SF.ParseStatement("return " + StubbingUtils.GenerateInvokeDelegateStmt(delegateTypeName, getMethodSymbol.Name, parameters))
|
||||
})));
|
||||
|
||||
propDclr = CreatePropertyDclr(getMethodSymbol, indexerType);
|
||||
propDclr = propDclr.AddAccessorListAccessors(accessorDclr);
|
||||
|
||||
}
|
||||
else if (methodSymbol.IsPropertySetter())
|
||||
if (propertySymbol.SetMethod != null)
|
||||
{
|
||||
IMethodSymbol setMethodSymbol = propertySymbol.SetMethod;
|
||||
string parameters = $"{StubbingUtils.FormatParameters(setMethodSymbol)}";
|
||||
string delegateTypeName = NamingUtils.GetDelegateTypeName(setMethodSymbol, stubbedInterface);
|
||||
var accessorDclr = SF.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, SF.Block(
|
||||
SF.List(new[]
|
||||
{
|
||||
SF.ParseStatement(StubbingUtils.GenerateInvokeDelegateStmt(delegateTypeName, methodSymbol.Name, "value"))
|
||||
SF.ParseStatement(StubbingUtils.GenerateInvokeDelegateStmt(delegateTypeName, setMethodSymbol.Name, parameters))
|
||||
})));
|
||||
if (propDclr == null)
|
||||
{
|
||||
propDclr = CreatePropertyDclr(setMethodSymbol, indexerType);
|
||||
}
|
||||
propDclr = propDclr.AddAccessorListAccessors(accessorDclr);
|
||||
}
|
||||
|
||||
if (propDclr != null)
|
||||
{
|
||||
PropertyDeclarationSyntax existingPropDclr = GetPropDclr(classDclr, propName);
|
||||
if (existingPropDclr != null)
|
||||
{
|
||||
classDclr = classDclr.ReplaceNode(existingPropDclr, propDclr);
|
||||
}
|
||||
else
|
||||
{
|
||||
classDclr = classDclr.AddMembers(propDclr);
|
||||
}
|
||||
}
|
||||
|
||||
return classDclr;
|
||||
}
|
||||
|
||||
private static PropertyDeclarationSyntax GetPropDclr(ClassDeclarationSyntax classDclr, string propName)
|
||||
private BasePropertyDeclarationSyntax CreatePropertyDclr(IMethodSymbol methodSymbol, string propType)
|
||||
{
|
||||
return
|
||||
classDclr.DescendantNodes()
|
||||
.OfType<PropertyDeclarationSyntax>()
|
||||
.FirstOrDefault(p => p.Identifier.Text == propName);
|
||||
if (methodSymbol.IsIndexerAccessor())
|
||||
{
|
||||
IndexerDeclarationSyntax indexerDclr = SF.IndexerDeclaration(
|
||||
SF.ParseTypeName(propType))
|
||||
.WithExplicitInterfaceSpecifier(SF.ExplicitInterfaceSpecifier(
|
||||
SF.IdentifierName(methodSymbol.GetContainingInterfaceGenericQualifiedName())));
|
||||
indexerDclr = indexerDclr.AddParameterListParameters(
|
||||
RoslynUtils.GetMethodParameterSyntaxList(methodSymbol).ToArray());
|
||||
return indexerDclr;
|
||||
}
|
||||
|
||||
string propName = methodSymbol.AssociatedSymbol.Name;
|
||||
PropertyDeclarationSyntax propDclr = SF.PropertyDeclaration(SF.ParseTypeName(propType), SF.Identifier(propName))
|
||||
.WithExplicitInterfaceSpecifier(SF.ExplicitInterfaceSpecifier(
|
||||
SF.IdentifierName(methodSymbol.GetContainingInterfaceGenericQualifiedName())));
|
||||
return propDclr;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,8 +30,11 @@ namespace Etg.SimpleStubs.CodeGen.DI
|
|||
{
|
||||
new OrdinaryMethodStubber(),
|
||||
new EventStubber(),
|
||||
new PropertyStubber(),
|
||||
new StubbingDelegateGenerator()
|
||||
},
|
||||
new IPropertyStubber[]
|
||||
{
|
||||
new PropertyStubber()
|
||||
});
|
||||
return interfaceStubber;
|
||||
}).As<IInterfaceStubber>().SingleInstance();
|
||||
|
|
|
@ -102,6 +102,7 @@
|
|||
<Compile Include="CodeGen\IInterfaceStubber.cs" />
|
||||
<Compile Include="CodeGen\InterfaceStubber.cs" />
|
||||
<Compile Include="CodeGen\IProjectStubber.cs" />
|
||||
<Compile Include="CodeGen\IPropertyStubber.cs" />
|
||||
<Compile Include="CodeGen\ProjectStubber.cs" />
|
||||
<Compile Include="Utils\StubbingUtils.cs" />
|
||||
<Compile Include="CodeGen\StubProjectResult.cs" />
|
||||
|
|
|
@ -42,7 +42,7 @@ namespace Etg.SimpleStubs.CodeGen.Utils
|
|||
methodName = SerializeName(methodSymbol.ContainingSymbol) + "_" + methodName;
|
||||
}
|
||||
|
||||
if (methodSymbol.IsOrdinaryMethod())
|
||||
if (methodSymbol.IsOrdinaryMethod() || methodSymbol.IsIndexerAccessor())
|
||||
{
|
||||
if (methodSymbol.Parameters.Any())
|
||||
{
|
||||
|
|
|
@ -42,6 +42,22 @@ namespace Etg.SimpleStubs.CodeGen.Utils
|
|||
return methodSymbol.MethodKind == MethodKind.PropertyGet;
|
||||
}
|
||||
|
||||
public static bool IsIndexerGetter(this IMethodSymbol methodSymbol)
|
||||
{
|
||||
return methodSymbol.Name == "get_Item";
|
||||
}
|
||||
|
||||
public static bool IsIndexerSetter(this IMethodSymbol methodSymbol)
|
||||
{
|
||||
return methodSymbol.Name == "set_Item";
|
||||
}
|
||||
|
||||
public static bool IsIndexerAccessor(this IMethodSymbol methodSymbol)
|
||||
{
|
||||
IPropertySymbol propertySymbol = methodSymbol.AssociatedSymbol as IPropertySymbol;
|
||||
return propertySymbol != null && propertySymbol.IsIndexer;
|
||||
}
|
||||
|
||||
public static string GetGenericName(this IMethodSymbol methodSymbol)
|
||||
{
|
||||
string name = methodSymbol.Name;
|
||||
|
@ -52,7 +68,7 @@ namespace Etg.SimpleStubs.CodeGen.Utils
|
|||
return name;
|
||||
}
|
||||
|
||||
public static string GetContainingInterfaceGenericQualifiedName(this IMethodSymbol methodSymbol)
|
||||
public static string GetContainingInterfaceGenericQualifiedName(this ISymbol methodSymbol)
|
||||
{
|
||||
return methodSymbol.ContainingSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
}
|
||||
|
@ -102,5 +118,21 @@ namespace Etg.SimpleStubs.CodeGen.Utils
|
|||
};
|
||||
return !typeDclr.Modifiers.Any(modifier => nonInternalModifiers.Contains(modifier.RawKind));
|
||||
}
|
||||
|
||||
public static BasePropertyDeclarationSyntax AddAccessorListAccessors(this BasePropertyDeclarationSyntax baseDclr, params AccessorDeclarationSyntax[] accessors)
|
||||
{
|
||||
var propDclr = baseDclr as PropertyDeclarationSyntax;
|
||||
if (propDclr != null)
|
||||
{
|
||||
return propDclr.AddAccessorListAccessors(accessors);
|
||||
}
|
||||
|
||||
var indexerDclr = baseDclr as IndexerDeclarationSyntax;
|
||||
if (indexerDclr != null)
|
||||
{
|
||||
return indexerDclr.AddAccessorListAccessors(accessors);
|
||||
}
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -55,21 +55,21 @@ namespace Etg.SimpleStubs.CodeGen.Utils
|
|||
return paramsSyntaxList;
|
||||
}
|
||||
|
||||
public static List<IMethodSymbol> GetAllMethods(INamedTypeSymbol interfaceType)
|
||||
public static List<T> GetAllMembers<T>(INamedTypeSymbol interfaceType)
|
||||
{
|
||||
var methodsToStub = new List<IMethodSymbol>(interfaceType.GetMembers().OfType<IMethodSymbol>());
|
||||
methodsToStub.AddRange(GetAllInheritedMethods(interfaceType));
|
||||
var methodsToStub = new List<T>(interfaceType.GetMembers().OfType<T>());
|
||||
methodsToStub.AddRange(GetAllInheritedMethods<T>(interfaceType));
|
||||
return methodsToStub;
|
||||
}
|
||||
|
||||
public static IEnumerable<IMethodSymbol> GetAllInheritedMethods(ITypeSymbol typeSymbol)
|
||||
public static IEnumerable<T> GetAllInheritedMethods<T>(ITypeSymbol typeSymbol)
|
||||
{
|
||||
var methods = new List<IMethodSymbol>();
|
||||
var methods = new List<T>();
|
||||
if (typeSymbol.AllInterfaces.Any())
|
||||
{
|
||||
foreach (var baseInterfaceType in typeSymbol.AllInterfaces)
|
||||
{
|
||||
methods.AddRange(baseInterfaceType.GetMembers().OfType<IMethodSymbol>());
|
||||
methods.AddRange(baseInterfaceType.GetMembers().OfType<T>());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,5 +9,21 @@ namespace Etg.SimpleStubs.CodeGen.Utils
|
|||
{
|
||||
return $"_stubs.GetMethodStub<{delegateTypeName}>(\"{methodName}\").Invoke({parameters});\n";
|
||||
}
|
||||
|
||||
public static string FormatParameters(IMethodSymbol methodSymbol)
|
||||
{
|
||||
return string.Join(", ", methodSymbol.Parameters.Select(p =>
|
||||
{
|
||||
if (p.RefKind == RefKind.Out)
|
||||
{
|
||||
return $"out {p.Name}";
|
||||
}
|
||||
if (p.RefKind == RefKind.Ref)
|
||||
{
|
||||
return $"ref {p.Name}";
|
||||
}
|
||||
return p.Name;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TestClassLibrary
|
||||
|
@ -20,12 +21,18 @@ namespace TestClassLibrary
|
|||
event EventHandler<long> PhoneNumberChanged;
|
||||
}
|
||||
|
||||
// just to make sure all inherited members are stubbed
|
||||
public interface IPhoneBookSpecial : IPhoneBook
|
||||
{
|
||||
}
|
||||
|
||||
public interface IContainer
|
||||
{
|
||||
T GetElement<T>(int index);
|
||||
void SetElement<T>(int index, T value);
|
||||
|
||||
bool GetElement(int index, out object value);
|
||||
|
||||
}
|
||||
|
||||
public interface IRefUtils
|
||||
|
@ -74,6 +81,18 @@ namespace TestClassLibrary
|
|||
T GetX();
|
||||
}
|
||||
|
||||
public interface IGenericContainer<T>
|
||||
{
|
||||
T this[int index] { get; set; }
|
||||
|
||||
T this[string key, int n] { get; }
|
||||
}
|
||||
|
||||
// just to make sure inherited indexers are stubbed
|
||||
public interface IGenericContainerSubInterface : IGenericContainer<int>
|
||||
{
|
||||
}
|
||||
|
||||
public interface IInterfaceWithGenericMethod
|
||||
{
|
||||
T GetFoo<T>() where T : class;
|
||||
|
|
|
@ -115,7 +115,7 @@ namespace TestClassLibraryTest
|
|||
public void TestThatMethodStubCanBeOverwritten()
|
||||
{
|
||||
var stub = new StubIPhoneBook().GetContactPhoneNumber((p1, p2) => 12345678);
|
||||
stub.GetContactPhoneNumber((p1, p2) => 11122233, overwrite:true);
|
||||
stub.GetContactPhoneNumber((p1, p2) => 11122233, overwrite: true);
|
||||
|
||||
IPhoneBook phoneBook = stub;
|
||||
Assert.AreEqual(11122233, phoneBook.GetContactPhoneNumber("John", "Smith"));
|
||||
|
@ -165,12 +165,63 @@ namespace TestClassLibraryTest
|
|||
int i1 = 1;
|
||||
int i2 = 2;
|
||||
|
||||
((IRefUtils) stub).Swap<int>(ref i1, ref i2);
|
||||
((IRefUtils)stub).Swap<int>(ref i1, ref i2);
|
||||
Assert.AreEqual(2, i1);
|
||||
Assert.AreEqual(1, i2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestIndexerGet()
|
||||
{
|
||||
var stub = new StubIGenericContainer<int>();
|
||||
stub.Item_Get(index =>
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
return 13;
|
||||
case 1:
|
||||
return 5;
|
||||
default:
|
||||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
});
|
||||
|
||||
IGenericContainer<int> container = stub;
|
||||
Assert.AreEqual(13, container[0]);
|
||||
Assert.AreEqual(5, container[1]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestIndexerSet()
|
||||
{
|
||||
var stub = new StubIGenericContainer<int>();
|
||||
int res = -1;
|
||||
stub.Item_Set((index, value) =>
|
||||
{
|
||||
if (index != 0) throw new IndexOutOfRangeException();
|
||||
res = value;
|
||||
});
|
||||
|
||||
IGenericContainer<int> container = stub;
|
||||
container[0] = 13;
|
||||
|
||||
Assert.AreEqual(13, res);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestThatMultipleIndexerDontConflict()
|
||||
{
|
||||
var stub = new StubIGenericContainer<int>();
|
||||
stub.Item_Get(index => 12).Item_Get((key, i) => 3);
|
||||
|
||||
IGenericContainer<int> container = stub;
|
||||
Assert.AreEqual(12, container[0]);
|
||||
Assert.AreEqual(3, container["foo", 0]);
|
||||
}
|
||||
|
||||
// this test is only used for debugging
|
||||
[Ignore]
|
||||
[TestMethod]
|
||||
public async Task TestGenerateStubs()
|
||||
{
|
||||
|
|
Загрузка…
Ссылка в новой задаче