IL2CPP interne : Cadres de test

L'équipe IL2CPP a une forte mentalité de développement "test-first". Une grande partie du code de l'IL2CPP est écrit en utilisant la pratique du développement piloté par les tests (TDD), et très peu de demandes de téléchargement sont fusionnées dans le code de l'IL2CPP sans une couverture de test significative.
Comme IL2CPP dispose d'un ensemble fini (bien qu'assez large) d'entrées - la spécification ECMA 335 - le processus de développement s'inscrit parfaitement dans les concepts TDD. La plupart des tests sont écrits avant le code de production, et ces tests doivent toujours échouer d'une manière attendue avant que le code permettant de les faire passer ne soit écrit.
Ce processus contribue à la conception d'IL2CPP, mais il fournit également à l'équipe de développement une vaste banque de tests qui s'exécutent assez rapidement et exercent presque tous les comportements existants dans IL2CPP. En tant qu'équipe de développement, cette suite de tests présente deux avantages importants.
1) La confiance : La plupart des modifications apportées au code de refactorisation dans IL2CPP peuvent être effectuées avec une grande confiance. Si les tests sont concluants, il est très peu probable qu'une régression ait été introduite.
2) Dépannage : Étant donné que le code de l'IL2CPP se comporte comme nous l'attendons, les bogues sont presque toujours des sections non implémentées du code ou des cas que nous n'avons pas encore envisagés. En réduisant ainsi l'espace des causes possibles d'un bogue donné, nous pouvons corriger les bogues beaucoup plus rapidement.
Les différents types de tests que nous effectuons sur la base du code IL2CPP se répartissent en plusieurs niveaux. Voici le nombre de tests que nous avons actuellement à chaque niveau (je discuterai plus bas de la nature de chaque type de test).
- Tests unitaires
- C# : 472
- C++ : 44
- Tests d'intégration
- C# : 1735
- IL : 173
Si tous ces tests sont positifs, nous sommes convaincus de pouvoir expédier IL2CPP à ce moment-là. Nous maintenons une branche de développement principale pour IL2CPP, qui suit toujours la branche de pointe pour le développement de Unity dans son ensemble. Les tests sont toujours verts sur cette branche principale de développement. Lorsqu'ils se cassent (ce qui arrive de temps en temps), quelqu'un les répare généralement en quelques minutes.
Comme les développeurs de notre équipe forkent souvent cette branche principale pour leur développement personnel, elle doit être verte en permanence. Les statuts de construction et de test de la branche principale de développement et des branches personnelles sont maintenus sur Katana, le système interne de gestion des constructions d'Unity.
Nous utilisons NUnit pour exécuter tous ces tests et nous pilotons NUnit de l'une des trois manières suivantes
- Fenêtres : ReSharper
- OSX : Xamarin Studio
- Ligne de commande sur Windows et OSX sur nos machines de construction : un script Perl personnalisé
Types de tests
J'ai mentionné plus haut quatre types de tests différents sans trop d'explications. Chacun de ces types de tests a un objectif différent, et ils fonctionnent tous ensemble pour aider à faire avancer le développement de l'IL2CPP.
Les tests unitaires vérifient le comportement d'un petit bout de code, généralement une méthode. Ils mettent en place une situation, exécutent le code testé et affirment finalement un comportement attendu.
Les tests d'intégration pour IL2CPP exécutent l'utilitaire il2cpp.exe sur un assemblage, compilent le code C++ généré en un exécutable, puis exécutent l'exécutable. Puisque nous avons une bonne référence pour le comportement de IL2CPP (la version existante de Mono utilisée dans Unity), ces tests d'intégration exécutent également la même assembly avec Mono (et .Net, sur Windows). Notre programme de test compare ensuite les résultats des deux (ou trois) exécutions transférées sur la sortie standard et signale les différences éventuelles. Les tests d'intégration d'IL2CPP n'ont donc pas de valeurs attendues explicites ou d'assertions listées dans le code de test, comme c'est le cas pour les tests unitaires.
Tests unitaires C#
Ces tests sont les plus rapides et les plus bas que nous écrivons. Ils sont utilisés pour vérifier le comportement de nombreuses parties de il2cpp.exe, l'utilitaire de compilation AOT pour IL2CPP. Comme il2cpp.exe est entièrement écrit en C#, nous pouvons utiliser des tests unitaires rapides en C# pour obtenir un bon délai d'exécution des modifications. Tous les tests unitaires C# s'effectuent en quelques secondes sur une belle machine de développement.
Tests unitaires C++
La grande majorité du code d'exécution de IL2CPP (appelé libil2cpp) est écrite en C++. Pour les parties de ce code qui ne sont pas facilement accessibles à partir d'une API publique, nous utilisons des tests unitaires C++. Nous avons relativement peu de ces tests, car la plupart du comportement du code dans libil2cpp peut être exercé via notre suite de tests d'intégration plus large. Ces tests nécessitent plus de temps que les tests unitaires, car ils doivent exécuter il2cpp.exe lui-même pour configurer leurs données.
Tests d'intégration C#
La suite de tests la plus importante et la plus complète pour IL2CPP est la suite de tests d'intégration C#. Ces tests sont divisés en segments plus petits, se concentrant sur les tests qui vérifient le comportement des icalls, la génération de code, p/invoke, et le comportement général. La plupart des tests de cette suite sont assez courts, de l'ordre de 5 à 10 lignes seulement. La suite complète s'exécute en moins d'une minute sur la plupart des machines, mais nous pouvons l'exécuter avec diverses options IL2CPP relatives à des éléments tels que le dépouillement et la génération de code.
Tests d'intégration de l'IL
La chaîne d'outils de ces tests est similaire à celle des tests d'intégration C#. Cependant, au lieu d'écrire le code de test en C#, nous utilisons la classe ILGenerator pour créer directement un assemblage. Bien que ces tests puissent prendre un peu plus de temps à écrire que les tests C#, ils offrent une plus grande flexibilité. Nous rencontrons souvent des problèmes avec du code IL qui n'est pas valide ou qui n'est pas généré par notre compilateur Mono C# actuel. Dans ce cas, il est souvent possible d'écrire un bon scénario de test avec du code IL. Les tests sont également utiles pour tester de manière exhaustive des opcodes tels que conv.i (et les opcodes similaires de sa famille) qui ont un comportement clair avec de nombreuses variations légères. Tous les tests de l'IL se déroulent de bout en bout en moins d'une minute.
Nous effectuons tous ces tests à travers de nombreuses variantes et options de Katana. Entre l'extraction du code source et l'exécution des tests, la durée d'exécution est d'environ 20 à 30 minutes, en fonction de la charge de la ferme de construction.
Sur la base de ces descriptions, il peut sembler que notre pyramide de tests pour l'IL2CPP soit à l'envers. En effet, les tests d'intégration de bout en bout (près du sommet de la pyramide) représentent la majeure partie de notre couverture de test.
Il peut également s'avérer difficile de suivre la pratique du TDD lorsque les tests durent plus de quelques secondes. Nous nous efforçons d'atténuer ce problème en autorisant l'exécution de segments individuels des suites de tests d'intégration et en construisant de manière incrémentielle le code C++ généré dans les suites de tests (c'est ainsi que nous testons certaines possibilités de construction incrémentielle pour les projets Unity avec IL2CPP, alors restez à l'écoute). Ensuite, le délai d'exécution d'un test individuel est raisonnable (même s'il n'est pas aussi rapide que nous le souhaiterions).
Cette utilisation intensive des tests d'intégration a été une décision délibérée. Une grande partie du code de l'IL2CPP est différente de ce qu'elle était auparavant, même lors de nos premières publications publiques en janvier 2015. Nous avons beaucoup appris et modifié de nombreux détails de mise en œuvre dans la base de code IL2CPP depuis sa création, mais nous disposons encore de nombreux tests originaux rédigés il y a plusieurs années. Après avoir essayé des tests à différents niveaux (y compris la validation du contenu du code source C++ généré), nous avons décidé que ces tests d'intégration nous donnaient le meilleur rapport entre la durée d'exécution et la stabilité des tests. Il est rare, voire inexistant, que nous devions modifier l'un des tests d'intégration existants lorsque quelque chose change dans le code d'IL2CPP. Ce fait nous donne une grande confiance dans le fait qu'une modification du code qui entraîne l'échec d'un test est réellement un problème. Il nous permet également de remanier et d'améliorer le code IL2CPP autant que nécessaire sans crainte.
En dehors d'IL2CPP lui-même, le code d'IL2CPP s'inscrit dans l'écosystème beaucoup plus large des tests Unity. Pour chaque plateforme que nous livrons et qui prend en charge IL2CPP, nous exécutons les tests d'exécution du lecteur Unity. Ces tests construisent un seul projet Unity avec plus de 1000 scènes, puis exécutent chaque scène et valident le comportement attendu par le biais d'assertions. Nous n'ajoutons généralement pas de nouveaux tests à cette suite pour les modifications apportées à l'IL2CPP (ces tests finissent généralement par se situer à un niveau inférieur). Cette suite sert à vérifier les régressions que nous pourrions introduire avec IL2CPP sur une plateforme donnée. Cette suite nous permet également de tester le code utilisé pour l'intégration d'IL2CPP dans la chaîne d'outils de construction d'Unity, qui varie encore une fois pour chaque plateforme. Une suite de tests d'exécution typique se termine en 60 à 90 minutes, bien que nous exécutions souvent des tests individuels localement beaucoup plus rapidement.
Les tests les plus importants et les plus lents que nous utilisons pour IL2CPP sont les tests d'intégration de l'éditeur Unity. Chacun de ces tests exécute en fait une instance différente de l'éditeur Unity. La plupart des tests d'intégration de l'éditeur IL2CPP se concentrent sur la construction et l'exécution d'un projet, généralement avec différents paramètres de construction de l'éditeur. Nous utilisons ces tests pour vérifier des éléments tels que l'intégration d'éditeurs complexes, la notification des messages d'erreur et la taille de la compilation du projet (parmi beaucoup d'autres). En fonction de la plateforme, les suites de tests d'intégration sont exécutées en quelques heures, et généralement au moins une fois par nuit, voire plus souvent.
Chez Unity, l'un de nos principes directeurs est de "résoudre des problèmes difficiles". J'aime penser à la difficulté des problèmes en termes d'échec. Plus un problème est difficile à résoudre, plus j'ai besoin d'accomplir d'échecs avant de trouver la solution.
La création d'un nouveau compilateur et d'une machine virtuelle AOT très performants et très portables à utiliser comme base de script dans Unity est un problème difficile. Il va sans dire que nous avons enregistré des milliers d'échecs en cours de route. Il y a plus de problèmes à résoudre, et donc plus d'échecs à venir. Mais en capturant les informations utiles de la quasi-totalité de ces échecs dans une suite de tests complète et rapide, nous pouvons procéder à des itérations très rapidement.
Pour les développeurs d'IL2CPP, notre suite de tests n'est pas tant un moyen de vérifier l'absence de bogues dans le code (bien qu'elle en détecte), ou d'aider à porter IL2CPP sur de multiples plateformes (elle le fait aussi), mais plutôt un outil que nous pouvons utiliser pour échouer rapidement et résoudre des problèmes difficiles afin que nos utilisateurs puissent se concentrer sur la création de belles choses.
Nous espérons que vous avez apprécié la série d'articles sur les internes d'IL2CPP. Nous sommes heureux de partager les détails de la mise en œuvre et de fournir des conseils de débogage et de performance lorsque nous le pouvons. Si vous souhaitez en savoir plus sur d'autres sujets liés à la conception et à la mise en œuvre de l'IL2CPP, n'hésitez pas à nous en faire part.
