IL2CPP-Interna: Test-Rahmenwerke

JOSH PETERSON / UNITY TECHNOLOGIESSenior Software Engineer
Jul 20, 2015|9 Min.
IL2CPP-Interna: Test-Rahmenwerke
Diese Website wurde aus praktischen Gründen für Sie maschinell übersetzt. Die Richtigkeit und Zuverlässigkeit des übersetzten Inhalts kann von uns nicht gewährleistet werden. Sollten Sie Zweifel an der Richtigkeit des übersetzten Inhalts haben, schauen Sie sich bitte die offizielle englische Version der Website an.
Dies ist der achte und letzte Beitrag in der IL2CPP Internals -Serie. In diesem Beitrag werde ich ein wenig vom Inhalt früherer Beiträge abweichen und nicht auf einige Aspekte der Funktionsweise von IL2CPP zur Kompilier- oder Laufzeit eingehen. Stattdessen werden wir einen kurzen Überblick darüber geben, wie wir IL2CPP entwickeln und testen.
Entwicklung nach dem Prinzip "Test first

Das IL2CPP-Team hat eine ausgeprägte "Test-first"-Entwicklungsmentalität. Ein großer Teil des Codes für IL2CPP wird nach der Praxis des Test Driven Development (TDD ) geschrieben, und nur sehr wenige Pull-Requests werden ohne signifikante Testabdeckung in den IL2CPP-Code eingefügt.

Da IL2CPP eine begrenzte (wenn auch ziemlich große) Menge an Inputs hat - die ECMA 335 Spezifikation - passt der Prozess der Entwicklung gut zu TDD-Konzepten. Die meisten Tests werden vor dem Produktionscode geschrieben, und diese Tests müssen immer in einer erwarteten Weise fehlschlagen, bevor der Code geschrieben wird, der sie erfolgreich macht.

Dieser Prozess trägt dazu bei, das Design von IL2CPP voranzutreiben, aber er stellt dem Entwicklungsteam auch eine große Anzahl von Tests zur Verfügung, die relativ schnell ausgeführt werden können und fast das gesamte vorhandene Verhalten in IL2CPP testen. Für ein Entwicklungsteam bietet diese Testsuite zwei wichtige Vorteile.

1) Zuversicht: Die meisten Änderungen am Refactor-Code in IL2CPP können mit hoher Sicherheit vorgenommen werden. Wenn die Tests erfolgreich sind, ist es sehr unwahrscheinlich, dass eine Regression stattgefunden hat.

2) Fehlersuche: Da sich der Code in IL2CPP so verhält, wie wir es erwarten, handelt es sich bei Fehlern fast immer um nicht implementierte Teile des Codes oder um Fälle, die wir noch nicht berücksichtigt haben. Indem wir auf diese Weise den Raum der möglichen Ursachen für einen bestimmten Fehler eingrenzen, können wir Fehler viel schneller beheben.

Teststatistiken

Die verschiedenen Arten von Tests, die wir gegen die IL2CPP-Codebasis durchführen, lassen sich in mehrere Ebenen unterteilen. Im Folgenden finden Sie die Anzahl der Tests, die wir derzeit auf den einzelnen Stufen durchführen (ich werde weiter unten erläutern, worum es sich bei den einzelnen Testarten handelt).

  • Einheitliche Tests
  • C#: 472
  • C++: 44
  • Integrationstests
  • C#: 1735
  • IL: 173

Wenn alle diese Tests grünes Licht geben, sind wir zuversichtlich, dass wir IL2CPP zu diesem Zeitpunkt ausliefern können. Wir unterhalten einen Hauptentwicklungszweig für IL2CPP, der immer den führenden Zweig für die Entwicklung in Unity als Ganzes verfolgt. Auf diesem Hauptentwicklungszweig sind die Tests immer grün. Wenn sie kaputt gehen (was hin und wieder vorkommt), werden sie normalerweise innerhalb weniger Minuten repariert.

Da die Entwickler in unserem Team diesen Hauptzweig häufig für die eigene Entwicklung faken, muss er immer grün sein. Der Build- und Teststatus sowohl für den Hauptentwicklungszweig als auch für persönliche Zweige wird auf Katana, dem internen Build-Management-System von Unity, verwaltet.

Wir verwenden NUnit, um all diese Tests auszuführen, und steuern NUnit auf eine von drei verschiedenen Arten

  • Fenster: ReSharper
  • OSX: Xamarin Studio
  • Befehlszeile unter Windows und OSX auf unseren Build-Maschinen: ein benutzerdefiniertes Perl-Skript

Arten von Tests

Ich habe oben vier verschiedene Arten von Tests erwähnt, ohne viel zu erklären. Jede dieser Testarten dient einem anderen Zweck, und alle zusammen tragen dazu bei, die Entwicklung von IL2CPP voranzutreiben.

Die Unit-Tests überprüfen das Verhalten eines kleinen Teils des Codes, in der Regel einer Methode. Sie stellen eine Situation her, führen den zu testenden Code aus und behaupten schließlich ein erwartetes Verhalten.

