冥冥乃志

ソフトウェア開発会社でチームマネージャをしているエンジニアの雑記。アウトプットは少なめです。

follow us in feedly

ScalaプロジェクトをCircleCIでビルドしてカバレッジをとってみた

カバレッジ100%とか馬鹿なことは言いませんが、テスト書いてなさすぎるとかないよね、という指標にカバレッジを使う価値は十分にあります。テスト自体は流せているので、せっかくなのでカバレッジも取得しましょうか。自分のプロダクトで取るの慣れておきたいところですし。

カバレッジの計測

先日の続き。下記エントリを引き続き参考にします。

www.todesking.com

sbt-scoverage プラグイン

Scalaプロジェクトでカバレッジを計測するためのプラグインです。scoverageというカバレッジ計測ツールをsbtプラグインとして利用可能になるようにします。上記エントリは参照しているプラグインのバージョンがちょっと古いので公式を当たってみます。

github.com

上記エントリの時はカバレッジ用にデータを埋め込む形でコンパイルしないといけなかったみたいですが、今はそうでもないみたいですね。リゾルバを追加する必要もないみたいです。プラグイン追加後に sbt clean coverage test を実行します。

$ sbt clean coverage test
[info] Loading project definition from /Users/shinsuke-abe/IdeaProjects/Thoth/project
[info] Set current project to Thoth (in build file:/Users/shinsuke-abe/IdeaProjects/Thoth/)
[success] Total time: 0 s, completed 2015/10/19 13:35:49
[info] Set current project to Thoth (in build file:/Users/shinsuke-abe/IdeaProjects/Thoth/)
[info] Updating {file:/Users/shinsuke-abe/IdeaProjects/Thoth/}thoth...
[info] Resolving jline#jline;2.12.1 ...
[info] Done updating.
[info] Compiling 9 Scala sources to /Users/shinsuke-abe/IdeaProjects/Thoth/target/scala-2.11/classes...
[info] [info] Cleaning datadir [/Users/shinsuke-abe/IdeaProjects/Thoth/target/scala-2.11/scoverage-data]
[info] [info] Beginning coverage instrumentation
[info] [info] Instrumentation completed [253 statements]
[info] [info] Wrote instrumentation file [/Users/shinsuke-abe/IdeaProjects/Thoth/target/scala-2.11/scoverage-data/scoverage.coverage.xml]
[info] [info] Will write measurement data to [/Users/shinsuke-abe/IdeaProjects/Thoth/target/scala-2.11/scoverage-data]
〜〜中略〜〜
[info] Reading scoverage instrumentation [/Users/shinsuke-abe/IdeaProjects/Thoth/target/scala-2.11/scoverage-data/scoverage.coverage.xml]
[info] Reading scoverage measurements...
[info] Generating scoverage reports...
[info] Written Cobertura report [/Users/shinsuke-abe/IdeaProjects/Thoth/target/scala-2.11/coverage-report/cobertura.xml]
[info] Written XML coverage report [/Users/shinsuke-abe/IdeaProjects/Thoth/target/scala-2.11/scoverage-report/scoverage.xml]
[info] Written HTML coverage report [/Users/shinsuke-abe/IdeaProjects/Thoth/target/scala-2.11/scoverage-report/index.html]
[info] Coverage reports completed
[info] All done. Coverage was [71.54%]
[info] Passed: Total 32, Failed 0, Errors 0, Passed 32
[success] Total time: 32 s, completed 2015/10/19 13:36:21

テストも通りましたし、標準出力にカバレッジ計測結果も出ています。レポートもデフォルトで複数の形式で出してくれています。ほぼ個別の設定なしでこれだけ出してくれれば十分ですね。71.54%はちょっと予想外でしたが、内容見てまあ納得。。。

次はこれをCircleCIで計測して出力したArtifactにレポートします。

general:
  artifacts:
    - "target/scala-2.11/coverage-report"

dependencies:
  cache_directories:
    - graphviz-2.38.0
    - "~/.ivy2"
    - "~/.sbt"
  pre:
    - wget -q https://dl.bintray.com/sbt/debian/sbt-0.13.8.deb
    - sudo dpkg -i sbt-0.13.8.deb
    - wget -O graphviz.tar.gz --quiet http://www.graphviz.org/pub/graphviz/ARCHIVE/graphviz-2.38.0.tar.gz
    - tar -zxf graphviz.tar.gz
    - graphviz-2.38.0/configure --silent
    - make --silent --ignore-errors && make --silent --ignore-errors install > /dev/null
    - echo 'which dot && version:'
    - which dot
    - dot -V
    - sudo apt-get -y install pandoc

