IL2CPP interno: Marcos de pruebas

JOSH PETERSON / UNITY TECHNOLOGIESSenior Software Engineer
Jul 20, 2015|9 minutos
IL2CPP interno: Marcos de pruebas
Para tu comodidad, tradujimos esta página mediante traducción automática. No podemos garantizar la precisión ni la confiabilidad del contenido traducido. Si tienes alguna duda sobre la precisión del contenido traducido, consulta la versión oficial en inglés de la página web.
Este es el octavo y último post de la serie IL2CPP Internals . En este post me desviaré un poco del contenido de posts anteriores, y no discutiré algún aspecto de cómo funciona IL2CPP en tiempo de compilación o en tiempo de ejecución. En su lugar, veremos un breve resumen de cómo desarrollamos y probamos el IL2CPP.
Desarrollo basado en pruebas

El equipo de IL2CPP tiene una fuerte mentalidad de desarrollo que da prioridad a las pruebas. Gran parte del código de IL2CPP se escribe utilizando la práctica del Desarrollo Dirigido por Pruebas (TDD), y muy pocas peticiones de pull se fusionan con el código de IL2CPP sin una cobertura de pruebas significativa.

Dado que IL2CPP tiene un conjunto finito (aunque bastante amplio) de entradas -la especificación ECMA 335-, el proceso de desarrollo encaja perfectamente con los conceptos de TDD. La mayoría de las pruebas se escriben antes que el código de producción, y estas pruebas siempre tienen que fallar de una manera esperada antes de que se escriba el código para hacerlas pasar.

Este proceso ayuda a impulsar el diseño de IL2CPP, pero también proporciona al equipo de desarrollo un gran banco de pruebas que se ejecutan con bastante rapidez y ejercitan casi todo el comportamiento existente en IL2CPP. Como equipo de desarrollo, este conjunto de pruebas aporta dos ventajas importantes.

1) Confianza: La mayoría de los cambios para refactorizar código en IL2CPP pueden realizarse con gran confianza. Si las pruebas pasan, es muy poco probable que se haya introducido una regresión.

2) Resolución de problemas: Dado que el código en IL2CPP se comporta como esperamos que lo haga, los fallos son casi siempre secciones del código sin implementar o casos que aún no hemos considerado. Al reducir de esta forma el espacio de posibles causas de un fallo determinado, podemos corregir los fallos mucho más rápidamente.

Pruebas estadísticas

Los distintos tipos de pruebas que realizamos con el código IL2CPP se dividen en varios niveles. A continuación se indica el número de pruebas que tenemos actualmente en cada nivel (más adelante explicaré en qué consiste cada tipo de prueba).

  • Pruebas unitarias
  • C#: 472
  • C++: 44
  • Pruebas de integración
  • C#: 1735
  • IL: 173

Si todas estas pruebas salen bien, estamos seguros de que podemos enviar el IL2CPP en ese momento. Mantenemos una rama de desarrollo principal para IL2CPP, que siempre sigue la rama de vanguardia para el desarrollo en Unity en su conjunto. Las pruebas son siempre verdes en esta rama principal de desarrollo. Cuando se rompen (cosa que ocurre de vez en cuando), alguien suele arreglarlas en pocos minutos.

Dado que los desarrolladores de nuestro equipo bifurcan a menudo esta rama principal para su desarrollo personal, debe estar siempre en verde. El estado de compilación y prueba tanto de la rama principal de desarrollo como de las ramas personales se mantiene en Katana, el sistema interno de gestión de compilación de Unity.

Utilizamos NUnit para ejecutar todas estas pruebas y la unidad NUnit en una de tres maneras diferentes

  • Ventanas: ReSharper
  • OSX: Xamarin Studio
  • Línea de comandos en Windows y OSX en nuestras máquinas de compilación: un script Perl personalizado

Tipos de pruebas

Antes he mencionado cuatro tipos diferentes de pruebas sin dar muchas explicaciones. Cada uno de estos tipos de pruebas tiene una finalidad distinta, y todas ellas trabajan juntas para ayudar a que el desarrollo del IL2CPP siga avanzando.

Las pruebas unitarias verifican el comportamiento de un pequeño fragmento de código, normalmente un método. Establecen una situación, ejecutan el código sometido a prueba y, por último, afirman algún comportamiento esperado.