Die Integrationstests für IL2CPP führen das Dienstprogramm il2cpp.exe auf einer Assembly aus, kompilieren den generierten C++-Code zu einer ausführbaren Datei und führen diese dann aus. Da wir eine schöne Referenz für das IL2CPP-Verhalten haben (die bestehende Version von Mono, die in Unity verwendet wird), führen diese Integrationstests auch die gleiche Assembly mit Mono (und .Net, unter Windows) aus. Unser Testrunner vergleicht dann die Ergebnisse der beiden (oder drei) Läufe, die in die Standardausgabe ausgegeben werden, und meldet alle Unterschiede. Die IL2CPP-Integrationstests haben also keine expliziten erwarteten Werte oder Assertions, die im Testcode aufgeführt sind, wie es bei den Unit-Tests der Fall ist.

C#-Unit-Tests

Diese Tests sind die schnellsten und am wenigsten anspruchsvollen Tests, die wir schreiben. Sie werden verwendet, um das Verhalten vieler Teile von il2cpp.exe, dem AOT-Compilerprogramm für IL2CPP, zu überprüfen. Da il2cpp.exe vollständig in C# geschrieben ist, können wir schnelle C#-Unit-Tests verwenden, um eine gute Durchlaufzeit für Änderungen zu erhalten. Alle C#-Unit-Tests sind in wenigen Sekunden auf einer guten Entwicklungsmaschine abgeschlossen.

C++ Einheitstests

Der überwiegende Teil des Laufzeitcodes für IL2CPP (genannt libil2cpp) ist in C++ geschrieben. Für Teile des Codes, die nicht einfach über eine öffentliche API zugänglich sind, verwenden wir C++-Unit-Tests. Wir haben relativ wenige dieser Tests, da das meiste Verhalten des Codes in libil2cpp durch unsere größere Integrationstestsuite überprüft werden kann. Diese Tests benötigen mehr Zeit, als Sie für Unit-Tests erwarten würden, da sie il2cpp.exe selbst ausführen müssen, um ihre Fixture-Daten einzurichten.

C#-Integrationstests

Die größte und umfassendste Testsuite für IL2CPP ist die C#-Integrationstestsuite. Diese Tests sind in kleinere Segmente unterteilt und konzentrieren sich auf Tests, die das Verhalten von Icons, Codegenerierung, p/invoke und das allgemeine Verhalten überprüfen. Die meisten der Tests in dieser Suite sind recht kurz, nur etwa 5 - 10 Zeilen lang. Die gesamte Suite läuft auf den meisten Rechnern in weniger als einer Minute, aber wir können sie mit verschiedenen IL2CPP-Optionen ausführen, die sich auf Dinge wie Stripping und Codegenerierung beziehen.

IL-Integrationstests

Diese Tests sind in der Toolchain ähnlich wie die C#-Integrationstests. Anstatt jedoch den Testcode in C# zu schreiben, verwenden wir die ILGenerator-Klasse, um direkt eine Assembly zu erstellen. Obwohl diese Tests etwas mehr Zeit in Anspruch nehmen können als C#-Tests, bieten sie mehr Flexibilität. Oft stoßen wir auf Probleme mit IL-Code, der ungültig ist oder von unserem aktuellen Mono C# Compiler nicht erzeugt wird. In diesen Fällen können wir oft einen guten Testfall mit IL-Code schreiben. Die Tests sind auch für umfassende Tests von Opcodes wie conv.i (und ähnlichen Opcodes in seiner Familie) nützlich, die ein klares Verhalten mit vielen leichten Variationen aufweisen. Alle IL-Tests sind in weniger als einer Minute abgeschlossen.

Wir führen all diese Tests in vielen Variationen und Optionen auf Katana durch. Von einem sauberen Pull des Quellcodes bis zum Abschluss der Testläufe vergehen je nach Auslastung der Build-Farm etwa 20-30 Minuten.

Warum so viele Integrationstests?

Anhand dieser Beschreibungen könnte man meinen, dass unsere Testpyramide für IL2CPP auf dem Kopf steht. Und tatsächlich machen die End-to-End-Integrationstests (an der Spitze der Pyramide) den größten Teil unserer Testabdeckung aus.

Die Einhaltung der TDD-Praxis mit Testzeiten von mehr als ein paar Sekunden kann ebenfalls schwierig sein. Wir arbeiten daran, dies abzumildern, indem wir einzelne Segmente der Integrationstestsuiten laufen lassen und den in den Testsuiten generierten C++-Code inkrementell erstellen (auf diese Weise erproben wir einige inkrementelle Erstellungsmöglichkeiten für Unity-Projekte mit IL2CPP, also bleiben Sie dran). Dann ist die Bearbeitungszeit für einen einzelnen Test angemessen (wenn auch nicht so schnell, wie wir es uns wünschen würden).