test:
  override:
    - "sbt clean coverage test"

カバレッジレポートが出力されるディレクトリを general:artifacts に指定して、テスト実行コマンドを、 sbt-scoverage で指定されているコマンドに上書きします。プッシュしたあとにArtifactを見てみましょうか。

f:id:mao_instantlife:20151019162321p:plain

テスト時の標準出力にhtml形式の出力があるのにArtifactsには covertura.xml しかいない。。。どゆこと?まあ、どうせCoverallsに送るつもりだし、人間が生でみたければローカルで出せばいいので放っておきます。

sbt-coveralls プラグイン

これらの計測結果、テストを流した時だけではなくブランチやコミットごとの変化なんかを見たいですよね。カバレッジ下がってきつつあるから気をつけよう、とか。CircleCIのArtifactをテストごとに見てもいいんでしょうけど、どうせならそれに特化したサービスがあれば、ということでCoverallsというサービスを使ってみます。

coveralls.io

これは、GithubやBitBucketのリポジトリのコミットに対してカバレッジの計測結果を時系列、ブランチ別で整理してくれるサービスです。パブリックリポジトリだったら無料で使うことができます。時系列でまとめてくれるだけでなく、プルリクエスト作成時のカバレッジ閾値なんかも設定可能です *1

というわけでこれらの計測結果をCoverallsに送るための設定をします。

github.com

環境変数 COVERALLS_REPO_TOKEN をCircleCIのプロジェクトにセットしましょう。トークンはCoverallsでプロジェクトの設定を行ったあとにプロジェクトのトップに表示されます。下図の service_name の下に repo_token という設定があってそこにしれっと書かれています。マニュアルっぽくしれっと書いてあるので最初完全にスルーしてました *2

f:id:mao_instantlife:20151019162408p:plain

circle.yml を以下のように追記します。

general:
  artifacts:
    - "target/scala-2.11/coverage-report"

dependencies:
  cache_directories:
    - graphviz-2.38.0
    - "~/.ivy2"
    - "~/.sbt"
  pre:
    - wget -q https://dl.bintray.com/sbt/debian/sbt-0.13.8.deb
    - sudo dpkg -i sbt-0.13.8.deb
    - wget -O graphviz.tar.gz --quiet http://www.graphviz.org/pub/graphviz/ARCHIVE/graphviz-2.38.0.tar.gz
    - tar -zxf graphviz.tar.gz
    - graphviz-2.38.0/configure --silent
    - make --silent --ignore-errors && make --silent --ignore-errors install > /dev/null
    - echo 'which dot && version:'
    - which dot
    - dot -V
    - sudo apt-get -y install pandoc

test:
  override:
    - "sbt clean coverage test"
  post:
    - "sbt coveralls"

テスト実行後に sbt coveralls を実行するように指定するだけです。のはずなんですが、こけました。