Las pruebas de integración de IL2CPP en realidad ejecutan la utilidad il2cpp.exe en un ensamblado, compilan el código C++ generado en un ejecutable y, a continuación, ejecutan el ejecutable. Dado que tenemos una buena referencia para el comportamiento de IL2CPP (la versión existente de Mono utilizada en Unity), estas pruebas de integración también ejecutan el mismo ensamblado con Mono (y .Net, en Windows). A continuación, nuestro ejecutor de pruebas compara los resultados de las dos (o tres) ejecuciones volcadas a la salida estándar e informa de cualquier diferencia. Por lo tanto, las pruebas de integración IL2CPP no tienen valores esperados explícitos o afirmaciones que figuran en el código de prueba como las pruebas unitarias.

Pruebas unitarias de C#

Estas pruebas son las más rápidas y de más bajo nivel que escribimos. Se utilizan para verificar el comportamiento de muchas partes de il2cpp.exe, la utilidad compiladora AOT para IL2CPP. Dado que il2cpp.exe está escrito íntegramente en C#, podemos utilizar pruebas unitarias rápidas de C# para conseguir un buen tiempo de respuesta para los cambios. Todas las pruebas unitarias de C# se completan en unos segundos en una buena máquina de desarrollo.

Pruebas unitarias en C

La mayor parte del código de ejecución de IL2CPP (denominado libil2cpp) está escrito en C++. Para las partes de ese código que no son fácilmente accesibles desde una API pública, utilizamos pruebas unitarias de C++. Tenemos relativamente pocas de estas pruebas, ya que la mayor parte del comportamiento del código en libil2cpp se puede ejercitar a través de nuestro conjunto de pruebas de integración más grande. Estas pruebas requieren más tiempo que las pruebas unitarias, ya que necesitan ejecutar il2cpp.exe para configurar sus datos.

Pruebas de integración en C#

El conjunto de pruebas más amplio y completo para IL2CPP es el conjunto de pruebas de integración de C#. Estas pruebas se dividen en segmentos más pequeños, centrándose en las pruebas que verifican el comportamiento de las icalls, la generación de código, p/invoke y el comportamiento general. La mayoría de las pruebas de esta suite son bastante cortas, de sólo unas 5 - 10 líneas. El paquete completo se ejecuta en menos de un minuto en la mayoría de las máquinas, pero podemos ejecutarlo con varias opciones de IL2CPP relacionadas con cosas como el stripping y la generación de código.

Pruebas de integración de IL

La cadena de herramientas de estas pruebas es similar a la de las pruebas de integración de C#. Sin embargo, en lugar de escribir el código de prueba en C#, utilizamos la clase ILGenerator para crear directamente un ensamblado. Aunque estas pruebas pueden llevar un poco más de tiempo que las de C#, ofrecen una mayor flexibilidad. A menudo nos encontramos con problemas con código IL que no es válido o no es generado por nuestro actual compilador Mono C#. En estos casos, a menudo podemos escribir un buen caso de prueba con código IL. Las pruebas también son beneficiosas para la comprobación exhaustiva de opcodes como conv.i (y opcodes similares de su familia) que tienen un comportamiento claro con muchas ligeras variaciones. Todas las pruebas de IL se completan de principio a fin en menos de un minuto.

Realizamos todas estas pruebas a través de muchas variaciones y opciones en Katana. Desde la extracción limpia del código fuente hasta la ejecución completa de las pruebas, el tiempo de ejecución oscila entre 20 y 30 minutos, en función de la carga de la granja de compilación.

¿Por qué tantas pruebas de integración?

Basándonos en estas descripciones, podría parecer que nuestra pirámide de pruebas para IL2CPP está al revés. Y, efectivamente, las pruebas de integración de extremo a extremo (cerca de la cúspide de la pirámide) constituyen la mayor parte de nuestra cobertura de pruebas.

Seguir la práctica de TDD con tiempos de prueba de más de unos segundos también puede resultar difícil. Trabajamos para mitigar esto permitiendo que se ejecuten segmentos individuales de los conjuntos de pruebas de integración, y haciendo una construcción incremental del código C++ generado en los conjuntos de pruebas (así es como estamos probando algunas posibilidades de construcción incremental para proyectos Unity con IL2CPP, así que permanezca atento). Entonces, el plazo de entrega de una prueba individual es razonable (aunque todavía no tan rápido como nos gustaría).

