17 Jan 2015

Setting javac paths in Ant and Antrun

Resume: article describes how you configure Java compiler in Ant both in a standalone option and as Antrun Maven Plugin. If configured wrong, you may end up with unnecessary forked processes or errors like this:

Unable to find a javac compiler;
 [ERROR] com.sun.tools.javac.Main is not on the classpath.
 [ERROR] Perhaps JAVA_HOME does not point to the JDK.
 [ERROR] It is currently set to "/Library/Java/JavaVirtualMachines/jdk1.7.0_71.jdk/Contents/Home/jre"

As it appears, there are subtleties which may drive you crazy before you start understanding what’s going on.

Which javac should Ant choose?

There are 2 options Ant can use to compile code:

  • load and use compiler classes from classpath

  • trigger external javac fork (many ways of configuring this)

Using javac classes from classpath

When you specify this option <javac compiler="javac1.6"> it will try to load com.sun.tools.javac.Main and if it’s not found you’ll get: Unable to find a javac compiler. Why wouldn’t Ant find it? If you run Ant as standalone script, it will add $JAVA_HOME/lib/tools.jar to the classpath and the class will be found. But if you wrapped Ant by Maven using antrun plugin, Maven won’t load tools.jar and will directly call Ant classes. Ant cannot load the required compiler class and thus throws the aforementioned error. In order to escape this, you need to add tools.jar explicitly as a dependency to the plugin:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-antrun-plugin</artifactId>
    <dependencies>
        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.5.0</version>
            <scope>system</scope>
            <systemPath>${env.JAVA_HOME}/lib/tools.jar</systemPath>
        </dependency>
    </dependencies>
</plugin>

Note, that potentially some JDK distributions may have this jar in different places. Or may not have it at all (these classes reside in a different jar). Use Maven Profiles with different paths to the dependency set. And then activate those profiles by JDK version or another environment property. Another option - configure this in settings.xml.

How come Ant doesn’t find tools.jar while maven-compiler-plugin finds it without additional tweaks? Maven Compiler uses Plexus Compiler which is just a wrapper that uses standard javax.tools.ToolProvider. This ToolProvider looks for jars using java.home and loads tools.jar on its own. This differs from Ant which simply tries to load com.sun.tools.javac.Main and doesn’t search for any jars.

Change javac using JAVA_HOME

It’s possible to change javac by changing what Java is used by Ant itself. And it will work for both fork and non-fork options, just change JAVA_HOME when you start Ant: JAVA_HOME='/path/to/java/home' ant.

The rest of options are related only to forked javac.

Using System javac

System javac is found by Ant in java.home/../bin path. Which is based on Java you started Ant with. It’s used when Ant is trying to fork an external javac, but the location of javac wasn’t specified explicitly. 2 options to do that: <javac compiler="extjavac"> and <javac fork="true">

Since Ant uses java.home to locate a system JDK, you may be tempted to try changing the property to point to a different JDK (not the one Ant was started with). I wouldn’t recommend it, but it’s still worth mentioning. If Ant is invoked directly by another tool (e.g. it was wrapped by Maven or Gradle), it’s possible in their code or configuration to change system properties like java.home. Note, that it should point to JRE inside of JDK. This is by far the most complicated and subtle way of changing javac in Ant.

Explicitly specifying javac

You can specify what javac to use (and it again will be a fork): <javac compiler="extjavac" executable="/path/to/javac"> and <javac fork="true" executable="/path/to/javac">. This will find javac itself as well as related JDK (e.g. tools.jar). Note, that if you specify executable without extjavac or fork=true, the option will be ignored and you won’t actually change javac.

To fork or not to fork

In many situations it’s vital to restrict resources consumed by processes. CI Server shared between many teams would be one example since you don’t want a build to eat all resources and block other builds. In this case a CI engineer would set the memory limits (Heap, PermGen) in build or CI-wide. And while the original Ant process will use these to restrict its appetite, other processes spawned (forked) by the original build will be able to eat more. Ergo, it’s better not to use forks if it’s possible.

