Maven のテスト失敗メッセージだけ文字化けする(surefire-reports のファイルは無事)

テストの失敗メッセージだけ文字化けする(JDK 18 以降は stdout だけ native encoding のまま)

FIX SUMMARY verified
Applies when
maven@sha256:c3c9d3ac4ce8431a3995c0318b8d390f448e693dd4fabc16e9b68d2e1f3d7b46maven-surefire-plugin 3.5.4 on Eclipse Temurin 21.0.11 (JEP 400 applies to file.encoding only; stdout.encoding / stderr.encoding became standard properties in JDK 19)moving from JDK 17 to JDK 18 or later in a container or CI whose locale is not UTF-8 — the Surefire report file turns UTF-8 while the console keeps writing in the native encoding, so only the failure message on the console is unreadable

Verified: reproduced in maven@sha256:c3c9d3ac4ce8431a3995c0318b8d390f448e693dd4fabc16e9b68d2e1f3d7b46, then the STDOUT_ENCODING_MISMATCH signature was gone after the fix (exit 0).

Maven でテストを走らせると、失敗したテストの説明文だけが読めない文字列になります。アサーションのメッセージに 非 ASCII 文字が含まれていると、その文字がコンソールでは次のように壊れます。

org.opentest4j.AssertionFailedError: ????caf? ==> expected: <1> but was: <2>

同じ実行が生成した target/surefire-reports/dev.errfix.EncodingFailureTest.txt を開くと、そこは壊れていません。

org.opentest4j.AssertionFailedError: 日本語—café ==> expected: <1> but was: <2>

置換の見え方は環境で変わります。上は LANG=C のコンテナで採取したもので、非 ASCII の1文字が ? 1個に 置き換わっています。JEP 400 は、この既定文字コードが Windows では windows-31j(日本語)などロケール由来に なることを挙げています。その環境では置換ではなく、その文字コードのバイト列が書かれます。 本記事のバイト列はすべて LANG=C の実測で、Windows での採取はしていません。

直し方の要点を先に

プロジェクト直下に .mvn/jvm.config を置き、標準ストリームの encoding を UTF-8 に固定します。

-Dstdout.encoding=UTF-8
-Dstderr.encoding=UTF-8

実測では、JDK・locale・テストコードを一切動かさずにこの2行だけを足したところ、コンソールの失敗説明文が レポートファイルと同一のバイト列になりました。テスト自体は意図どおり失敗したままなので Maven の終了コードは 1 で変わりません。変わるのは表示だけです。

CI で .mvn/ を置けない場合は、同じ2つのプロパティを Maven プロセスの JVM 引数として渡します (MAVEN_OPTS="-Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8")。

渡す先は Maven のプロセスです。Surefire の argLine では直りません。 同じプロジェクトで3通り測りました。

渡し方渡る先コンソールの失敗説明文
何も付けない置換
-DargLine="-Dstdout.encoding=UTF-8 …"Surefire が fork するテスト側 JVM置換のまま
MAVEN_OPTS="-Dstdout.encoding=UTF-8 …"Maven のプロセスUTF-8

コンソールに出る失敗の要約行を符号化しているのは Maven のプロセスなので、テスト側の JVM にいくら渡しても 表示は変わりません。

JDK 18 で踏んでいる場合は、プロパティ名が違います。 実測では、JDK 18 に -Dstdout.encoding=UTF-8 を 渡しても出力バイトは変わらず、-Dsun.stdout.encoding=UTF-8 を渡したときだけ UTF-8 になりました。 stdout.encoding が効き始めるのは JDK 19 からです。ただし sun. で始まるほうは JDK 内部のプロパティで、 仕様として保証されたものではありません。JDK 18 に留まっている間の回避として使い、19 以降へ上げたら 標準の stdout.encoding / stderr.encoding に移します。

なぜ JDK 18 以降で出るのか:UTF-8 化されたのはファイル側だけ

JDK 18 の JEP 400 が既定文字コードを UTF-8 にしましたが、標準出力と標準エラーは対象外です。JEP 400 は System.out / System.err を既定文字コードの変更対象から明示的に外し、コンソールの文字コードに従わせています。 同一のプログラムで 日本語—café\n を出力し、プロパティと実バイトを採取した実測です(LANG=C / LC_ALL=C、 Eclipse Temurin の各版)。UTF-8 の期待値は 19 バイト、US-ASCII に置換された結果は 9 バイトになります。

