Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Abonnements

Comment tester un rewriter Roslyn ?

Hier, j’ai codé un SyntaxRewriter qui rajoute les tests de nullité dans le corps de méthodes.

Ceci peut s’avérer très facile comme ce code:

public int Foo1(OrderDetail od)
{
    return od.Product.Category.CategoryID;
}

Qui devient:

public int Foo1(OrderDetail od)
{
    if (od == null || od.Product == null || od.Product.Category == null)
        return default(int);
    return od.Product.Category.CategoryID;
}

Ou un peu plus complexe comme ce code:

public int Foo2(OrderDetail od)
{
    if (od.Product.CategoryID == 0 && od.Order.OrderID == 0 && od.Order.Customer.CompanyName.Length == 0)
        return 1;
    return 2;
}

Qui devient :

public int Foo2(OrderDetail od)
{
    if (od == null || od.Product == null)
        return default(int);
    bool waqsLogicald7a8b2e418d443b48a5202206ad37500 = od.Product.CategoryID == 0;
    if (waqsLogicald7a8b2e418d443b48a5202206ad37500)
    {
        if (od.Order == null)
            return default(int);
        waqsLogicald7a8b2e418d443b48a5202206ad37500 = od.Order.OrderID == 0;
        if (waqsLogicald7a8b2e418d443b48a5202206ad37500)
        {
            if (od.Order.Customer == null || od.Order.Customer.CompanyName == null)
                return default(int);
            waqsLogicald7a8b2e418d443b48a5202206ad37500 = od.Order.Customer.CompanyName.Length == 0;
        }
    }

    if (waqsLogicald7a8b2e418d443b48a5202206ad37500)
        return 1;
    return 2;
}

Ou beaucoup plus complexe…


J’ao donc décidé d’écrire de tests unitaires pour tester mon Rewriter.

Mais là, c’est le drâme, comment tester du meta-code?

Il n’est pas possible de, basiquement, comparer deux string vu que le Rewriter peut rajouter des variables dont le nom comporte un Guid.

Etant un grand fan de Regex, j’ai tout d’abord opté pour cette solution.

Ainsi, pour Foo2, j’ai écrit la méthode suivante :

