Hola Habr! Me llamo Egor Danilenko. Estoy desarrollando una plataforma digital para la banca corporativa por Internet de Sberbank Business Online, y hoy quiero contarles sobre el procedimiento de desarrollo de CI adoptado por nosotros.
¿Cómo llegan los cambios de desarrollador a una infusión en la rama de lanzamiento? El desarrollador realiza cambios localmente y se introduce en nuestro sistema de control de versiones. Usamos Bitbucket con el complemento de un autor (escribimos sobre este complemento anteriormente
aquí ). En estos cambios, se inicia el ensamblaje y se persiguen las pruebas (unidad, integración, funcional). Si el ensamblaje no falló y todas las pruebas pasaron con éxito, así como después de una revisión exitosa, la solicitud de extracción se vierte en la rama principal.
Pero con el tiempo, el número de equipos ha crecido. El número de pruebas ha crecido proporcionalmente. Entendimos que un número tan grande de equipos aceleraría la aparición del problema de "lenta extracción-solicitud-verificación", y sería imposible desarrollar un producto. Actualmente, tenemos alrededor de 40 equipos. Junto con nuevas características, traen nuevas pruebas, que también deben ejecutarse en solicitudes de extracción.
Pensamos que sería genial si supiéramos qué pruebas ejecutar para cambiar un fragmento de código en particular.
Y así es como resolvimos este problema.
Declaración del problema.
Hay un proyecto con pruebas, y queremos determinar qué pruebas deben ejecutarse cuando se "toca" un determinado archivo.
Todos sabemos acerca de la biblioteca de cobertura de código EclEmma JaCoCo. Lo tomamos como base.
Un poco sobre JaCoCo
JaCoCo es una biblioteca para medir la cobertura de código con pruebas. El trabajo se basa en el análisis de bytes de código. El agente recopila información de ejecución y la carga a pedido o apagado de la JVM.
Hay tres modos de recopilación de datos:
- Sistema de archivos: después de detener la JVM, los datos se escribirán en un archivo.
- TCP Socket Server: puede conectar herramientas externas a la JVM y recibir datos a través del socket.
- TCP Socket Client: cuando se inicia, el agente JaCoCo se conecta a un punto final TCP específico.
Hemos elegido la segunda opción.
Solución
Es necesario darse cuenta de la capacidad de ejecutar aplicaciones y las pruebas con el agente JaCoCo.
En primer lugar, agregamos a gradle la capacidad de ejecutar pruebas con el agente JaCoCo.
El agente de Java se puede iniciar:
-javaagent:[yourpath/]jacocoagent.jar=[option1]=[value1],[option2]=[value2]
Agregue una dependencia a nuestro proyecto:
dependencies { compile 'org.jacoco:org.jacoco.agent:0.8.0' }
Solo necesitamos comenzar con el agente para recopilar estadísticas, por lo que agregamos el indicador withJacoco con el valor predeterminado false a gradle.properties. También especificamos el directorio donde se recopilarán las estadísticas, la dirección y el puerto.
Agregue el argumento jvm con el agente a la tarea de inicio de prueba:
if (withJacoco.toBoolean()) { … jvmArgs "-javaagent:${tempPath}=${jacocoArgs.join(',')}".toString() }
Ahora, después de cada finalización exitosa de la prueba, necesitamos recopilar estadísticas con JaCoCo. Para hacer esto, escriba TestNG oyente.
public class JacocoCoverageTestNGListener implements ITestListener { private static final IntegrationTestsCoverageReporter reporter = new IntegrationTestsCoverageReporter(); private static final String TEST_NAME_PATTERN = "%s.%s"; @Override public void onTestStart(ITestResult result) { reporter.resetCoverageDumpers(String.format(TEST_NAME_PATTERN, result.getInstanceName(), result.getMethod().getMethodName())); } @Override public void onTestSuccess(ITestResult result) { reporter.report(String.format(TEST_NAME_PATTERN, result.getInstanceName(), result.getMethod().getMethodName())); } }
Agregue un oyente a testng.xml y coméntelo, ya que no lo necesitamos en una ejecución de prueba normal.
Ahora tenemos la oportunidad de ejecutar pruebas con el agente JaCoCo, con cada prueba exitosa se recopilarán estadísticas.
Un poco más sobre cómo se implementa el reportero para recopilar estadísticas.
Durante la inicialización del reportero, se establece una conexión con los agentes, se crea un directorio donde se almacenarán las estadísticas y se recopilarán las estadísticas.
Agregue el método de informe:
public void report(String test) { reportClassFiles(test); reportResources(test); }
El método reportClassFile crea la carpeta jvm en el directorio de estadísticas, en el que se almacenan las estadísticas recopiladas por los archivos de clase.
El método reportResources crea la carpeta de recursos, que almacena las estadísticas recopiladas sobre los recursos (para todos los archivos que no son de clase).
El informe contiene toda la lógica para conectarse a un agente, leer datos de un socket y escribir en un archivo. Implementado por herramientas proporcionadas por JaCoCo, como org.jacoco.core.runtime.RemoteControlReader / RemoteControlWriter.
Las funciones reportClassFiles y reportResources usan la función dumpToFile genérica.
public void dumpToFile(File file) { try (Writer fileWriter = new BufferedWriter(new FileWriter(file))) { for (RemoteControlReader remoteControlReader : remoteControlReaders) { remoteControleReader.setExecutionDataVisitor(new IExecutionDataVisitor() { @Override public void visitClassExecution(ExecutionData data) { if (data.hasHits()) { String name = data.getName(); try { fileWriter.write(name); fileWriter.write('\n'); } catch (IOException e) { throw new RuntimeException(e); } } } }); } } }
El resultado de la función será un archivo con un conjunto de clases / recursos que afecta esta prueba.
Y así, después de ejecutar todas las pruebas, tenemos un directorio con estadísticas sobre los archivos y recursos de la clase.
Queda por escribir una tubería para la recopilación de estadísticas diarias y agregar al lanzamiento de la tubería de comprobaciones de solicitud de extracción.
No estamos interesados en las etapas de montaje del proyecto, pero consideraremos la etapa para publicar estadísticas con más detalle.
stage('Agregate and parse result') { def inverterInJenkins = downloadMavenDependency( url: NEXUS_RELEASE_REPOSITORY, group: '', name: 'coverage-inverter', version: '0', type: 'jar', mavenHome: wsp ) dir('coverage-mapping') { gitFullCheckoutRef '', '', 'coverage-mapping', "refs/heads/${params.targetBranch}-integration-tests" sh 'rm -rf *' } sh "ls -lRa ..//out/coverage/" def inverter = wsp + inverterInJenkins.substring(wsp.length()) sh "java -jar ${inverter} " + "-d ..//out/coverage/jvm " + "-o coverage-mapping//jvm " + "-i coverage-config/jvm-include " + "-e coverage-config/jvm-exclude" sh "java -jar ${inverter} " + "-d ..//out/coverage/resources " + "-o coverage-mapping//resources " + "-i coverage-config/resources-include " + "-e coverage-config/resources-exclude" gitPush '', '', 'coverage-mapping', "${params.targetBranch}-integration-tests" }
En el mapeo de cobertura, necesitamos almacenar el nombre del archivo y dentro de él una lista de pruebas que deben ejecutarse. Dado que el resultado de recopilar estadísticas es el nombre de la prueba que almacena el conjunto de clases y recursos, debemos invertir todo y excluir datos innecesarios (clases de bibliotecas de terceros).
Invertimos nuestras estadísticas y empujamos a nuestro repositorio.
Las estadísticas se recopilan todas las noches. Se almacena en un repositorio separado para cada rama de lanzamiento.
Bingo!
Ahora, al ejecutar las pruebas, solo tenemos que encontrar el archivo modificado y determinar las pruebas que deben ejecutarse.
Los problemas que encontramos:
- Dado que JaCoCo solo funciona con bytecode, es imposible recopilar estadísticas sobre archivos como .xml, .gradle, .sql desde el cuadro. Por lo tanto, tuvimos que "sujetar" nuestras decisiones.
- Monitoreo constante de la relevancia de las estadísticas y la frecuencia de la asamblea, si la asamblea nocturna falla por alguna razón, entonces las estadísticas de "ayer" se usarán para la verificación en las solicitudes de extracción.