Gradle のマルチモジュールプロジェクトで JaCoCo の結果を集計する
Java / Kotlin のコードカバレッジツールとして JaCoCo を使いたい。 Gradle のマルチモジュールプロジェクトでの JaCoCo の導入について記す。
(この図は JaCoCo によるコードカバレッジの集計結果の履歴を Codecov で表示した例。)
JaCoCo について
JaCoCo は、JVM 言語のコードカバレッジツールである。 Java Agent によるオンザフライ方式のバイトコード instrumentation によるカバレッジ計測が可能である。
以前、Kotlin のコードカバレッジツールについて書いたので、こちらも参考にどうぞ。
Gradle プロジェクトで JaCoCo を使ったカバレッジ計測を行う
Gradle の JaCoCo プラグインを使うことで、Gradle のプロジェクトで JaCoCo を使ったコードカバレッジ計測を手軽に行うことができる。 ちなみに Gradle 4.9 の段階ではこのプラグインは incubating である。
Junit 5 のテスト実行時にカバレッジを計測する
各モジュールのテスト実行時にコードカバレッジを計測するだけなら非常に簡単で、単に JaCoCo プラグインを適用するだけで良い。
While all tasks of type
The JaCoCo Plugin - Gradle User ManualTest
are automatically enhanced to provide coverage information when thejava
plugin has been applied, any task that implementsJavaForkOptions
can be enhanced by the JaCoCo plugin. That is, any task that forks Java processes can be used to generate coverage information.
バージョンを表す変数の定義や repositories
の定義などは省略しているが、おおよそ以下のようなビルドスクリプトで JUnit 5 でのテスト実行時に JaCoCo によるコードカバレッジの計測がなされる。
apply plugin: 'kotlin' apply plugin: 'jacoco' test { useJUnitPlatform() } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" testCompileOnly("org.junit.jupiter:junit-jupiter-api:$junit_version") testRuntime("org.junit.jupiter:junit-jupiter-engine:$junit_version") }
デフォルトでは、カバレッジの計測結果は build/jacoco/test.exec というファイルに出力されるようである。
複数モジュールのコードカバレッジの集計
難しいのはここからで、複数モジュールでのコードカバレッジをどう集計するかで私は結構悩んだ。 JaCoCo の結果集計のためのタスクとしては JacocoMerge
というのがあるのだが、これをどう定義するのかが難しい。 (どのサブモジュールのカバレッジを集計するのかを明示的に指定するのであればそれほど大変ではないが、自動で全サブモジュールのカバレッジを集計するためのタスク定義をするのが難しい。)
結論としては、ルートプロジェクトに以下のようなタスク定義を行うことで対応した。 (ルートプロジェクトにも jacoco
プラグインを適用している。)
task jacocoMerge(type: JacocoMerge) { // ルートプロジェクトの評価時にはまだサブプロジェクトは存在しないので、 // 各プロジェクトの評価完了時に中の処理が走るように。 gradle.afterProject { p, state -> // ルートプロジェクトと `jacoco` プラグインが適用されていないプロジェクトは除く。 if (p.rootProject != p && p.plugins.hasPlugin('jacoco')) { executionData p.tasks.test.jacoco.destinationFile dependsOn(p.tasks.test) } } }
大雑把に説明すると、jacocoMerge
タスクの executionData
に各サブモジュールのテスト実行時のカバレッジ計測結果を渡していき、かつ、jacocoMerge
タスクの依存に各サブモジュールの test
タスクを追加する、ということをしている。
gradle.afterProject
を使っている理由や if
文を使っている理由はコメントに書いてあるとおりである。
Gradle のプロジェクトの評価順については下記ページを参照されたい。
集計結果のコードカバレッジのレポーティング
さらにレポーティングのタスクは、ルートプロジェクトに下記のように定義することで行える。 java
プラグインが適用されている全サブモジュールのソースファイルをコードカバレッジのレポート対象に含める場合の例である。
task jacocoMergedReport(type: JacocoReport, dependsOn: [tasks.jacocoMerge]) { executionData jacocoMerge.destinationFile sourceDirectories = files() classDirectories = files() gradle.afterProject { p, state -> if (p.rootProject != p && p.plugins.hasPlugin('java')) { sourceDirectories = sourceDirectories + files([p.sourceSets.main.allJava.srcDirs]) classDirectories = classDirectories + p.sourceSets.main.output } } reports { xml.enabled = true xml.destination file("${buildDir}/reports/jacoco/report.xml") html.destination file("${buildDir}/reports/jacoco/html") } }
本当は JacocoReport#sourceSets
を使いたかったが、内部で gradle.afterProject
を使われていて期待する挙動にならなかったので諦めた。
ちなみに上の xml.destination
の値は Codecov が期待するレポートファイルの位置である。
参考
- Gradleのマルチプロジェクト構成でJUnitやJacocoのレポートを集計する : プロジェクトの評価順を気にしなくてよければ
gradle.afterProject
を使わずにこういう感じで書ける。 - Aggregated Jacoco reports in a multi-project Gradle build · GitHub : こちらもプロジェクトの評価順を気にしなくてよいように書いた場合の例。
Windows 10 で Git 環境を整える
自分用メモ。 昔は GitHub Desktop をインストールすれば Git Shell も使えるようになって PowerShell で快適 Git 生活が送れていたのだけど、最近 GitHub Desktop をインストールしてみたところ、どうやら Git Shell は同梱されないようになってしまったっぽい。
今ならこうすると良さそう、というメモ。
Git 環境を整える
GitHub Desktop インストール
人によっては全然いらないと思うけど、あったらあったで便利なので。 (追記 : 2019 年 5 月現在、個人的にこれはなくていいかなーと思いつつある。 特に使わないので。)
GitHub Desktop | Simple collaboration from your desktop
起動したら GitHub にログイン。 それで Git の設定でユーザー名やメールアドレスが自動的に設定されるので便利。
Git for Windows をインストール
GitHub Desktop にも git
コマンドは同梱されているけど、GitHub Desktop とは独立してバージョンアップなどをできるように Git for Windows をインストールしておく。
Git - Downloading Package (Git 本家サイトの Git for Windows のダウンロードページ)
Git for Windows (こっちは Git for Windows のサイト)
インストール途中の選択肢で、push 時や pull 時の改行コードの変換をどうするか聞いてくれる。 が、as-is を選んでも改行コードが変換されないようにならなかったことがある。 (後でやったときはちゃんと設定された。) また、GitHub Desktop をインストールしない場合にはユーザー名やメールアドレスを設定する必要がある。
# 設定が必要なら user.name=.... user.email=.... # core.autocrlf の設定がなされていないならば git config --global core.autocrlf false
posh-git をインストール
PowerShell で Git が便利になる。 タブでサブコマンドやブランチ名の補完をしてくれたり、プロンプトに現在のブランチを表示してくれたりする。 (昔の GitHub Desktop に同梱されていた Git Shell で PowerShell を使う場合には自動で有効になってた。)
posh-git by dahlbyk
Git - PowershellでGitを使う
書かれてる通りにインストール。
必要な設定
GitHub に SSH 鍵登録
Generating a new SSH key and adding it to the ssh-agent - GitHub Help
Adding a new SSH key to your GitHub account - GitHub Help
文字化け (?) 対応
初期状態だと git log
などで日本語の表示がおかしくなる。 文字化けというか、バイト値が 16 進数表記で連なる感じ。
git config --global core.pager "LESSCHARSET=utf-8 less"
という感じで、config の core.pager
を 「LESSCHARSET=utf-8 less」 に設定すれば良さそう。
~\Documents\test [master]> git log --graph --decorate * commit a5e5b4d21432f3fcba4ffbf1dd0d4bc598c4b051 (HEAD -> master) Author: Nobuoka Yu <nobuoka@vividcode.info> Date: Wed Aug 8 23:33:26 2018 +0900 <E3><81><93><E3><82><93><E3><81><AB><E3><81><A1><E3><81><AF> ~\Documents\test [master]> git config --global core.pager "LESSCHARSET=utf-8 less" ~\Documents\test [master]> git log --graph --decorate * commit a5e5b4d21432f3fcba4ffbf1dd0d4bc598c4b051 (HEAD -> master) Author: Nobuoka Yu <nobuoka@vividcode.info> Date: Wed Aug 8 23:33:26 2018 +0900 こんにちは
読んだ : 入門 Kubernetes
Kubernetes に興味を持ちつつ何も手を付けていなかったのだけど、会社に置かれてたのでまずは本を読んだ。
- 作者: Kelsey Hightower,Brendan Burns,Joe Beda,松浦隼人
- 出版社/メーカー: オライリージャパン
- 発売日: 2018/03/22
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
Kubernetes を何故使うのか、というところから始まり、Kubernetes における各種オブジェクトの大まかな概念の説明から使い方まで一通り解説されている。
読書メモ
Kubernetes を何故使うのか、という導入では以下のような内容が書かれている。
- ベロシティ : 宣言的設定でイミュータブルなコンテナを扱うことで自己回復しやすくスケールもしやすくなっている。
- サービスとチームのスケール : マイクロサービス化による開発チームのスケールや、アプリケーションやクラスタのスケールをしやすくする、など。
- インフラの抽象化・アプリケーション実行のためのプラットフォーム : 『プロダクションレディマイクロサービス』 で言われていた 4 層モデルの 「アプリケーションプラットフォーム」 に該当する。 アプリケーションプラットフォーム側がアプリケーション開発側に対して API を提供することで、アプリケーションプラットフォーム側とアプリケーション側の責務を切り離す。
- インフラの効率性。 アプリケーションのコンテナを同じ物理マシン (または仮想マシン) 上に同居させることができる。
便利。
プロダクションレディマイクロサービス ―運用に強い本番対応システムの実装と標準化
- 作者: Susan J. Fowler,佐藤直生,長尾高弘
- 出版社/メーカー: オライリージャパン
- 発売日: 2017/09/13
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
それから、「Pod」 や 「ReplicaSet」、「DaemonSet」、「Deployment」、「Job」、「Service」、「Namespace」 といった概念について、本を通して解説される。 下記に今のところの自分の理解を書いておく。
- Pod : コンテナの集まり。 Web サービスのアプリケーションの 1 インスタンスなどが Pod になる。
- ReplicaSet : 特定種類の Pod の集まり。 Web サービスを複数インスタンスで動かす際などには ReplicaSet で複数 Pod を動かす、という形になる。
- Deployment : ReplicaSet の履歴を保持したオブジェクト。 Web サービスに変更を加える際には、新バージョンに対応する ReplicaSet を Deployment に追加してアップデートを行う、という形。
- Service : Kubernetes クラスタ上のアプリケーション同士の通信や、外部への公開のために使われる。
良かった
Kubernetes に入門するには良い本だった。 著者は Kubernetes の開発者なので、どういう思想でこうなっているのか、というのもところどころに書かれていて 「なるほどねー」 と言いながら読める。 本書をざーっと読んで概念を理解したら、あとは実際に Kubernetes を触って動かしていくと良さそう。 今なら Docker for Windows で Kubernetes クラスタを簡単に用意できたりするので、軽く試すだけなら簡単にできる。
Kubernetes やっていこう。
Firefox の JS コンテキストのロケール指定方法
背景・目的
WebDriver 経由で geckodriver + Firefox を操作して JS の自動テストを行いたい。 その時、JS にロケール依存の処理が含まれていれば、テスト実行時にも Firefox の JS コンテキストのロケール指定を行いたい。
3 行まとめ
詳しい話
ECMAScript と国際化
ECMAScript における国際化のための仕様として ECMA-402 (ECMAScript Internationalization API Specification) というものがある。
この仕様により、例えば Date.prototype.toLocaleString
メソッドなどが定義される。 このメソッドの呼び出し時にロケールを明示的に指定しなかった場合、使用されるロケールは DefaultLocale
抽象操作によって決まる。 DefaultLocale
は実装依存である。
Firefox での DefaultLocale
(JS コンテキストのデフォルトロケール)
Firefox における ECMA-402 の DefaultLocale
の実装は、Firefox のロケールを返すようになっている模様。 ちゃんとしたドキュメントは見当たらなかったが、Bugzilla で教えてもらった。
JS context locale is tied to Firefox locale, not requested locale.
1475876 - intl.locale.requested doesn't affect to JS locale unless Language Pack is installed
Firefox のロケールの決まり方
で、Firefox のロケールの決まり方であるが、RFC 5656 をベースに、available locales と requested locales から決まるらしい。
Due to the imperfections in data matching, all operations on locales should always use a language negotiation algorithm to resolve the best available set of locales, based on the list of all available locales and an ordered list of requested locales.
Such algorithms may vary in sophistication and number of strategies. Mozilla’s solution is based on modified logic from RFC 5656.
Locale management — Mozilla Source Tree Docs 63.0a1 documentation
Available locales は、Firefox のパッケージに含まれているロケール (packaged locales) と、拡張機能としてインストールされた言語パックのロケール。
In Gecko, available locales come from the Packaged Locales and the installed language packs. Language packs are a variant of web extensions providing just localized resources for one or more languages.
Locale management — Mozilla Source Tree Docs 63.0a1 documentation
デスクトップ版の Firefox では、Packaged locales は普通は 1 つのみ。 Android 版の方は 100 ぐらいのロケールを含むらしい。
When the Gecko application is being packaged it bundles a selection of locale resources to be available within it. At the moment, for example, most Firefox for Android builds come with almost 100 locales packaged into it, while Desktop Firefox comes with usually just one packaged locale.
Locale management — Mozilla Source Tree Docs 63.0a1 documentation
Requested locales は次のとおりで、intl.locale.requested
pref に保持される。
After the sanitization, the value will be stored in a pref
intl.locale.requested
. The pref usually will store a comma separated list of valid BCP47 locale codes, but it can also have two special meanings:
- If the pref is not set at all, Gecko will use the default locale as the requested one.
- If the pref is set to an empty string, Gecko will look into OS app locales as the requested.
The former is the current default setting for Firefox Desktop, and the latter is the default setting for Firefox for Android.
Locale management — Mozilla Source Tree Docs 63.0a1 documentation
なので、基本的には使いたいロケールが Firefox のパッケージに含まれるロケールか言語パックとしてインストールされたロケールであれば、intl.locale.requested
にそのロケールを指定してやれば JS コンテキストのロケールがそのロケールになる。
en-US ロケールは特殊なロケール
日本語ロケールのデスクトップ版 Firefox で intl.locale.requested=en-US
とした場合、英語の言語パックをインストールしていないと en-US ロケールにはならなさそう (available locales に含まれてないはずなので) なのだけど、実際には JS コンテキストのロケールは en-US になっているような挙動になった。
なんでだろー、と思ったのだけど、どうやら last fallback locale という特殊な扱いで en-US が使われてるらしい。
Gecko also support a notion of last fallback locale, which is currently hardcoded to “en-US”, and is the very final locale to try in case nothing else (including the default locale) works.
Locale management — Mozilla Source Tree Docs 63.0a1 documentation
悩み
というわけで、複数のロケールを切り替えて JS のテストをしたい場合は、言語パックをインストールすることになるっぽい。 もしくはロケールごとに Firefox のバイナリを用意するか。
言語パックのインストールがコマンドライン上で簡単にできればいいのだけど、ちょっと調べた感じでは大変そうで、どうしたものかなーと思っている。 簡単にできる方法があれば教えてください!
参考
- Locale management — Mozilla Source Tree Docs 63.0a1 documentation : 詳しいことは全部ここに書かれている。
- 言語パックで Firefox のインターフェイスを他の言語にする | Firefox ヘルプ
- Firefoxの言語設定 - Hideki Saito Wiki Japanese : Firefox 59 から UI のロケールの指定方法が変わったとのこと。
Acknowledgement
この記事の内容は、株式会社 OND の仕事の一環として調べたものです。
MySQL (InnoDB) とトランザクション分離レベル・ロック
(注記) 去年書いて下書き状態になってた記事をそのまま公開した。 MySQL 8.0 公開前に書いた内容なので MySQL 5.7 について言及しているが、多分 MySQL 8.0 でも基本的には変わらない気がする。
背景・目的
DB 上に指定の ID の行が存在していれば UPDATE
して、存在していなければ INSERT
したい、ということは web アプリケーションを書いているとよくあること。 SQL:2003 標準には MERGE
が導入されていて、Oracle Database なんかだと MERGE
を使用できるようだが、MySQL では使えない (バージョン 5.7 時点)。 (代わりに INSERT ... ON DUPLICATE KEY UPDATE
がある。)
また、複数のテーブルで 1 つの集約 *1 を表現していて、集約の更新時に不整合が起きないようにロックをかけたいということも多い。
といったあたりで、MySQL (InnoDB) における参照や挿入、更新とトランザクション分離レベルやロックについて調べたのでまとめておく。 (初心者が調べてみたぐらいの内容なので、指摘やコメント等頂けると嬉しいです!)
調べた結果どうしたらいいかの個人的見解
いろいろ調べた結論としては、個人的には以下のような感じがいいのかなーと思っている。
- 参照系ではトランザクション分離レベルを REPEATABLE READ にする。 → 参照だけであれば一貫性のある読み取りができる。
- 単一の集約に対する更新系 (新しい集約の新規作成や既存の集約の更新; INSERT or UPDATE) では、トランザクション分離レベルを READ COMMITTED にして *2、集約ルート (に対応するテーブルの行) の存在確認を
SELECT FOR UPDATE
で行う。- 存在すればそのままロックを保持して更新処理を行う。 → 集約ルートに対する排他ロックを獲得しているので、他のトランザクションによる更新と衝突しない。
- 存在しなければ
INSERT
する。 Duplicate error になったら (すなわち同時に実行されていた他のトランザクションによる挿入が行われた場合は) トランザクション自体を再開してやり直すか、エラーということでそのまま処理を終了する。 → こちらも挿入に成功すれば排他ロックを獲得するので、そのまま処理を進めて良い。 Duplicate error になった場合には対象行の共有ロックを獲得してしまうため、やり直す場合にはトランザクション自体をやり直さなければならない *3。 - 実装依存でも良ければ
INSERT ... ON DUPLICATE KEY UPDATE
で良い場面が多そう。 - パフォーマンスを気にしなくてもいいならトランザクション分離レベルを SERIALIZABLE にするとかも有りかもしれない。
- (だいたい songmu さんが書いてるとおりの結論ではある。)
参考ページ
- doc/innodb.md at master · ichirin2501/doc · GitHub : MySQL のロックの挙動がいろいろ書かれている。 非常に参考になる。
- 漢(オトコ)のコンピュータ道: InnoDBのREPEATABLE READにおけるLocking Readについての注意点 : REPEATABLE READ で参照系が一貫性のある読み取りになる話。 (あるいは Locking Read は一貫性のある読み取りにはならない話。)
- 世界の何処かで MySQL(InnoDB)の REPEATABLE READ に嵌る人を1人でも減らすために - KAYAC engineers' blog : こちらも REPEATABLE READ における Locking Read が一貫性のある読み取りにはならない話。
- kamipo TRADITIONALでは防げないINSERT IGNOREという名の化け物 | おそらくはそれさえも平凡な日々 :
INSERT IGNORE
は良くないという話と、INSERT or UPDATE の方法について。 ちなみに 『DELETE
してINSERT
』 は、REPEATABLE READ で対象の行が存在しない場合 (DELETE
で 1 件も削除されないとき) に、デッドロックが発生しうる *4 気がする。
MySQL (InnoDB) のロックについて
以下、自分が知らなくて調べた話をだらだら書いておく。
ロックの種類
MySQL 5.7 の英語ドキュメントにまとまっている。
インデックスに対するロック
InnoDB では、インデックスに対するロックによって行レベルロックを実現している *5。 MySQL の公式ドキュメントやかみぽさんの解説がわかりやすい。
- レコードロック (Record Locks) : インデックスレコードに対するロック。
- ギャップロック (Gap Locks) : インデックスレコード間の隙間 (gap) に対するロック。 隙間は、複数のインデックス値にかかることもあるし、単一のインデックス値や空のところにさえもかかりうる *6。 ギャップロックは、他のトランザクションが同じ隙間のギャップロックを獲得することは防がない。 対象の隙間へのレコードの挿入のみが防がれる。 (つまり、共有ギャップロック (gap S-lock) も排他ギャップロック (gap X-lock) も効果は同じ。)
- トランザクション分離レベルを
READ COMMITTED
にすると、ギャップロックは無効化される。
- トランザクション分離レベルを
- ネクストキーロック (Next-Key Locks) : レコードロックと、そのレコードの前の隙間に対するギャップロックの組み合わせ。
Insert Intention Locks
INSERT
操作時に、行の挿入前に獲得されるギャップロックの一種として、挿入インテンションロック (Insert Intention Locks) がある。
『MySQLでINSERTのデッドロックに嵌る人を1人でも減らすために - ichirin2501's diary』 におけるデッドロックは、別のトランザクションが行を挿入して獲得した排他レコードロックと、挿入しようとしている行のための挿入インテンションロックの衝突が 2 箇所で起こってしまう、ということなのかな? と思ったりしたけど、InnoDB monitor の出力を見た感じでは共有レコードロック待ちになってたので挿入インテンションロック待ちとは違ってそうだった。 挿入インテンションロックは純粋にギャップロックとのみ衝突するもので、レコードロックとは衝突しない模様。
ギャップロックとファントム行、あるいはデッドロック
ファントムリードを防ぐために
ギャップロックと挿入インテンションロックのデッドロック : mysqlのネクストキーロックと挿入インテンションギャップロックのデッドロックを確認する | ++頭道++
mysqlのネクストキーロックと挿入インテンションギャップロックのデッドロックを確認する | ++頭道++ → この人の解釈では DELETE
により指定の ID に対するロックができる (存在しないレコードに対するロックという解釈をしている) となっているが、私の手元の MySQL 5.7.20 で試した限りでは、存在しない ID を指定しての DELETE
でギャップロックが獲得されていた。
下書きはココで終わっていた
多分他にも書きたいことがありそうだったけどもはや何も覚えてないのでこのまま公開する。
*1:DDD でいうところの集約。 Aggregates。
*2:この理由は主にギャップロック無効化のため。 ギャップロック有効だとデッドロックしうる; REPEATABLE READ でもギャップロック無効化できるしそうする方がいいのかもしれない
*3:そうしなければ、複数トランザクションで同一行に対する duplicate error が起こった場合にトランザクションをやり直さないと排他ロックの獲得ができず、デッドロックになる
*4:複数のトランザクションが DELETE するとそれぞれのトランザクションがギャップロックを獲得し、その後の INSERT がそれぞれ待ち状態になってしまう。
*5:という理解であってるよね?
*6:空のインデックスでも全体に対するギャップロックがあり得る、ってことかな?