JAVA_HOME vs. java.home

Many threads about javac task talk about JAVA_HOME, I thought it will be appropriate to squeeze in this information here. These variables might be confusing, but they are different things that point to different locations. While JAVA_HOME is an environment variable set by user himself (or installer), java.home is a system property set by JVM during bootstrap and it usually points to $JAVA_HOME/jre. Though it may not. It’s not guaranteed because it’s determined by several factors. More details can be found in JVM code, see init_system_properties_values() function (thanks to this SO post for pointing that out).

Read more
06 Aug 2014

Nexus: why you shouldn't use LATEST

In every team where I happened to work with Sonatype Nexus I’ve seen the same misunderstanding on what LATEST version of artifacts is. People didn’t always understand why they shouldn’t use it.

Every artifact has a meta file maven-metadata.xml, which looks something like this:

<?xml version="1.0" encoding="UTF-8"?>
<metadata>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-javadoc-plugin</artifactId>
  <versioning>
    <latest>2.9.1</latest>
    <release>2.9.1</release>
    <versions>
      <version>2.8.1</version>
      <version>2.9</version>
      <version>2.9.1</version>
    </versions>
    <lastUpdated>20130628064237</lastUpdated>
  </versioning>
</metadata>

Now let’s imagine you want to download the latest version of the artifact. Of course first links in google will lead you to the REST query: http://somerepo.org/service/local/artifact/maven/redirect?r=central-proxy&g=groupId&a=artifactId&v=LATEST And maybe, just maybe, it will work for some time. But later at some point Nexus will start returning old artifacts.

This is because when Nexus searches for LATEST first it checks whether maven-metadata.xml has <latest> and chooses it. If this tag is absent (which is usually true at first), then the last item in <versions> is used.

This mechanism meets your expectations for some time because at the beginning maven-metadata.xml doesn’t have <latest>. Versions are added to the end of the list as new artifacts get uploaded which means that the latest version is returned. But then one of these happens:

  • Someone/something puts <latest> to the meta file.

  • Developers work in branches and several teams independently upload artifacts with different versions. The last upload is the winner - one of artifacts will be listed in the end of <versions> and therefore it will be chosen as LATEST.

As it appears in Nexus LATEST works correctly only for plugins and this mechanism MUST NOT be used for casual artifacts.

Couple of Q/As:

Who updates maven-metadata.xml?

Albeit many think that Nexus is the one, it’s actually Maven Deploy Plugin who does the trick: it first fetches existing maven-medatadata.xml from remote repo, then it updates all the required information and uploads it back to the repo. If the file doesn’t exist in the first place, then it gets created and uploaded.

How come <latest> shows up if Maven doesn’t use it?

Maven doesn’t actually add this tag. Nexus has its utility mechanisms, one of them is Rebuild Metadata. This item can be found in the context menu of the repositories and folders. It removes existing maven-metadata.xml, then it iterates over the artifacts and creates a brand new meta file according to what it found. And this is when <latest> is added. What for? I wish I knew. I even noticed <latest> in cases no one (presumably) ran Rebuild Metadata, thus one may assume there are other events that trigger these updates. Note that during Rebuild the versions are sorted according to the internal algorithms of Maven, these algorithms are then used by Nexus:

The ordering is derived by parsing the version string and supports sematic versioning with additional semantics for specific classifiers. Further details can be found in the documentation for the implementing class GenericVersionScheme.

This means that what was LATEST before may not be latest anymore.

Why <latest> is not always up-to-date?

Even if you happened to start Rebuild Metadata, this is still a one-time action. Afterwards it’s Maven who updates meta files. And it won’t do anything with <latest> - Maven will simply copy it from the maven-metadata.xml originally obtained from Nexus.

Let’s sum up

