Halo, Habr! Nama saya Egor Danilenko. Saya sedang mengembangkan platform digital untuk perbankan Internet korporat dari Sberbank Business Online, dan hari ini saya ingin memberi tahu Anda tentang prosedur pengembangan CI yang diadopsi oleh kami.
Bagaimana perubahan pengembang sampai pada infus ke cabang rilis? Pengembang membuat perubahan secara lokal dan mendorong ke sistem kontrol versi kami. Kami menggunakan Bitbucket dengan plugin penulis (kami menulis tentang plugin ini sebelumnya di
sini ). Pada perubahan ini, perakitan diluncurkan dan tes dikejar (unit, integrasi, fungsional). Jika perakitan tidak gagal dan semua tes berhasil lulus, serta setelah peninjauan yang berhasil, permintaan tarik dituangkan ke cabang utama.
Namun seiring waktu, jumlah tim telah bertambah. Jumlah tes telah tumbuh secara proporsional. Kami memahami bahwa sejumlah tim seperti itu akan mempercepat timbulnya masalah "cek tarik-permintaan-lambat", dan tidak mungkin untuk mengembangkan suatu produk. Saat ini, kami memiliki sekitar 40 tim. Bersama dengan fitur-fitur baru, mereka membawa tes baru, yang juga harus dijalankan pada permintaan tarik.
Kami pikir akan keren jika kami tahu tes mana yang harus dijalankan untuk mengubah bagian kode tertentu.
Dan inilah cara kami memecahkan masalah ini.
Pernyataan masalah
Ada proyek dengan tes, dan kami ingin menentukan tes mana yang harus dijalankan ketika file tertentu "disentuh".
Kita semua tahu tentang perpustakaan cakupan kode EclEmma JaCoCo. Kami menganggapnya sebagai dasar.
Sedikit tentang JaCoCo
JaCoCo adalah perpustakaan untuk mengukur cakupan kode dengan tes. Pekerjaan ini didasarkan pada analisis byte kode. Agen mengumpulkan informasi eksekusi dan mengunggahnya atas permintaan atau penutupan JVM.
Ada tiga mode pengumpulan data:
- Sistem file: setelah menghentikan JVM, data akan ditulis ke file.
- TCP Socket Server: Anda dapat menghubungkan alat eksternal ke JVM dan menerima data melalui soket.
- TCP Socket Client: Ketika diluncurkan, agen JaCoCo terhubung ke titik akhir TCP tertentu.
Kami telah memilih opsi kedua.
Solusi
Anda harus menyadari kemampuan untuk menjalankan aplikasi dan menguji sendiri dengan agen JaCoCo.
Pertama-tama, kami menambah kemampuan untuk menjalankan tes dengan agen JaCoCo.
Agen Java dapat dimulai:
-javaagent:[yourpath/]jacocoagent.jar=[option1]=[value1],[option2]=[value2]
Tambahkan ketergantungan pada proyek kami:
dependencies { compile 'org.jacoco:org.jacoco.agent:0.8.0' }
Kami hanya perlu mulai dengan agen untuk mengumpulkan statistik, jadi kami menambahkan flag withJacoco dengan nilai default false ke gradle.properties. Kami juga menentukan direktori tempat statistik, alamat, dan port akan dikumpulkan.
Tambahkan argumen jvm dengan agen ke tugas peluncuran pengujian:
if (withJacoco.toBoolean()) { … jvmArgs "-javaagent:${tempPath}=${jacocoArgs.join(',')}".toString() }
Sekarang, setelah setiap tes berhasil diselesaikan, kita perlu mengumpulkan statistik dengan JaCoCo. Untuk melakukan ini, tulis pendengar TestNG.
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())); } }
Tambahkan pendengar ke testng.xml dan komentari, karena kita tidak memerlukannya dalam uji coba normal.
Sekarang kita memiliki kesempatan untuk menjalankan tes dengan agen JaCoCo, dengan setiap statistik uji yang berhasil akan dikumpulkan.
Sedikit lagi tentang bagaimana reporter diimplementasikan untuk mengumpulkan statistik.
Selama inisialisasi reporter, koneksi dibuat ke agen, direktori dibuat di mana statistik akan disimpan, dan statistik akan dikumpulkan.
Tambahkan metode laporan:
public void report(String test) { reportClassFiles(test); reportResources(test); }
Metode reportClassFile membuat folder jvm di direktori statistik, di mana statistik dikumpulkan oleh file kelas disimpan.
Metode reportResources membuat folder sumber daya, yang menyimpan statistik yang dikumpulkan tentang sumber daya (untuk semua file non-kelas).
Laporan ini berisi semua logika untuk menghubungkan ke agen, membaca data dari soket dan menulis ke file. Diterapkan oleh alat yang disediakan oleh JaCoCo, seperti org.jacoco.core.runtime.RemoteControlReader / RemoteControlWriter.
Fungsi reportClassFiles dan reportResources menggunakan fungsi generik dumpToFile.
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); } } } }); } } }
Hasil dari fungsi akan menjadi file dengan sekumpulan kelas / sumber daya yang mempengaruhi tes ini.
Jadi, setelah menjalankan semua tes, kami memiliki direktori dengan statistik pada file dan sumber daya kelas.
Masih menulis saluran pipa untuk pengumpulan statistik harian dan menambah peluncuran pipa pemeriksaan permintaan tarik.
Kami tidak tertarik pada tahap perakitan proyek, tetapi kami akan mempertimbangkan tahap untuk menerbitkan statistik secara lebih rinci.
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" }
Dalam pemetaan cakupan, kita perlu menyimpan nama file dan di dalamnya daftar tes yang perlu dijalankan. Karena hasil pengumpulan statistik adalah nama tes, yang menyimpan sekumpulan kelas dan sumber daya, kita perlu membalikkan semuanya dan mengecualikan data yang tidak perlu (kelas dari perpustakaan pihak ketiga).
Kami membalikkan statistik kami dan mendorong ke repositori kami.
Statistik dikumpulkan setiap malam. Itu disimpan dalam repositori terpisah untuk setiap cabang rilis.
Bingo!
Sekarang, ketika menjalankan tes, kita perlu menemukan file yang dimodifikasi dan menentukan tes yang perlu dijalankan.
Masalah yang kami temui:
- Karena JaCoCo hanya bekerja dengan bytecode, tidak mungkin untuk mengumpulkan statistik pada file seperti .xml, .gradle, .sql dari kotak. Karena itu, kami harus "mempercepat" keputusan kami.
- Pemantauan konstan terhadap relevansi statistik dan frekuensi majelis, jika majelis malam hari gagal karena suatu alasan, maka statistik "kemarin" akan digunakan untuk verifikasi dalam permintaan tarikan.