CLOVER🍀

That was when it all began.

Maven Surefire Plugin(Maven Failsafe Plugin)とJaCoCo Maven PluginとargLineと

これは、なにをしたくて書いたもの?

JaCoCo Maven PluginのようなJavaVM引数を使うようなMavenプラグインと、Maven Surefire Plugin(Maven Failsafe Pluginも)のargLine
一緒に使うとちょっとハマるのでメモとして。

Maven Surefire Plugin(Maven Failsafe Plugin)のargLine

JaCoCo Maven Pluginのjacoco:prepare-agentを使うと、こんな感じでargLineにJavaVM引数(具体的には-javaagent)を指定します。

[INFO] --- jacoco:0.8.12:prepare-agent (prepare-agent) @ jacoco-surefire-argline ---
[INFO] argLine set to -javaagent:$HOME/.m2/repository/org/jacoco/org.jacoco.agent/0.8.12/org.jacoco.agent-0.8.12-runtime.jar=destfile=/path/to/target/jacoco.exec

ここでMaven Surefire Plugin(Maven Failsafe Plugin)のargLineオプションを指定すると、JaCoCoがうまく動かなくなります。
理由は、Maven Surefire Plugin向けに設定したargLineがJaCoCo Maven Pluginが指定したJavaVM引数を上書きしてしまうからです。

これはよくハマる問題のようで、Maven Surefire Pluginのtestゴールにも書いてありますし

Since the Version 2.17 using an alternate syntax for argLine, @{...} allows late replacement of properties when the plugin is executed, so properties that have been modified by other plugins will be picked up correctly. See the Frequently Asked Questions page with more details:

surefire:test / Parameter Details / argLine

JaCoCo Maven Pluginのjacoco:prepare-agentゴールのドキュメントにも書いてあります。

f your project already defines VM arguments for test execution, be sure that they will include property defined by JaCoCo.

JaCoCo - jacoco:prepare-agent

Maven Surefire PluginのFAQ。

Frequently Asked Questions / How do I use properties set by other plugins in argLine?

結論としては、Maven Surefire Pluginの@{argLine}を使って指定するのがよさそうです。

少し試しておきます。

環境

今回の環境はこちら。

$ java --version
openjdk 21.0.4 2024-07-16
OpenJDK Runtime Environment (build 21.0.4+7-Ubuntu-1ubuntu222.04)
OpenJDK 64-Bit Server VM (build 21.0.4+7-Ubuntu-1ubuntu222.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.8 (36645f6c9b5079805ea5009217e36f2cffd34256)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 21.0.4, vendor: Ubuntu, runtime: /usr/lib/jvm/java-21-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-117-generic", arch: "amd64", family: "unix"

準備

Maven依存関係などはこちら。

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.26.3</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.3.1</version>
            </plugin>
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.12</version>
                <executions>
                    <execution>
                        <id>prepare-agent</id>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>report</id>
                        <goals>
                            <goal>report</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>

ソースコードを作成

とてもわざとらしいですが、システムプロパティを使ったソースコードとテストコードを用意します。

src/main/java/org/littlewings/jacoco/SystemPropertyService.java

package org.littlewings.jacoco;

public class SystemPropertyService {
    public String get(String name) {
        return System.getProperty(name);
    }
}

src/test/java/org/littlewings/jacoco/SystemPropertyServiceTest.java

package org.littlewings.jacoco;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class SystemPropertyServiceTest {
    @Test
    void systemProperty() {
        SystemPropertyService systemPropertyService = new SystemPropertyService();

        assertThat(systemPropertyService.get("message")).isNotNull();
        System.out.printf("message = %s%n", systemPropertyService.get("message"));
    }
}

システムプロパティよりも-Xmxなどの方がわかりやすい(というかそもそも使わなくてもいい)と思うのですが、テストでの見た目としては
このあたりの方がわかりやすいかなということで。

Maven Surefire PluginのargLineを使いつつJaCoCoのカバレッジレポートを取得する

まずはテストを実行してみます。

$ mvn test

システムプロパティを指定していないので、このテストは当然失敗するのですが

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running org.littlewings.jacoco.SystemPropertyServiceTest
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.769 s <<< FAILURE! -- in org.littlewings.jacoco.SystemPropertyServiceTest
[ERROR] org.littlewings.jacoco.SystemPropertyServiceTest.systemProperty -- Time elapsed: 0.636 s <<< FAILURE!
java.lang.AssertionError:

Expecting actual not to be null
        at org.littlewings.jacoco.SystemPropertyServiceTest.systemProperty(SystemPropertyServiceTest.java:12)
        at java.base/java.lang.reflect.Method.invoke(Method.java:580)
        at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
        at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)

[INFO]
[INFO] Results:
[INFO]
[ERROR] Failures:
[ERROR]   SystemPropertyServiceTest.systemProperty:12
Expecting actual not to be null
[INFO]
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0

jacoco.execはできるのでレポートは作ることができます。

$ mvn jacoco:report

こんな感じで。

[INFO] --- jacoco:0.8.12:report (default-cli) @ jacoco-surefire-argline ---
[INFO] Loading execution data file /path/to/target/jacoco.exec
[INFO] Analyzed bundle 'jacoco-surefire-argline' with 1 classes