If you need a mechanism to fetch the latest version of the artifact, you MUST implement it yourself (presumably you’ll write a script). Those out-of-box mechanisms Nexus provides will simply stop working at some point.

Also note, that if you think you need the latest versions, you’re probably moving in a wrong direction, I haven’t seen good workflows which require latests.

PS: the links on Sonatype Wiki are now broken, they probably moved their pages or removed them from public access. URLs will be updated when I find new locations.

Read more
13 Jan 2014

Nexus - почему не нужно использовать LATEST

Везде, где мне приходилось работать с Sonatype Nexus в какой-то момент возникало недопонимание о том что такое LATEST версия артефакта и почему ее нельзя использовать.

Любой артефакт обладает неким информационным файлом maven-metadata.xml, содержимое которого приблизительно такое:

<?xml version="1.0" encoding="UTF-8"?>
<metadata>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-javadoc-plugin</artifactId>
  <versioning>
    <latest>2.9.1</latest>
    <release>2.9.1</release>
    <versions>
      <version>2.8.1</version>
      <version>2.9</version>
      <version>2.9.1</version>
    </versions>
    <lastUpdated>20130628064237</lastUpdated>
  </versioning>
</metadata>

Итак, вы захотели получить последнюю версию артефакта. Вы быстро нагуглите как это можно сделать: http://somerepo.org/service/local/artifact/maven/redirect?r=central-proxy&g=groupId&a=artifactId&v=LATEST И первое время это возможно даже будет работать. Однако в какой-то момент Nexus начнет вам возвращать старые артефакты.

Это происходит из-за алгоритма, который использует Nexus для нахождения LATEST: сначала смотрит если в maven-metadata.xml есть <latest> и возвращает его. Если тега нет, то вернется последняя версия в списке <versions>.

Изначально механизм этот работает лишь потому, что в maven-metadata.xml просто нет <latest>. Версии отсортированы в порядке добавления, возвращается последняя. Но в какой-то момент происходит что-то из двух:

  • Кто-то таки проставляет <latest> в мета-файл.

  • Работа идет в ветках и две команды независимо друг от друга заливают артефакты с разными версиями. Кто залил последний - того и последняя версия. Просто-напросто в конец добавляется та версия, которая сейчас деплоится.

Как оказывается в Nexus LATEST работает правильно только для плагинов и использовать этот механизм для обычных артефактов нельзя.

Несколько ожидаемых вопросов:

Кто обновляет maven-metadata.xml?

Этот файл обновляется не Nexus’ом, а Maven Deploy Plugin’ом: сначала он забирает из репозитория существующий maven-medatadata.xml, затем обновляет информацию и заливает обратно в репозиторий. Если существующего файла нет, создается новый и заливается в Nexus.

Откуда берется <latest> если Maven его никак не использует?

Maven этот тег и не проставляет. У Nexus’a есть некие утилитарные механизмы, среди таких - Rebuild Metadata. Этот пункт можно найти в контекстном меню репозиториев и каталогов. Этот механизм удаляет существующую maven-metadata.xml, просматривает артефакты и создает новый файл. И в этот момент он создает <latest>. Для чего - хрен знает, но создает. Причем я замечал <latest> даже в том случае, когда Rebuild Metadata вроде как и не делался, поэтому есть подозрения что это происходит еще по каким-то событиям. Имейте в виду, что сортировка версий происходит исходя из механизмов зашитых в Maven, а эти механизмы уже используются Nexus’ом:

The ordering is derived by parsing the version string and supports sematic versioning with additional semantics for specific classifiers. Further details can be found in the documentation for the implementing class GenericVersionScheme.

Почему <latest> устаревает?

Даже если Rebuild Metadata и происходит, то это разовое действие. Дальше метадата-файл обновлять все равно будет Maven. А тот просто ничего не делает с <latest> - она копируется из того maven-metadata.xml, который был загружен из Nexus’a.