JDKfile.encodingnative.encodingstdout.encodingstdout の実バイト
8ANSI_X3.4-1968(なし)(なし)3f3f3f3f6361663f0a
11ANSI_X3.4-1968(なし)(なし)3f3f3f3f6361663f0a
17ANSI_X3.4-1968ANSI_X3.4-1968(なし)3f3f3f3f6361663f0a
18UTF-8ANSI_X3.4-1968(なし)3f3f3f3f6361663f0a
19UTF-8ANSI_X3.4-1968ANSI_X3.4-19683f3f3f3f6361663f0a
21UTF-8ANSI_X3.4-1968ANSI_X3.4-19683f3f3f3f6361663f0a
25UTF-8ANSI_X3.4-1968ANSI_X3.4-19683f3f3f3f6361663f0a

同じ実行でファイル側がどうなるかは 出力ファイルの文字コードが JDK 18 以降で変わる にあります(18 で UTF-8 に変わるのはそちら側です)。

stdout.encodingfile.encoding ではなく native.encoding と同じ値になっています。 このプロパティが 現れるのは JDK 19 からで、コンソールを持たない実行(CI・パイプ・リダイレクト)では OS のロケール由来の 文字コードが入ります。file.encoding が UTF-8 になった後も、標準出力だけはそちらに従います。

JDK 17 までは、ファイルもコンソールもロケールに従って同じように置換されていました。JDK 18 で初めて、 ファイル側だけが UTF-8 になり、コンソール側が取り残される非対称が生まれます。 同じ表の LANG=C.UTF-8 側では 8 から 25 まで全部が UTF-8 で、置換は起きません。

上の表は出力をパイプへ流した実行(コンソールを持たない側)です。擬似端末を割り当てて System.console() が 存在する状態でも測りました——stdout.encoding の既定は同じく native.encoding の値で、置換も同じように 起きます。そして -Dstdout.encoding=UTF-8 を明示すると、コンソールがある場合でも出力バイトは UTF-8 になります。 明示した指定は、コンソールの有無に関わらず優先されます。 ただし測ったのは Linux の擬似端末までで、 Windows のコンソールでは測っていません。

そのため、JDK 17 から 18 以降へ上げたときに、それまで「全部化けていた」ものが「レポートやログファイルは 直ったのに、コンソールだけ化けたまま」に変わります。

なぜ Surefire のレポートファイルは無事なのか