Die intensive Nutzung von Integrationstests war jedoch eine bewusste Entscheidung. Ein Großteil des Codes in IL2CPP sieht anders aus als früher, selbst bei unseren ersten öffentlichen Veröffentlichungen im Januar 2015. Wir haben viel gelernt und viele der Implementierungsdetails in der IL2CPP-Codebasis seit ihrer Einführung geändert, aber wir haben immer noch viele der ursprünglichen Tests, die vor Jahren geschrieben wurden. Nach dem Ausprobieren von Tests auf verschiedenen Ebenen (einschließlich der Validierung des Inhalts des generierten C++-Quellcodes) haben wir beschlossen, dass diese Integrationstests das beste Verhältnis zwischen Laufzeit und Teststabilität bieten. Selten, wenn überhaupt, müssen wir einen der bestehenden Integrationstests ändern, wenn sich etwas am IL2CPP-Code ändert. Diese Tatsache gibt uns die Gewissheit, dass eine Codeänderung, die einen Test zum Scheitern bringt, wirklich ein Problem darstellt. Außerdem können wir den IL2CPP-Code so oft wie nötig refaktorisieren und verbessern, ohne Angst haben zu müssen.

Noch größere Tests

Außerhalb von IL2CPP selbst fügt sich der IL2CPP-Code in das viel größere Ökosystem der Unity-Tests ein. Für jede von uns ausgelieferte Plattform, die IL2CPP unterstützt, führen wir die Laufzeittests für den Unity-Player durch. Diese Tests bauen ein einzelnes Unity-Projekt mit mehr als 1000 Szenen auf, führen dann jede Szene aus und validieren das erwartete Verhalten über Assertions. Normalerweise fügen wir dieser Suite keine neuen Tests für IL2CPP-Änderungen hinzu (diese Tests befinden sich in der Regel auf einer niedrigeren Ebene). Diese Suite dient zur Überprüfung von Regressionen, die wir mit IL2CPP auf einer bestimmten Plattform einführen könnten. Mit dieser Suite können wir auch den Code testen, der bei der Integration von IL2CPP in die Unity-Build-Toolchain verwendet wird, die wiederum für jede Plattform unterschiedlich ist. Eine typische Laufzeit-Testsuite ist in etwa 60-90 Minuten abgeschlossen, obwohl wir einzelne Tests oft lokal viel schneller ausführen.

Die größten und langsamsten Tests, die wir für IL2CPP verwenden, sind die Integrationstests des Unity-Editors. Jeder dieser Tests führt eine andere Instanz des Unity-Editors aus. Die meisten Integrationstests des IL2CPP-Editors konzentrieren sich auf das Erstellen und Ausführen eines Projekts, in der Regel mit verschiedenen Editor-Einstellungen. Wir verwenden diese Tests, um Dinge wie die Integration komplexer Editoren, die Meldung von Fehlermeldungen und die Größe des Projektaufbaus (neben vielen anderen) zu überprüfen. Je nach Plattform laufen Integrationstestsuiten innerhalb weniger Stunden und werden in der Regel mindestens einmal pro Nacht, wenn nicht häufiger, ausgeführt.

Welche Auswirkungen haben diese Tests?

Einer unserer Leitsätze bei Unity lautet "schwierige Probleme lösen". Ich denke gerne über die Schwierigkeit von Problemen in Bezug auf das Scheitern nach. Je schwieriger ein Problem zu lösen ist, desto mehr Fehler muss ich begehen, bevor ich die Lösung finden kann.

Die Entwicklung eines neuen hochperformanten, hochportablen AOT-Compilers und einer virtuellen Maschine zur Verwendung als Skripting-Backend in Unity ist ein schwieriges Problem. Es ist unnötig zu erwähnen, dass wir auf dem Weg dorthin Tausende von Misserfolgen zu verzeichnen hatten. Es gibt noch mehr Probleme zu lösen, und es wird noch mehr Misserfolge geben. Indem wir aber die nützlichen Informationen aus fast allen dieser Fehler in einer umfassenden und schnellen Testsuite erfassen, können wir sehr schnell iterieren.

Für die IL2CPP-Entwickler ist unsere Test-Suite nicht so sehr ein Mittel, um fehlerfreien Code zu verifizieren (obwohl sie Fehler findet) oder um IL2CPP auf mehrere Plattformen zu portieren (das tut sie auch), sondern sie ist vielmehr ein Werkzeug, mit dem wir schnell scheitern und schwierige Probleme lösen können, damit sich unsere Benutzer auf die Entwicklung schöner Dinge konzentrieren können.

Fazit

Wir hoffen, dass Ihnen die IL2CPP Internals-Serie gefallen hat. Wir sind gerne bereit, Details zur Implementierung mitzuteilen und Hinweise zur Fehlersuche und Leistung zu geben, wenn wir können. Lassen Sie uns wissen, wenn Sie mehr über andere Themen im Zusammenhang mit der Gestaltung und Umsetzung von IL2CPP erfahren möchten.