Подведем итоги

Если вам нужен механизм получения последней версии артефакта, этот механизм вы сами и должны будете создать (написать скрипт). Механизм, используемый Nexus’ом по умолчанию в какой-то момент просто перестанет работать.

К тому же если вы считаете вам нужна где-то последняя версия артефактов, скорей всего вы двигаетесь в неправильном направлении, мне не приходилось видеть хорошо поставленные процессы где была нужда в LATEST.

PS: ссылки на Sonatype Wiki сломались, они либо перенесли куда-то страницы, либо убрали их из открытого доступа. Я обновлю адреса как только найду новое расположение.

Read more
12 Mar 2013

Интеграционные тесты + Maven

Резюме: хоть Maven и является инструментом, стандартизирующим структуру проекта и его сборку, однако он с крахом провалил стандартизацию разного рода тестов. Ща разберем полеты.

Итак, почему же все-таки Maven не справляется со стандартизацией тестов, ведь:

  • Модульные тесты:

    • Выполняются на фазе test
    • Лежат в src/test каталоге
    • Названия тестовых классов содержат слово Test
  • Интеграционные тесты:

    • Выполнаются на фазе integration-test
    • Лежат в src/test
    • В их имени должны быть буквы IT обозначающие собсно IntegrationTests.

Проблем на самом деле с этим несколько:

  • Фаза integration-test выполняется после модульных тестов, это значит что отдельно их не запустить. Каждый раз когда мы запускаем интеграционные тесты, выполняются модульные. Однако обычно мы хотим запустить модульные тесты один раз, а затем отдельно запускать интеграционные. Выходит чтоб модульные тесты не выполнялись нам нужно пропускать их с помощью -DskipTests, затем окажется что интеграционные тесты тоже не запускаются потому что failsafe плагин использует под собой surefire, начнется геморрой с созданием профилей и в конце концов начнет казаться что все это слишком сложно. Кстати, почему нам важно запускать тесты раздельно:

  • Разработчики могут быстро получить “зеленый” фидбек и продолжить работать. Именно модульные тесты способны быстро дать базовый ответ.

  • Немодульные тесты зависят от окружения, а значит могут падать. Если упали такие тесты, то еще нужно разобраться из-за окружения ли, либо и вправду сломалась логика.

  • Интеграционные тесты медленные, в зависимости от длительности их можно запускать по каждому коммиту, раз в день и т.п. Они тоже могут дробиться на более мелкие группы тестов, например, основные Smoke тесты выполняются первыми, затем регрессия, затем приемочные, затем какие-нибудь нагрузочные и т.п. Их удобно разделять потому что мы точно будем знать что сломалось. А также мы можем запускать отдельно определенную группу тестов и не ждать 4 часа чтоб прошло все.

  • Мы можем хотеть запускать одни и те же системные несколько раз. Например, мы их прогнали по текущему коммиту, а затем еще по тому что был неделю назад для того чтоб увидеть разницу между результатами.

  • Для системных тестов часто нужно подготовить окружение прежде чем их запускать.

  • Стандартно failsafe использует каталог src/test, а нам редко когда нужно помещать интеграционные и модульные тесты и ресурсы в одни и те же пакеты.

  • Фаза integration-test запускается перед install каждый раз. Мы же не хотим запускать медленные интеграционные тесты каждый раз когда устанавливаем артефакто в локальный репозиторий.

  • К сожалению для большинства разработчиков существуют лишь модульные, интеграционные тесты и “то, что делают QA”. Однако на самом деле тесты разделяются как минимум на модульные, системные и компонентные в зависимости от масштабов. Также есть функциональные и нефункциональные тесты и т.п. Более подробно можно с видами тестирования ознакомиться в одноименной статье. Но что главное - мы можем захотеть разделить все эти тесты. Для одних нужно поднимать все приложение, для других - лишь часть, для третьих вообще одного класса хватит. Однако Maven их никак не различает и не разделяет, у него есть либо модульные, либо интеграционные.