Surefire が書く target/surefire-reports/*.txt はファイル出力なので、JDK 18 以降の既定 UTF-8 側に乗ります。 実測では、同一実行で採取した2つのバイト列が次のように分かれました。

  • コンソールの行:...4572726f723a203f3f3f3f6361663f203d3d3e...? 4個 + caf + ?
  • レポートの行:...4572726f723a20e697a5e69cace8aa9ee28094636166c3a9203d3d3e...(UTF-8 のまま)

つまり失敗の情報自体は失われていません。コンソールで読めないときは、まず target/surefire-reports/<テストクラスの FQCN>.txt を開けば元の文字列が読めます。

-Dfile.encoding=UTF-8 は JDK 17 まで効いて、18 で効かなくなった

Java の文字化けに対して最初に打たれるのは -Dfile.encoding=UTF-8 です。このオプションは JDK 17 では コンソールにも効きました。JDK 18 以降は効きません。 同じプログラムの標準出力バイトを、オプションごとに 測った結果です(LANG=C)。

JDK何も付けない-Dfile.encoding=UTF-8-Dsun.stdout.encoding=UTF-8-Dstdout.encoding=UTF-8
17置換UTF-8UTF-8置換
18置換置換UTF-8置換
19置換置換UTF-8UTF-8
21置換置換UTF-8UTF-8
25置換置換UTF-8UTF-8

17 まで file.encoding は標準出力の文字コードも決めていました。JEP 400 が既定文字コードを UTF-8 に固定した とき、標準出力はそこから切り離されて native.encoding 側に残ったので、同じオプションが同じ目的に対して 効かなくなります。JDK 17 から上げたときに「以前と同じ対処をしたのに直らない」という形になるのはこのためです。

効くプロパティも版で入れ替わります。18 は sun.stdout.encoding だけ、19 以降は stdout.encoding が使えます。

切り分け

  • LANG=C.UTF-8LANG=ja_JP.UTF-8 のコンテナでは出ないnative.encoding が UTF-8 になるため、 stdout.encoding の既定も UTF-8 になります。ロケールを UTF-8 系に揃えるのも直し方の1つですが、 ロケールは日付・数値・照合順序の書式も動かします。表示だけを直すなら上のプロパティ指定のほうが影響範囲が狭くなります。
  • ファイルに書いた日本語のほうが化けるようになった:症状が逆向きなら、踏んでいるのは同じ JEP 400 の もう一方の側です。charset を指定しない FileWriter の出力バイトが JDK 18 で変わる境界は charset 未指定の FileWriter が JDK 18 以降で別のバイトを書く で扱っています。
  • JDK を 17 に戻したらコンソールが直った:起動オプションに -Dfile.encoding=UTF-8 が入っている構成です。 上の表のとおり、そのオプションは 17 ではコンソールにも効き、18 以降は効きません。17 に戻すと直るのは 版が正しいからではなく、そのオプションが標準出力に届く版に戻ったからです。18 以降のまま直すなら stdout.encoding 側を指定します。
  • オプションを何も付けずに 17 へ戻したが、コンソールは化けたまま:ロケールが UTF-8 系でない環境では、 17 はファイルもコンソールも同じ文字コードで書きます。非対称が消えるだけで、置換自体は 17 でも起きています (上の表の「何も付けない」列)。
  • argLine に同じプロパティを書いたのに直らない:上の表のとおり、テスト側の JVM に渡しても失敗の要約行は 変わりません。.mvn/jvm.configMAVEN_OPTS に移します。テスト内の System.out.println の出力も Surefire が Maven のコンソールへ中継しているので、そちら側の指定が要ります(テスト内出力そのものは 本記事では実測していません)。
  • 化け方が ? ではなく、読めない別の文字の並びになる:ロケールが US-ASCII なら表現できない文字が ? に置換されますが、MS932 など別の文字コードのロケールでは、置換ではなくその文字コードのバイト列が 書かれ、端末がそれを別の文字として表示します。検証したのは LANG=C? 置換ですが、判定はバイト列の 一致で行っているので、表示上の見え方には依存しません。
  • Robolectric のテストを Java 21 へ上げた直後に出はじめた:その変更で JEP 400 の境界(18)を 越えています。経緯は Robolectric の SDK 36 が Java 21 を要求する にあります。上げること自体は正しく、ここで直すのは表示側だけです。

検証環境

  • image: maven:3.9.12-eclipse-temurin-21(digest 固定 sha256:c3c9d3ac...)/Java 21.0.11 / Maven Surefire Plugin 3.5.4 / LANG=CLC_ALL=C
  • 再現:JUnit 5 の assertEquals(1, 2, "日本語—café") を失敗させ、Maven のコンソール出力と target/surefire-reports/dev.errfix.EncodingFailureTest.txt から同じ AssertionFailedError: の行を抜き出して バイト比較。レポート側は UTF-8、コンソール側はそれと不一致で終了コード 1
  • 修正:.mvn/jvm.config-Dstdout.encoding=UTF-8-Dstderr.encoding=UTF-8 を追加。JDK・locale・ テストコード・pom はすべて同一のまま、両者のバイト列が一致する。 このとき mvn test 自体は依然として終了コード 1(テストは意図どおり失敗させたまま)で、 終了コード 0 になるのはバイト一致を判定する検証スクリプトのほう。ハーネスの report.json に出る fix.exitCode: 0 はその判定スクリプトの値であって、Maven の値ではない
  • 版とロケールの表は Eclipse Temurin の 8 / 11 / 17 / 18 / 21 / 25 を LANG=CLANG=C.UTF-8 で それぞれ実行し、java -XshowSettings:properties のプロパティと実際の出力バイトを採取したもの
  • オプション別の表-Dfile.encoding / -Dsun.stdout.encoding / -Dstdout.encoding)は、Eclipse Temurin の 17.0.19 / 18.0.2.1 / 19.0.2 / 21.0.11 / 25.0.3 に同じプログラムを与え、標準出力のバイト列を採取したもの。 どのプロパティがどの版で効くかは仕様からの推定ではなく、この実測による

再現から修正までは errfix の検証ハーネスが機械的に確認しています。判定は表示された文字ではなくバイト列の 一致で行っているため、ターミナルの表示設定には依存しません。JEP 400 が System.out / System.err を 既定文字コードの変更対象から外していることは JEP 400 本文で確認しています。

検証(machine-verified)

この修正は maven@sha256:c3c9d3ac4ce8431a3995c0318b8d390f448e693dd4fabc16e9b68d2e1f3d7b46 のバージョン固定コンテナ内で再現し、修正後に STDOUT_ENCODING_MISMATCH のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs jvm/stdout-encoding-mojibake
● reproduce STDOUT_ENCODING_MISMATCH present ✓
● apply fix exit 0
● re-run STDOUT_ENCODING_MISMATCH gone ✓
PASS verified · maven@sha256:c3c9d3ac4ce8431a3995c0318b8d390f448e693dd4fabc16e9b68d2e1f3d7b46 · signature gone

確認したのは上のイメージの中だけです。別の環境で直らなかった、記述が違う、という場合は 報告してください(対象と検証イメージは件名・本文に入ります)。