target/site/jacocoディレクトリ内の中身については省略します。

ではここで1度mvn cleanします。

$ mvn clean

そしてMaven Surefire PluginのargLineにJavaVM引数を設定します。

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.3.1</version>
                <configuration>
                    <argLine>-Xmx1G -Dmessage=Hello</argLine>
                </configuration>
            </plugin>

テストを実行。今度は成功します。

$ mvn test

結果。

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running org.littlewings.jacoco.SystemPropertyServiceTest
message = Hello
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.243 s -- in org.littlewings.jacoco.SystemPropertyServiceTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

そしてJaCoCoのレポートを生成しようとすると

$ mvn jacoco:report

スキップされます。

[INFO] --- jacoco:0.8.12:report (default-cli) @ jacoco-surefire-argline ---
[INFO] Skipping JaCoCo execution due to missing execution data file.

そもそもtarget/jacoco.execも生成されていません。

$ ll target/jacoco.exec
ls: 'target/jacoco.exec' にアクセスできません: そのようなファイルやディレクトリはありません

JaCoCoのエージェントは組み込まれているんですけどね。

[INFO] --- jacoco:0.8.12:prepare-agent (prepare-agent) @ jacoco-surefire-argline ---
[INFO] argLine set to -javaagent:$HOME/.m2/repository/org/jacoco/org.jacoco.agent/0.8.12/org.jacoco.agent-0.8.12-runtime.jar=destfile=/path/to/target/jacoco.exec

これが最初に書いた問題です。

ちなみに、Maven Surefire Pluginはこの設定のままで

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.3.1</version>
            </plugin>

-DargLineで指定した場合でもテストは成功しますが

$ mvn test -DargLine='-Xmx1G -Dmessage=Hello'

カバレッジレポートを生成しようとすると

$ mvn jacoco:report

やっぱりスキップされます。

[INFO] --- jacoco:0.8.12:report (default-cli) @ jacoco-surefire-argline ---
[INFO] Skipping JaCoCo execution due to missing execution data file.

ではどうやって回避するかというと@{argLine}を追加します。

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.3.1</version>
                <configuration>
                    <argLine>@{argLine} -Xmx1G -Dmessage=Hello</argLine>
                </configuration>
            </plugin>

@{argLine}には、Mavenプラグインが実行時に設定した値が入ります。

Since the Version 2.17 using an alternate syntax for argLine, @{...} allows late replacement of properties when the plugin is executed, so properties that have been modified by other plugins will be picked up correctly.

surefire:test / Parameter Details / argLine

これでテストを実行してカバレッジレポートを作成すると

$ mvn clean test jacoco:report

今度はうまくいくようになります。

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running org.littlewings.jacoco.SystemPropertyServiceTest
message = Hello
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.908 s -- in org.littlewings.jacoco.SystemPropertyServiceTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] --- jacoco:0.8.12:report (default-cli) @ jacoco-surefire-argline ---
[INFO] Loading execution data file /paht/to/target/jacoco.exec
[INFO] Analyzed bundle 'jacoco-surefire-argline' with 1 classes

別解としては、Maven Surefire Pluginの設定は戻して

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.3.1</version>
            </plugin>

propertiesとしてargLineを定義することですね。

    <properties>
        ...

        <argLine>-Xmx1G -Dmessage=Hello</argLine>
    </properties>

これでもうまくいきます(jacoco:reportの実行結果は省略します)。

ただ、この方法だとコマンドライン-DargLineを指定するパターンとは共存できません。

$ mvn test -DargLine='-Xmx1G -Dmessage="Hello World"'

テスト自体はうまくいきますが

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running org.littlewings.jacoco.SystemPropertyServiceTest
message = Hello World
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.116 s -- in org.littlewings.jacoco.SystemPropertyServiceTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

レポートを生成しようとすると

$ mvn jacoco:report

やっぱりスキップされます。

[INFO] --- jacoco:0.8.12:report (default-cli) @ jacoco-surefire-argline ---
[INFO] Skipping JaCoCo execution due to missing execution data file.

これが嫌な場合は、以下のように@{argLine}${argLine}も書いておくとうまくいくようになります。

    <properties>
        ...

        <argLine>-Xmx1G -Dmessage=Hello</argLine>
    </properties>

    ...

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.3.1</version>
                <configuration>
                    <argLine>@{argLine} ${argLine}</argLine>
                </configuration>
            </plugin>

            ...

        </plugins>
    </build>

以下のパターンのいずれでも、target/jacoco.execが生成されるようになります。

$ mvn test
$ mvn test -DargLine='-Xmx1G -Dmessage="Hello World"'

-DargLineを指定しなかった場合は、propertiesに定義したデフォルト値で動作するという感じですね。

デフォルト値が要らないのであれば、中身は空でよいでしょう。

    <properties>
        ...

        <argLine></argLine>
    </properties>

こんなところでしょうか。

おわりに

Maven Surefire Plugin(Maven Failsafe Plugin)とJaCoCo Maven Pluginなどを組み合わせた時に、argLineが競合するとハマるということで
書いてみました。

知っていれば対応できるのですが、知らないと延々とハマりそうだなということでメモとして。