El uso intensivo de pruebas de integración fue una decisión consciente. Gran parte del código en IL2CPP se ve diferente de lo que solía, incluso en nuestros lanzamientos públicos iniciales en enero de 2015. Hemos aprendido mucho y hemos cambiado muchos de los detalles de implementación en la base de código IL2CPP desde su creación, pero todavía tenemos muchas de las pruebas originales escritas hace años. Tras probar pruebas a distintos niveles (incluso validando el contenido del código fuente C++ generado), decidimos que estas pruebas de integración nos ofrecen la mejor relación entre tiempo de ejecución y estabilidad de las pruebas. Rara vez, o nunca, necesitamos modificar una de las pruebas de integración existentes cuando algo cambia en el código IL2CPP. Este hecho nos da una enorme seguridad de que un cambio de código que hace que falle una prueba es realmente un problema. También nos permite refactorizar y mejorar el código IL2CPP tanto como necesitemos sin miedo.

Pruebas aún mayores

Aparte del propio IL2CPP, el código de IL2CPP encaja en el ecosistema de pruebas de Unity, mucho más amplio. Para cada plataforma que distribuimos compatible con IL2CPP, ejecutamos las pruebas de ejecución del reproductor Unity. Estas pruebas construyen un único proyecto Unity con más de 1000 escenas, luego ejecutan cada escena y validan el comportamiento esperado a través de aserciones. Normalmente no añadimos nuevas pruebas a esta suite para los cambios de IL2CPP (esas pruebas suelen acabar estando en un nivel inferior). Esta suite sirve para comprobar las regresiones que podríamos introducir con IL2CPP en una plataforma determinada. Esta suite también nos permite probar el código utilizado en la integración IL2CPP en la cadena de herramientas de compilación de Unity, que de nuevo varía para cada plataforma. Un conjunto de pruebas de ejecución típico se completa en unos 60-90 minutos, aunque a menudo ejecutamos pruebas individuales de forma local mucho más rápido.

Las pruebas más grandes y lentas que utilizamos para IL2CPP son las pruebas de integración del editor Unity. Cada una de estas pruebas en realidad ejecuta una instancia diferente del editor de Unity. La mayoría de las pruebas de integración del editor IL2CPP se centran en la creación y ejecución de un proyecto, normalmente con varias configuraciones de creación del editor. Utilizamos estas pruebas para verificar aspectos como la integración de editores complejos, la notificación de mensajes de error y el tamaño de la compilación del proyecto (entre muchos otros). Dependiendo de la plataforma, las suites de pruebas de integración se ejecutan en unas pocas horas, y normalmente se ejecutan al menos cada noche, si no más a menudo.

¿Cuál es el impacto de estas pruebas?

En Unity, uno de nuestros principios rectores es "resolver problemas difíciles". Me gusta pensar en la dificultad de los problemas en términos de fracaso. Cuanto más difícil es resolver un problema, más fallos tengo que cometer antes de encontrar la solución.

Crear un nuevo compilador AOT y una máquina virtual de alto rendimiento y alta portabilidad para utilizarlos como backend de scripting en Unity es un problema difícil. Ni que decir tiene que hemos cosechado miles de fracasos por el camino. Hay más problemas que resolver y, por tanto, más fracasos por venir. Pero al capturar la información útil de casi todos esos fallos en un conjunto de pruebas completo y rápido, podemos iterar muy rápidamente.

Para los desarrolladores de IL2CPP, nuestro conjunto de pruebas no es tanto un medio para verificar un código libre de errores (aunque los detecta), o para ayudar a portar IL2CPP a múltiples plataformas (también lo hace), sino más bien, es una herramienta que podemos utilizar para fallar rápido y resolver problemas difíciles para que nuestros usuarios puedan centrarse en la creación de cosas hermosas.

Conclusión

Esperamos que haya disfrutado de la serie de entradas sobre IL2CPP Internals. Estaremos encantados de compartir los detalles de la implementación y proporcionar sugerencias de depuración y rendimiento cuando podamos. Háganos saber si desea saber más sobre otros temas relacionados con el diseño y la aplicación de IL2CPP.