В моем предыдущем сообщении об общих ловушках с задачами Gradle я упоминал входы и выходы задачи как один из источников, сбивающих с толку пользователей. Поскольку я едва коснулся поверхности, вот еще одна часть на эту тему.

Не повторяйся

Вы можете быть удивлены, но важной особенностью инструмента сборки, который считается быстрым, на самом деле является уход от работы, которую не нужно выполнять. В этом есть смысл: если исходный файл вообще не был изменен с момента его компиляции (или в более общем смысле: обработан), бессмысленно компилировать его снова и тратить время и ресурсы машины.

Эта функция называется инкрементная сборка и, конечно же, поддерживается Gradle. Для каждой отдельной задачи может быть определено 0 или несколько входов и выходов. Перед выполнением задачи создается снимок входных данных, а после завершения задачи на этот раз делается другой снимок выходных данных. Эти снимки сохраняются в кеше. Теперь, если задача вызывается еще раз, но ни один из ее входов или выходов не изменился с момента последнего запуска, она считается UP-TO-DATE и не будет выполнена. Это означает, что будут использоваться его кэшированные выходные данные.

Инкрементальная сборка обычно связана с компилированием файлов, да - вы правы. Но есть много других трудоемких задач, которые могут интенсивно использовать эту функцию, например:

  • применение миграции БД,
  • обработка файлов шаблонов,
  • загрузка зависимостей и так далее.

Обычно вход может быть определен как:

  • простое значение, такое как строка или число (что-то, что реализует Serializable интерфейс),
  • тип файловой системы, такой как каталог или файл,
  • вложенное значение - в основном это существо, которое не вписывается в две предыдущие категории, но имеет свои собственные свойства, которые могут быть как входами, так и выходами, например объект значения или класс данных.

В свою очередь, выходом задачи обычно является файл или каталог, но он также может быть @Destroyable, что является результатом удаления, например, набор файлов. Полный список возможных входов и выходов смотрите в документации.

Пример

Давайте посмотрим на следующую диаграмму классов:

Он представляет собой Version интерфейс, абстрактную AbstractVersion реализацию и две конкретные версии, расширяющие последнюю. Один правильный, а другой намеренно испорчен. Он всегда возвращает false для поля snapshot, независимо от аргумента, переданного через конструктор.

Существует также набор тестов VersionTest, но реализация которого AbstractVersion проверяется во время выполнения на основе имени из переменной среды VERSION_CLASS или DefaultVersion, если упомянутая переменная пуста. Почему такая странная установка? Это часть викторины по программированию, где пользователя просят написать набор тестов для класса Version (что намного сложнее, чем в этом тривиальном примере). Когда выполняются пользовательские тесты, реализации Verions загружаются динамически, и проверяется, прошли ли тесты для данной версии тесты или нет. После этого пользователь получает оценку.

Репозиторий GitHub с приведенным выше примером можно найти здесь, клонируйте его и проверьте тег task-io-no-input. Когда вы запустите ./gradlew, вы получите входные и выходные данные, определенные для test задачи, перечисленные на экране. Как видите, входные данные содержат как исходные наборы (тестовые и основные), так и зависимости. Что интересно, скрипт Gradle сам по себе является входом, однако его нет в списке. Выходные данные содержат результаты тестирования и отчет о тестировании.

Теперь, пожалуйста, выполните следующую команду:

Вы увидите, что задача thetest была запущена, и все результаты были сгенерированы. Если вы запустите ту же команду во второй раз, то заметите, что test (и другие) задачи были помечены как UP-TO-DATE и ничего не изменилось.

Теперь мы хотим проверить, не завершились ли тесты для намеренно испорченного класса: FailedVersionSnapshotAlwaysFalse, поэтому мы запускаем:

Однако .. тоже ничего не произошло. Мы ожидали, что на этот раз тесты не пройдут, но на самом деле произошло правильное поведение. Исходники скомпилированы, ни один из выходов не удален, зачем запускать тесты второй раз?

Теперь у нас есть 3 варианта, чтобы заставить его работать: Хороший, Плохой и Уродливый.

Плохая - это добавить clean к выполняемым задачам. Это удалит все выходы и повторит всю работу заново. Это бессмысленно. Исходники скомпилированы, перекомпилировать их не нужно, так как тестовые классы загружаются динамически.

Уродливое - удалить один из выходов, определенных для задачи test - Gradle заметит, что один из выходов отсутствует, и повторно запустит задачу. Это работает, но таким образом мы взламываем систему вместо того, чтобы использовать ее функции должным образом.

Хорошо - определить правильный ввод, который Gradle будет учитывать при вычислении моментального снимка входных данных. Для этого вам нужно добавить в скрипт build.gradle.kts следующий фрагмент кода:

или просто проверьте клонированное репо в HEAD из master ветки, где оно уже определено. Теперь, когда вы измените значение переменной VERSION_CLASS, вы заметите, что Gradle обнаружил, что ввод был изменен, и перезапустит тесты. Однако, поскольку они не были изменены, исходники не перекомпилируются.

Если вам интересно, действительно ли определение ввода как свойства является реальной ситуацией, ознакомьтесь с этим вопросом, опубликованным на StackOverflow. И проголосуйте за ответ;)

Резюме

Прочитав этот пост, вы должны знать, что:

  • чтобы ускорить процесс, вам следует избегать этого;)
  • каждая задача в Gradle имеет настраиваемые входы и выходы
  • вы несете ответственность за правильную настройку входов и выходов
  • вы должны использовать встроенные функции вашего набора инструментов