В общем подумав немного можно прити к выводу, что раз стандартный механизм Maven настолько несовершенен и все равно не сможет поддерживать всего нужного, мы можем отойти от него. Вместо этого предлагаю использовать plain old surefire plugin. Да, этот плагин заточен на написание модульных тестов, однако они по факту ничем не будут отличаться от “немодульных” - те же JUnit/TestNG будут описывать всю их логику (хотя тут позволяется также использовать всякие BDD фреймворки навроде JBehave, однако не о них речь).

Так вот как же это будет выглядеть. Для каждого из видов тестирования мы будем создавать а) профиль б) каталог с исходниками и ресурсами. Конфигурироваться же Maven будет следующим образом:

<properties>
    <test.sourceDirectory>${project.basedir}/src/test/java</test.sourceDirectory>
    <test.resourceDirectory>${project.basedir}/src/test/resources</test.resourceDirectory>
  </properties>

  <build>
    <testSourceDirectory>${test.sourceDirectory}</testSourceDirectory>
    <testResources>
      <testResource>
        <directory>${test.resourceDirectory}</directory>
      </testResource>
    </testResources>
  </build>
  <profiles>
    <profile>
      <id>component-test</id>
      <properties>
        <test.sourceDirectory>${project.basedir}/src/component-test/java</test.sourceDirectory>
        <test.resourceDirectory>${project.basedir}/src/component-test/resources</test.resourceDirectory>
      </properties>
    </profile>
    <profile>
      <id>system-test</id>
      <properties>
        <test.sourceDirectory>${project.basedir}/src/system-test/java</test.sourceDirectory>
        <test.resourceDirectory>${project.basedir}/src/system-test/resources</test.resourceDirectory>
      </properties>
    </profile>
  </profiles>

Заметьте, что в относительно небольшом куске конфигурации мы описали 3 вида тестов и все они находятся в разных каталогах:

  • Модульные: mvn test

  • Компонентные: mvn test -Pcomponent-test

  • Системные: mvn test -Psystem-test

Структура каталогов тогда такая:

src
 |_main
      |_java
      |_resources
 |_test
      |_java
      |_resources
 |_component-test
      |_java
      |_resources
 |_system-test
      |_java
      |_resources

Правда системные тесты как правило имеет смысл выделять в отдельные модули, ато и проекты.

Единственное в чем я слукавил: фаза интеграционных тестов делиться на несколько шагов и это позволяет нам сначала поднять окружение, а затем его остановить. Подход с surefire такого не даст, вам придется вручную запускать окружение с помощью явно указанных команд. Однако:

  • Если нам нужно полностью развернутое окружение, то это не всегда возможно автоматизировать без написания скриптов, что значит что в большинстве случаев все равно придется руками дергать их

  • Сложность в конфигурировании разного рода тестов перекрывает сложность запуска какого-нибудь tomcat:start

  • Т.к. обычно системные тесты выносят в отдельные модули или проекты, то там вполне можно использовать фазу integration-test как единственную, которая запускает тесты.

Но если вам правда хочется удобным образом автоматизировать развертывание окружения, возможно вам будет лучше сконфигурировать integration-test фазу для этого.

JUnit Categories, TestNG Groups

Последнее о чем стоит упомянуть - это JUnit категории и TestNG группы. Они позволяют с помощью аннотаций как-то помечать тесты. Однако если в TestNG это хоть как-то можно сделать удобно, то в JUnit это сделано через прямую кишку и вам придется конфигурить много профилей, у вас не выйдет задать exclude=IntegrationTest по умолчанию, а затем через командную строку активировать какие-то другие категории, для этого вам придется все равно создавать профили, а значит никакого удобства такой способ не принесет. Раз не выйдет это использовать с JUnit’ом, значит не имеет смысл стандартизировать такой подход.

Read more