Un lanzamiento a menudo pasa desapercibido. Y cualquier error que se descubri贸 de repente frente a 茅l nos amenaza con un cambio en los plazos, las revisiones, el trabajo hasta la ma帽ana y los nervios. Cuando tal apuro comenz贸 a ocurrir sistem谩ticamente, nos dimos cuenta de que ya no puedes vivir as铆. Se decidi贸 desarrollar un sistema de validaci贸n integral para salvar al desarrollador
ordinario de Ryan , Artyom, que se fue a su casa antes del lanzamiento a las 9 p.m., a las 10 o a las 11 ... bueno, entiendes. La idea era que el desarrollador descubriera el error, mientras que los cambios a煤n no hab铆an llegado al repositorio, y 茅l mismo no hab铆a perdido el contexto de la tarea.
Hoy, los cambios realizados se comprueban cuidadosamente primero localmente y luego con una serie de pruebas de integraci贸n en la granja de ensamblado. En este art铆culo, hablaremos sobre la primera etapa de verificaci贸n: pruebas est谩ticas, que monitorean la correcci贸n de los recursos y analizan el c贸digo. Este es el primer subsistema de la cadena y representa la mayor parte de los errores encontrados.
Como empez贸 todo
El proceso manual de verificar el juego antes del lanzamiento comenz贸 en QA una semana y media antes del lanzamiento. Naturalmente, los errores que se encuentran en esta etapa deben corregirse lo antes posible.
Debido a la falta de tiempo para una buena soluci贸n, se agrega una "muleta" temporal, que luego se arraiga durante mucho tiempo y est谩 rodeada de otras soluciones no muy populares.
En primer lugar, decidimos automatizar el hallazgo de errores evidentes: bloqueos, incapacidad para completar el conjunto principal de acciones para el juego (abrir una tienda, hacer una compra, jugar un nivel). Para hacer esto, el juego comienza en un modo especial de juego autom谩tico y si algo sale mal, lo sabremos inmediatamente despu茅s de pasar la prueba en nuestra granja.
Pero la mayor铆a de los errores que encontraron tanto los probadores como nuestra prueba de humo automatizada fueron la falta de un recurso o la configuraci贸n incorrecta de diferentes sistemas. Por lo tanto, el siguiente paso fue
la prueba est谩tica : verificar la disponibilidad de recursos, sus relaciones y configuraciones sin iniciar la aplicaci贸n. Este sistema fue lanzado por un paso adicional en la granja de ensamblaje y simplific贸 enormemente la b煤squeda y correcci贸n de errores. Pero, 驴por qu茅 desperdiciar los recursos de la granja de ensamblados si puede detectar un error incluso antes de confirmar y obtener el c贸digo del problema en el repositorio? Esto se puede
hacer con ganchos de precompromiso , que se inician antes de que se cree el compromiso y se env铆e al repositorio.
Y s铆, somos tan geniales que las pruebas est谩ticas antes de comprometerse y en la granja de ensamblados se realizan con un c贸digo, lo que simplifica enormemente su soporte.
Nuestros esfuerzos se pueden dividir en tres 谩reas:
- creaci贸n de una granja de ensamblaje: el mismo lugar donde se recolectar谩 todo lo que se ha recolectado y verificado;
- desarrollo de pruebas est谩ticas: verificar la exactitud de los recursos, sus relaciones, lanzar analizadores de c贸digo;
- desarrollo de pruebas de tiempo de ejecuci贸n: lanzamiento de la aplicaci贸n en modo de reproducci贸n autom谩tica.
Una tarea separada era organizar el lanzamiento de pruebas en la m谩quina del desarrollador. Era necesario minimizar el tiempo de ejecuci贸n localmente (el desarrollador no tiene que esperar 10 minutos para confirmar una l铆nea) y asegurarse de que cada sistema que realiza los cambios tenga instalado nuestro sistema.
Muchos requisitos: un sistema
Durante el desarrollo, hay un conjunto completo de ensamblajes que pueden ser 煤tiles: con y sin trampas, beta o alfa, iOS o Android. En cada caso, se pueden necesitar diferentes recursos, configuraciones o incluso c贸digos diferentes. Escribir scripts para pruebas est谩ticas para cada posible ensamblaje da como resultado un sistema complejo con muchos par谩metros. Adem谩s de ser dif铆cil de mantener y modificar, cada proyecto tambi茅n tiene su propio conjunto de bicicletas con muletas.
A trav茅s de prueba y error, llegamos a un sistema, cada prueba en la que puede tener en cuenta el contexto de lanzamiento y decidir si ejecutarlo o no, qu茅 exactamente y c贸mo verificarlo. Al comienzo de las pruebas, identificamos tres propiedades principales:
- tipo de ensamblaje: para los recursos de liberaci贸n y depuraci贸n, las verificaciones diferir谩n en t茅rminos de rigor, integridad de la cobertura, as铆 como configuraciones para identificadores y verificaci贸n de la funcionalidad disponible;
- plataforma: lo que es v谩lido para Android puede ser incorrecto para iOS, los recursos tambi茅n se recopilan de manera diferente y no todos los recursos en la versi贸n de Android estar谩n en iOS y viceversa;
- ubicaci贸n de lanzamiento: d贸nde exactamente estamos lanzando: en el agente de compilaci贸n donde se necesitan todas las pruebas disponibles o en la computadora del usuario donde se debe minimizar la lista de startups.
Sistema de prueba est谩tica
El n煤cleo del sistema y el conjunto b谩sico de pruebas est谩ticas se implementan en python. La base es solo unas pocas entidades:
El contexto de prueba es un concepto extenso. Almacena tanto los par谩metros de compilaci贸n como de lanzamiento, de los que hablamos anteriormente, as铆 como la metainformaci贸n que las pruebas llenan y usan.
Primero debe comprender qu茅 pruebas ejecutar. Para esto, la metainformaci贸n contiene los tipos de recursos que nos interesan espec铆ficamente en este lanzamiento. Los tipos de recursos est谩n determinados por las pruebas registradas en el sistema. Una prueba se puede "asociar" con un solo tipo o varios, y si, en el momento de la confirmaci贸n, resulta que los archivos que verifica esta prueba han cambiado, entonces el recurso asociado ha cambiado. Esto se adapta convenientemente a nuestra ideolog铆a: ejecutar localmente la menor cantidad de comprobaciones posibles: si los archivos de los que es responsable la prueba no han cambiado, entonces no es necesario ejecutarlos.
Por ejemplo, hay una descripci贸n del pez, en la que se indican el modelo 3D y la textura. Si el archivo de descripci贸n ha cambiado, se verifica que el modelo y la textura indicados existen. En otros casos, no es necesario ejecutar un control de peces.
Por otro lado, cambiar un recurso puede requerir cambios y entidades dependiendo de 茅l: si el conjunto de texturas que almacenamos en archivos xml ha cambiado, entonces es necesario verificar modelos 3D adicionales, ya que puede resultar que la textura que necesita se elimine. Las optimizaciones descritas anteriormente se aplican solo localmente en la m谩quina del usuario en el momento de la confirmaci贸n, y cuando se inicia en la granja de ensamblados, se supone que todos los archivos han cambiado y ejecutamos todas las pruebas.
El siguiente problema es la dependencia de algunas pruebas en otras: es imposible verificar el pescado antes de encontrar todas las texturas y modelos. Por lo tanto, dividimos toda la ejecuci贸n en dos etapas:
- preparaci贸n de contexto
- comprobando
En la primera etapa, el contexto se llena con informaci贸n sobre los recursos encontrados (en el caso de los peces, con identificadores de patrones y texturas). En la segunda etapa, utilizando la informaci贸n almacenada, simplemente verifique si existe el recurso deseado. Un contexto simplificado se presenta a continuaci贸n.
class VerificationContext(object): def __init__(self, app_path, build_type, platform, changed_files=None): self.__app_path = app_path self.__build_type = build_type self.__platform = platform
Habiendo determinado todos los par谩metros que afectan el lanzamiento de la prueba, logramos ocultar toda la l贸gica dentro de la clase base. En una prueba espec铆fica, queda por escribir solo la prueba misma y los valores necesarios para los par谩metros.
class TestCase(object): def __init__(self, name, context, build_types=None, platforms=None, predicate=None, expected_resources=None, modified_resources=None): self.__name = name self.__context = context self.__build_types = build_types self.__platforms = platforms self.__predicate = predicate self.__expected_resources = expected_resources self.__modified_resources = modified_resources
Volviendo al ejemplo de los peces, necesitamos dos pruebas, una de las cuales encuentra texturas y las registra en contexto, la otra busca texturas para los modelos encontrados.
class VerifyTexture(TestCase): def __init__(self, context): super(VerifyTexture, self).__init__('VerifyTexture', context, build_types=['production', 'hook'], platforms=['windows', 'ios'], expected_resources=None, modified_resources=['Texture'], predicate=lambda file_path: os.path.splitext(file_path)[1] == '.png') def _prepare_impl(self): texture_dir = os.path.join(self.context.app_path, 'resources', 'textures') for root, dirs, files in os.walk(texture_dir): for tex_file in files: self.context.register_resource('Texture', tex_file) class VerifyModels(TestCase): def __init__(self, context): super(VerifyModels, self).__init__('VerifyModels', context, expected_resources=['Texture'], predicate=lambda file_path: os.path.splitext(file_path)[1] == '.obj') def _run_impl(self): models_descriptions = etree.parse(os.path.join(self.context.app_path, 'resources', 'models.xml')) for model_xml in models_descriptions.findall('.//Model'): texture_id = model_xml.get('texture') texture = self.context.get_resource('Texture', texture_id) if texture is None: self.fail('Texture for model {} was not found: {}'.format(model_xml.get('id'), texture_id))
Proyecto extendido
El desarrollo del juego en Playrix se lleva a cabo en su propio motor y, en consecuencia, todos los proyectos tienen una estructura de archivo y un c贸digo similares utilizando las mismas reglas. Por lo tanto, hay muchas pruebas generales que se escriben una vez y est谩n en el c贸digo general. Es suficiente que los proyectos actualicen la versi贸n del sistema de prueba y se conecten una nueva prueba.
Para simplificar la integraci贸n, escribimos un corredor que recibe el archivo de configuraci贸n y las pruebas de dise帽o (sobre ellos m谩s adelante). El archivo de configuraci贸n contiene informaci贸n b谩sica sobre la que escribimos anteriormente: tipo de ensamblaje, plataforma, ruta al proyecto.
class Runner(object): def __init__(self, config_str, setup_function): self.__tests = [] config_parser = RawConfigParser() config_parser.read_string(config_str) app_path = config_parser.get('main', 'app_path') build_type = config_parser.get('main', 'build_type') platform = config_parser.get('main', 'platform') ''' get_changed_files CVS ''' changed_files = None if build_type != 'hook' else get_changed_files() self.__context = VerificationContext(app_path, build_type, platform, changed_files) setup_function(self) @property def context(self): return self.__context def add_test(self, test): self.__tests.append(test) def run(self): for test in self.__tests: test.init() for test in self.__tests: test.prepare() for test in self.__tests: test.run()
La belleza del archivo de configuraci贸n es que se puede generar en la granja de ensamblaje para diferentes ensamblajes en modo autom谩tico. Pero pasar la configuraci贸n de todas las pruebas a trav茅s de este archivo puede no ser muy conveniente. Para hacer esto, hay un xml de configuraci贸n especial que se almacena en el repositorio del proyecto y se escriben en 茅l listas de archivos ignorados, m谩scaras para buscar en el c贸digo, etc.
Ejemplo de archivo de configuraci贸n [main] app_path = {app_path} build_type = production platform = ios
Ejemplo de tuning xml <root> <VerifySourceCodepage allow_utf8="true" allow_utf8Bom="false" autofix_path="ci/autofix"> <IgnoreFiles>*android/tmp/*</IgnoreFiles> </VerifySourceCodepage> <VerifyCodeStructures> <Checker name="NsStringConversion" /> <Checker name="LogConstructions" /> </VerifyCodeStructures> </root>
Adem谩s de la parte general, los proyectos tienen sus propias peculiaridades y diferencias; por lo tanto, hay conjuntos de pruebas de proyectos que est谩n conectados al sistema a trav茅s de la configuraci贸n del corredor. Para el c贸digo en los ejemplos, un par de l铆neas ser谩n suficientes para ejecutar:
def setup(runner): runner.add_test(VerifyTexture(runner.context)) runner.add_test(VerifyModels(runner.context)) def run(): raw_config = ''' [main] app_path = {app_path} build_type = production platform = ios ''' runner = Runner(raw_config, setup) runner.run()
Rastrillo recogido
Aunque Python en s铆 es multiplataforma, regularmente tuvimos problemas con el hecho de que los usuarios tienen su propio entorno 煤nico, en el que pueden no tener la versi贸n que esperamos, varias versiones o ning煤n int茅rprete. Como resultado, no funciona como esperamos o no funciona en absoluto. Hubo varias iteraciones para resolver este problema:
- Python y todos los paquetes son instalados por el usuario. Pero hay dos "peros": no todos los usuarios son programadores y la instalaci贸n mediante pip install para dise帽adores, y tambi茅n para programadores, puede ser un problema.
- Hay un script que instala todos los paquetes necesarios. Esto ya es mejor, pero si el usuario tiene instalada la python incorrecta, pueden producirse conflictos en el trabajo.
- Entregar la versi贸n correcta del int茅rprete y las dependencias del almacenamiento de artefactos (Nexus) y ejecutar pruebas en un entorno virtual.
Otro problema es el rendimiento. Cuantas m谩s pruebas, m谩s tiempo se verifica el cambio en la computadora del usuario. Cada pocos meses hay un perfil y optimizaci贸n de cuellos de botella. Por lo tanto, se mejor贸 el contexto, apareci贸 un cach茅 para archivos de texto, se mejoraron los mecanismos de predicados (determinando que este archivo es interesante para la prueba).
Y luego solo queda resolver el problema de c贸mo implementar el sistema en todos los proyectos y obligar a todos los desarrolladores a incluir ganchos de segunda mano, pero esta es una historia completamente diferente ...
Conclusi贸n
Durante el proceso de desarrollo, bailamos en el rastrillo, luchamos duro, pero a煤n as铆 obtuvimos un sistema que nos permite encontrar errores durante la confirmaci贸n, redujo el trabajo de los probadores y las tareas antes del lanzamiento sobre la falta de textura eran cosa del pasado. Para una felicidad completa, una configuraci贸n simple del entorno y la optimizaci贸n de las pruebas individuales no son suficientes, pero los golems del departamento de ci trabajan duro en esto.
Puede encontrar un ejemplo completo del c贸digo utilizado como ejemplos en el art铆culo en
nuestro repositorio .