[info] Loading project definition from /home/ubuntu/Thoth/project
[info] Set current project to Thoth (in build file:/home/ubuntu/Thoth/)
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
[info] Repository = ./.git
java.io.FileNotFoundException: /home/ubuntu/Thoth/markdown/ThothCustomMarkdownParser.scala (No such file or directory)
        at java.io.FileInputStream.open(Native Method)
        at java.io.FileInputStream.<init>(FileInputStream.java:146)
        at scala.io.Source$.fromFile(Source.scala:90)
        at scala.io.Source$.fromFile(Source.scala:75)
        at scala.io.Source$.fromFile(Source.scala:53)
        at org.scoverage.coveralls.CoberturaReader.reportForSource(CoberturaReader.scala:34)
        at org.scoverage.coveralls.CoverallsPlugin$$anonfun$doCoveralls$5.apply(CoverallsPlugin.scala:91)
        at org.scoverage.coveralls.CoverallsPlugin$$anonfun$doCoveralls$5.apply(CoverallsPlugin.scala:90)
        at scala.collection.immutable.HashSet$HashSet1.foreach(HashSet.scala:153)
        at scala.collection.immutable.HashSet$HashTrieSet.foreach(HashSet.scala:306)
        at org.scoverage.coveralls.CoverallsPlugin$.doCoveralls(CoverallsPlugin.scala:90)
        at org.scoverage.coveralls.CoverallsPlugin$$anonfun$coverallsCommand$1.apply(CoverallsPlugin.scala:28)
        at org.scoverage.coveralls.CoverallsPlugin$$anonfun$coverallsCommand$1.apply(CoverallsPlugin.scala:28)
        at sbt.Command$$anonfun$command$1$$anonfun$apply$1.apply(Command.scala:29)
        at sbt.Command$$anonfun$command$1$$anonfun$apply$1.apply(Command.scala:29)
        at sbt.Command$.process(Command.scala:92)
        at sbt.MainLoop$$anonfun$1$$anonfun$apply$1.apply(MainLoop.scala:98)
        at sbt.MainLoop$$anonfun$1$$anonfun$apply$1.apply(MainLoop.scala:98)
        at sbt.State$$anon$1.process(State.scala:184)
        at sbt.MainLoop$$anonfun$1.apply(MainLoop.scala:98)
        at sbt.MainLoop$$anonfun$1.apply(MainLoop.scala:98)
        at sbt.ErrorHandling$.wideConvert(ErrorHandling.scala:17)
        at sbt.MainLoop$.next(MainLoop.scala:98)
        at sbt.MainLoop$.run(MainLoop.scala:91)
        at sbt.MainLoop$$anonfun$runWithNewLog$1.apply(MainLoop.scala:70)
        at sbt.MainLoop$$anonfun$runWithNewLog$1.apply(MainLoop.scala:65)
        at sbt.Using.apply(Using.scala:24)
        at sbt.MainLoop$.runWithNewLog(MainLoop.scala:65)
        at sbt.MainLoop$.runAndClearLast(MainLoop.scala:48)
        at sbt.MainLoop$.runLoggedLoop(MainLoop.scala:32)
        at sbt.MainLoop$.runLogged(MainLoop.scala:24)
        at sbt.StandardMain$.runManaged(Main.scala:53)
        at sbt.xMain.run(Main.scala:28)
        at xsbt.boot.Launch$$anonfun$run$1.apply(Launch.scala:109)
        at xsbt.boot.Launch$.withContextLoader(Launch.scala:128)
        at xsbt.boot.Launch$.run(Launch.scala:109)
        at xsbt.boot.Launch$$anonfun$apply$1.apply(Launch.scala:35)
        at xsbt.boot.Launch$.launch(Launch.scala:117)
        at xsbt.boot.Launch$.apply(Launch.scala:18)
        at xsbt.boot.Boot$.runImpl(Boot.scala:41)
        at xsbt.boot.Boot$.main(Boot.scala:17)
        at xsbt.boot.Boot.main(Boot.scala)
[error] java.io.FileNotFoundException: /home/ubuntu/Thoth/markdown/ThothCustomMarkdownParser.scala (No such file or directory)

対応するソースの参照でパスを正しく読み切れずに落ちている模様。 markdown の前に src/main/scala-2.11 が入らないと正しいパスではありません。cobertura.xml の中にはソースのディレクトリがちゃんと出力されているので、scoverageが問題なわけではなさそうです。

プラグインのソース確認してみると、ソースのディレクトリは (sourceDirectories in Compile).gimme ってところから取得しているようですね。ソースファイル名の配列を生成する時にexistsで確認しているので、存在するファイルしかこの配列にないはずなのですが。。。

github.com

って、リリースしろよ。。。

Githubリポジトリからプラグインのソースを引っこ抜いて使う

こうやって踏み抜いて本題から離れたことをやらないといけないのが非常に私らしいところですね。ってか、これ結構ビルドに時間かかるようになってアレなんですが。。。

http://qiita.com/kawachi/items/71af20a102ecca41561d

を参考にしてみます。

logLevel := Level.Warn

addSbtPlugin("se.marcuslonnberg" % "sbt-docker" % "1.2.0")

addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.3.3")

lazy val root = project.in(file(".")).dependsOn(githubRepo)

lazy val githubRepo = uri("git://github.com/scoverage/sbt-coveralls.git")

とりあえずmasterブランチが欲しいので、コミットを指定していません。ローカルで動かしてcoverallsに飛ばすのは問題ありませんでした。プッシュしてみます。

まあ、見てください、9つしかテスト対象のないプロジェクトのビルドが10分オーバーですよ。そのほとんどが足回りですが。これは、プラグインとかライブラリとかはキャッシュが効くようにしないとまずいかもしれない。

というわけでようやくcoverallsに結果を飛ばせました。ご査収ください。

coveralls.io

*1:全体のカバレッジカバレッジの低下率の2指標

*2:rails用のセットアップ手順だったこともあります