Дело об исчезнувшем покрытии кода

Однажды, после штатного обновления версий библиотек, когда-то достаточно популярный плагин JaCoCo перестал считать покрытие кода тестами на одном из наших проектов в Каруне. Усердный поиск хоть как-то похожих проблем не принёс результатов. Помощи ждать было неоткуда, поэтому нам пришлось погрузиться в вопрос. Как же мы смогли вернуть покрытие? Расскажу под катом.

fb64aedabf4840da27a1ceb49543b590.jpeg

Первым делом, конечно же, мы нашли ту зависимость, которая повлияла на поведение плагина JaCoCo. Ей оказался Android Gradle plugin, который был обновлён c 4.1.3 на 4.2.0. Развитие плагина не стоит на месте: уже доступна следующая мажорная версия, но и миграция на неё почему-то не закрывает наш вопрос. 

Дальнейшим шагом логично почитать список изменений к выпуску Android Gradle plugin, которые заботливо расписаны тут. Первым, что привлекло внимание, оказалась миграция на Java 8.

New features

This version of the Android Gradle plugin includes the following new features. Java language version 8 by default

Может, оно? Но в конфигурации сборки с помощью sourceCompatibility указывалась 8-ю версия и до обновления.  

Есть и такой интересный пункт про миграцию на AAPT2.

New JVM resource compiler

A new JVM resource compiler in Android Gradle plugin 4.2 tool replaces portions of the AAPT2 resource compiler, potentially improving build performance, especially on Windows machines. The new JVM resource compiler is enabled by default.

Похоже на подсказку к нашей задаче. Для того, чтобы проанализировать наш код, плагин JaCoCo запускает из своей Gradle задачи собственный Java Agent, который, в свою очередь, генерирует отчёт о покрытии на основе байткода проекта. Отчёт генерируется в бинарном формате, который в дальнейшем можно конвертировать в доступные из коробки csv, html и конечно же xml.

Для дальнейшего исследования как нельзя лучше подойдёт простейший тест на однострочник, как в примере ниже:

class NumberProvider {
   fun provideNumber(): Int {
       return 42
   }
}

class NumberProviderTest {
   @Test
   fun testNumberProvider() {
       val numberProvider = NumberProvider()

       val number = numberProvider.provideNumber()

       Assertions.assertEquals(42, number)
   }
}

Запустив генерацию отчёта, мы получили интересный результат об отсутствии покрытия. Генератор JaCoCo нашёл все методы целевого класса, но не принял во внимание тестовый код, в то время как считалка покрытия кода от JetBrains, встроенная в Idea, прекрасно отработала и показала покрытие.

Зелёным Idea пометила покрытые тестами строкиЗелёным Idea пометила покрытые тестами строкиHTML отчёт JaCoCoHTML отчёт JaCoCo

Время смотреть логи!

% ./gradlew jacocoTestAppDebugUnitTestReport

> Configure project :sample-app


> Task :sample-app:appDebugUnitTestCoverage
[ant:jacocoReport] Classes in bundle 'sample-app' do not match with execution data. For report generation the same class files must be used as at runtime.
[ant:jacocoReport] Execution data for class com/example/android/NumberProvider does not match.

Очень интересно!  

Пристальное изучение StackOverflow натолкнуло на мысль о конфликте версии Java между gradle и JaCoCo агентом. Но вот незадача: на тестовой машине нет других версий Java.

Попробовали для теста создать пустой проект без внешних зависимостей и на удивление обнаружили, что покрытие работает!

HTML отчёт JaCoCoHTML отчёт JaCoCo

Шаг за шагом мы отсекали из боевого проекта подключённые плагины, и, о чудо! Нашли виновника. Им оказался следующий герой:

image-loader.svg

Конечно, можно подождать, когда данный плагин также как и Android Gradle plugin будет поддерживать новый формат генерации ресурсов (помните о упоминании в списке изменений миграции на aapt2?). Коллеги из Dropbox не стали дожидаться, радикально отказавшись от плагина, правда, по другим причинам. Есть простое решение, которое поможет отключить плагин только для пайпы на CI, которая считает покрытие.

def getPropertyOrDefault = { property, defaultValue ->
   return project.hasProperty(property) ? project.property(property) : defaultValue
}

if (getPropertyOrDefault('skipFirebasePerf', "false") == "false") {
   apply plugin: 'com.google.firebase.firebase-perf'
}

Таким образом, абсолютно все сборки по умолчанию будут собираться с Firebase Performance, но для расчёта покрытия мы можем передать ключ PskipFirebasePerf="true"':

./gradlew -g test"$GRADLE_TARGET"ReleaseUnitTestCoverage -PskipFirebasePerf="true"

Надеемся, что вот-вот выйдет обновление для Firebase Performance, которое позволит убрать наше временное неочевидное решение (=костыль). А какие интересные задачки вам подкидывало обновление версий библиотек?

© Habrahabr.ru