[TestMethod]
public void TestMethod2()
{
    var compilationUnitSyntax = GetCompilationUnitSyntax();
    Assert.IsTrue(Regex.IsMatch(compilationUnitSyntax.ChildNodes().OfType<NamespaceDeclarationSyntax>().First()
        .ChildNodes().OfType<
ClassDeclarationSyntax>().First().ChildNodes().OfType<MethodDeclarationSyntax>()
        .First(m => m.Identifier.ValueText ==
"Foo2").NormalizeWhitespace().ToString(),
@"^\s*public\s+int\s+Foo2\s*\(\s*OrderDetail\s+od\s*\)\s*
\s*\{\s*
\s*if\s*\(od\s*==\s*null\s*\|\|\s*od.Product\s*==\s*null\)\s*
\s*return\s+default\s*\(\s*int\s*\)\s*;\s*
\s*bool\s+(waqsLogical[\w\d]{32})\s*=\s*od.Product.CategoryID\s*==\s*0\s*;\s*
\s*if\s*\(\s*\1\s*\)\s*
\s*\{\s*
\s*if\s*\(\s*od.Order\s*==\s*null\s*\)\s*
\s*return\s+default\s*\(\s*int\s*\)\s*;\s*
\s*\1\s*=\s*od.Order.OrderID\s*==\s*0\s*;\s*
\s*if\s*\(\s*\1\s*\)\s*
\s*\{\s*
\s*if\s*\(\s*od.Order.Customer\s*==\s*null\s*\|\|\s*od.Order.Customer.CompanyName\s*==\s*null\s*\)\s*
\s*return\s+default\s*\(\s*int\s*\)\s*;\s*
\s*\1\s*=\s*od.Order.Customer.CompanyName.Length\s*==\s*0\s*;\s*
\s*\}\s*
\s*\}\s*
\s*if\s*\(\s*\1\s*\)\s*
\s*return\s+1\s*;\s*
\s*return\s+2\s*;\s*
\s*\}\s*$", RegexOptions.Multiline));
}

Mais bon, c’était un peu pénible à écrire et difficile à relire également.

Il me fallait donc trouver une autre technique.

J’ai donc décidé d’analiser mon code en utilisant Roslyn pour comparer le SyntaxTree fournit par le rewriter avec celui attendu.

Pour me simplifier la vie sur la définition de celui attendu, je décidais d’utiliser les méthodes de Parse de Roslyn.

Mais maintenant, le test de comparaison de deux SuntaxTree n’est pas du tout trivial... En effet, il y a énormément de types différents qui héritent de SyntaxNode et donc ça risquait de me prendre très longtemps pour tout coder.

Le truc cool en revanche, c’est que le code est très répétitif :

public class SyntaxNodeEqualityVisitor
{
    private readonly Func<SyntaxNode, SyntaxNode, bool?> _syntaxNodeComparer;
    private readonly Func<string, string, bool> _identifierComparer;

    public SyntaxNodeEqualityVisitor(Func<SyntaxNode, SyntaxNode, bool?> syntaxNodeComparer = null,
     Func<string, string, bool> identifierComparer = null)
    {
     _syntaxNodeComparer = syntaxNodeComparer;
        _identifierComparer = identifierComparer;
    }




   
public virtual bool AreEquals(SyntaxNode node1, SyntaxNode node2)
    {
     if (node1 == null)
         return node2 == null;
        if (node2 == null)
            return false;
        if (_syntaxNodeComparer != null)
        {
            bool? value = _syntaxNodeComparer(node1, node2);
            if (value.HasValue)
                return value.Value;
        }

        if (node1.GetType() != node2.GetType())
             return false;

        var accessorDeclaration1 = node1 as AccessorDeclarationSyntax;
        var accessorDeclaration2 = node2 as AccessorDeclarationSyntax
;
        if (accessorDeclaration1 != null && accessorDeclaration2 != null
)
            return
AreEqualsAccessorDeclaration(accessorDeclaration1, accessorDeclaration2);   
                                           
        var parameterList1 = node1 as ParameterListSyntax
;
        var parameterList2 = node2 as ParameterListSyntax
;
        if (parameterList1 != null && parameterList2 != null
)
            return
AreEqualsParameterList(parameterList1, parameterList2);    
                                           
        var bracketedParameterList1 = node1 as BracketedParameterListSyntax;
        var bracketedParameterList2 = node2 as BracketedParameterListSyntax
;
        if (bracketedParameterList1 != null && bracketedParameterList2 != null
)
            return
AreEqualsBracketedParameterList(bracketedParameterList1, bracketedParameterList2);



       
//...
    }


public virtual bool AreEqualsAccessorDeclaration(AccessorDeclarationSyntax node1, AccessorDeclarationSyntax node2)
    {
        int
attributeListsCount = node1.AttributeLists.Count;
        if
(node2.AttributeLists.Count != attributeListsCount)
         return false
;
        for (int
i = 0 ; i < attributeListsCount ; i ++)
         if
(! AreEquals(node1.AttributeListsIdea, node2.AttributeListsIdea))
return false
;
        int
modifiersCount = node1.Modifiers.Count;
        if
(node2.Modifiers.Count != modifiersCount)
            return false
;
        for (int
i = 0 ; i < modifiersCount ; i ++)
            if
(node1.ModifiersIdea.Kind != node2.ModifiersIdea.Kind)
                return false
;
        if
(node1.Keyword.Kind != node2.Keyword.Kind)
         return false
;
        if
(! AreEquals(node1.Body, node2.Body))
         return false
;
        if
(node1.SemicolonToken.Kind != node2.SemicolonToken.Kind)
            return false
;
        return true
;
}

public virtual bool AreEqualsParameterList(ParameterListSyntax node1, ParameterListSyntax
node2)
{
        if
(node1.OpenParenToken.Kind != node2.OpenParenToken.Kind)
            return false
;
        int
parametersCount = node1.Parameters.Count;
        if
(node2.Parameters.Count != parametersCount)
            return false
;
        for (int
i = 0 ; i < parametersCount ; i ++)
            if
(! AreEquals(node1.ParametersIdea, node2.ParametersIdea))
                return false
;
        if
(node1.CloseParenToken.Kind != node2.CloseParenToken.Kind)
         return false
;
        return true
;
}


   
//...
}

Profitant de cette répétition, j’ai écrit un template T4 qui génère ce code en analysant, par Reflection, la classe SyntaxVisitor.

Ensuite, j’ai ajouté une classe RoslynSyntaxTreeComparer :

public class RoslynSyntaxTreeComparer
{
    public static bool Equals(SyntaxNode node1, SyntaxNode node2,
     Func<SyntaxNode, SyntaxNode, bool?> syntaxNodeComparer = null,
Func<string, string, bool> identifierComparer = null)
    {
        return new SyntaxNodeEqualityVisitor(syntaxNodeComparer, identifierComparer).AreEquals(node1, node2);
    }
}

Puis j’ai buildé le tout et je peux maintenant l’utiliser de façon très simple :

[TestMethod]
public void TestMethod2()
{
    var compilationUnitSyntax = GetCompilationUnitSyntax();
    Assert.IsTrue(AreEquals(
@"public int Foo2(OrderDetail od)
{
if (od == null || od.Product == null)
return default(int);
bool waqsLogical1 = od.Product.CategoryID == 0;
if (waqsLogical1)
{
if (od.Order == null)
    return default(int);
waqsLogical1 = od.Order.OrderID == 0;
if (waqsLogical1)
{
    if (od.Order.Customer == null || od.Order.Customer.CompanyName == null)
        return default(int);
    waqsLogical1 = od.Order.Customer.CompanyName.Length == 0;
}
}
if (waqsLogical1)
return 1;
return 2;
}", compilationUnitSyntax.ChildNodes().OfType<NamespaceDeclarationSyntax>().First().ChildNodes()
.OfType<
ClassDeclarationSyntax>().First().ChildNodes().OfType<MethodDeclarationSyntax>()
.First(m => m.Identifier.ValueText ==
"Foo2")));
}
 
private bool AreEquals(string expectedCode, SyntaxNode currentCode)
{
    var identifierNames = new Dictionary<string, string>();
    var identifierNames2 = new Dictionary<string, string>();

    return RoslynSyntaxTreeComparer.Equals(Syntax.ParseCompilationUnit(expectedCode).Members[0], currentCode,
identifierComparer: (n1, n2) =>
        {
            if (n1 == n2)
                return true;
            if (n1.StartsWith("waqs"))
            {
                string identifierName;
                if (identifierNames.TryGetValue(n1, out identifierName))
                    return n2 == identifierName;
                identifierNames.Add(n1, n2);
                identifierNames2.Add(n2, n1);
return true;
            }
            return n1 == n2;
        });
}

Pour finir, afin que vous puissiez en profiter, j’ai publié un package NuGet qui contient la dll : https://www.nuget.org/packages/Roslyn.UnitTests.Helpers

Hope that helps

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

Publié mardi 10 septembre 2013 00:26 par Matthieu MEZIL

Commentaires

Pas de commentaires

Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- Office 365: Script PowerShell pour auditer l’usage des Office Groups de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 11:02

- Office 365: Script PowerShell pour auditer l’usage de Microsoft Teams de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 10:39

- Office 365: Script PowerShell pour auditer l’usage de OneDrive for Business de votre tenant par Blog Technique de Romelard Fabrice le 04-25-2019, 15:13

- Office 365: Script PowerShell pour auditer l’usage de SharePoint Online de votre tenant par Blog Technique de Romelard Fabrice le 02-27-2019, 13:39

- Office 365: Script PowerShell pour auditer l’usage d’Exchange Online de votre tenant par Blog Technique de Romelard Fabrice le 02-25-2019, 15:07

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Stream Portal par Blog Technique de Romelard Fabrice le 02-21-2019, 17:56

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Video Portal par Blog Technique de Romelard Fabrice le 02-18-2019, 18:56

- Office 365: Script PowerShell pour extraire les Audit Log basés sur des filtres fournis par Blog Technique de Romelard Fabrice le 01-28-2019, 16:13

- SharePoint Online: Script PowerShell pour désactiver l’Option IRM des sites SPO non autorisés par Blog Technique de Romelard Fabrice le 12-14-2018, 13:01

- SharePoint Online: Script PowerShell pour supprimer une colonne dans tous les sites d’une collection par Blog Technique de Romelard Fabrice le 11-27-2018, 18:01