LoginSignup
12
10

More than 3 years have passed since last update.

HaskellとHspecでBuddy CI

Last updated at Posted at 2019-10-02

まえがき

2019年のCIはBuddyを使うっぽいので、がんばる。

ちゅうい

この記事は最低限のことしか書いていない。過度に期待してはならない。

Haskell Stack じゃなくて Cabal じゃん

Buddy 公式のクイックスタートドキュメント CI/CD for Haskellがstackではなくcabalを使ったやり方だったので、寝たら忘れてしまうだろうから、この記事にメモっておく。

プロジェクトを作成する

Haskell Stackでプロジェクトの作成

bash
stack new プロジェクト名
cd プロジェクト名

Githubにpushする

Githubにリポジトリを作り、pushする

bash
# このへんでgit remoteの設定してから

git add .
git commit -m 'first commit'
git push

TDD しているフリをする

ひとまずテストが動くか試す

stack testコマンドを実行して、 Test suite not yet implemented が印字されたら「テストスイートがまだ未実装」が成功だ。

bash
stack test

依存ライブラリの設定

hspec を使うのでpackage.yamlhspecを追加する。

package.yaml
tests:
  プロジェクト名-test:
    main:                Spec.hs
    source-dirs:         test
    ghc-options:
    - -threaded
    - -rtsopts
    - -with-rtsopts=-N
    dependencies:
    - プロジェクト名
    - hspec

*Spec.hs を根こそぎimportされるようにする

手動ですべてのSpecファイルをimportしないで済むように、 テストプロジェクトのメインファイル test/Spec.hsを修正する。

GHCの魔法

test/Spec.hsを次の1行のみにすると、GHCの魔法がかかって*Spec.hsがなぜかimportされるようになる。

test/Spec.hs
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}

詳しくは、Automatic spec discoveryを参照。

LibSpec.hsの作成

srcディレクトリーにLib.hsがあるので、それに対応するLibSpec.hstestディレクトリーに作成する。

作成する関数は、みんな大好きフィボナッチ関数だ。

test/LibSpec.hs
module LibSpec (spec) where

import Test.Hspec
import Lib

spec :: Spec
spec = do
  describe "fib :: Int -> Int" $ do
    context "fib 0" $ do
      it "should be 0" $ do
        fib 0 `shouldBe` 0

テストがコンパイルエラーになるのを確認する

stack test を実行して、Haskell初心者が気を失うようなコンパイルエラーが印字されたら成功だ。

bash
stack test

# ...略
/Users/jdoiwork/github/try-buddy-ci/test/LibSpec.hs:11:9: error:
    • Variable not in scope: fib :: Integer -> Integer
    • Perhaps you meant ‘fit’ (imported from Test.Hspec)
   |        
11 |         fib 0 `shouldBe` 0
   |         ^^^


なぜコンパイルエラーになるのだろう? 当然だ。 テストコードは書いたが、肝心のfib関数を実装していないからだ。

fib関数を仮実装する

src/Lib.hsを修正して、fib関数を仮実装しよう。 ついでにstack newで自動生成されたsomeFunc関数は消してしまおう。

src/Lib.hs
module Lib
    ( fib
    ) where

fib :: Int -> Int
fib _ = 777

app側もsomeFuncを使っているコードがあるので、適当に消してしまおう。

app/Main.hs
module Main where

import Lib

main :: IO ()
main = print "Hello Main"

テストがエラー(RED)になるのを確認する

stack test を実行して、テストがエラーになったら成功だ。

bash
stack test

# ...略

Lib
  fib :: Int -> Int
    fib 0
      should be 0 FAILED [1]

Failures:

  test/LibSpec.hs:11:9: 
  1) Lib, fib :: Int -> Int, fib 0, should be 0
       expected: 0
        but got: 777

  To rerun use: --match "/Lib/fib :: Int -> Int/fib 0/should be 0/"

Randomized with seed 302256979

Finished in 0.0033 seconds
1 example, 1 failure

わざとらしく赤くしたところでグリーンにしよう。だってTDDだもん。

fib 0を実装する

ケント・ベックの本を読んだ人なら、これくらい一歩ずつ進むハズだ。

src/Lib.hs
fib :: Int -> Int
fib 0 = 0
fib _ = 777

テストが成功(GREEN)になるのを確認する

stack test を実行して、テストが成功になったら成功だ。

bash
stack test

# ...略

Lib
  fib :: Int -> Int
    fib 0
      should be 0

Finished in 0.0002 seconds
1 example, 0 failures

TDDに夢中になりそうなので、git commitgit pushしたら、そろそろBuddyを働かせよう。

Buddy CI

https://buddy.works/ に行く。

「ユーザー登録」と「プロジェクトの作成」は、大雑把にしか説明しないから、詳しく知りたい人は [CI] Buddy を始めましたを見よう。

ユーザー登録

ページ中央付近の「Sign up for free with...」 で github を指定してボタンをポチポチするだけでOKだ。

プロジェクトの作成

今作ったgithubリポジトリを指定する。

パイプラインの作成

Add a new pipelineボタンを押して、NAMEを適当に入力して、TRIGGER MODEOn pushにする。

add a new pipeline.png

入力内容を確認したら、フォームの下の方のAdd a new pipelineボタンを押す。

アクションの作成

テストを実行するためのアクションを作成する。

new action.png

Haskellアクションの選択

フィルターテキスト入力にhaskellと打って、アクションを絞り込んでHaskellをクリックする。

filter haskell.png

Run 設定の編集

BUILD COMMANDS のテキストエリアがcabalを使う形になっているので、以下のようにstack testに変更する。

stack test.png

Environment 設定の編集

Environmentボタンをクリックして、Imagefpco/stack-build-smallVersionlatest(または任意のバージョン)にする。

他のDocker Imageがいい人は、 https://hub.docker.com/u/fpco 辺りを探すか自作する。

fpco build.png

アクションの追加

下の方へスクロールして、Add this actionボタンをクリックしてアクションを追加する。

add this action.png

パイプラインの完成

おめでとう、最初のパイプラインの完成だ。

complete pipeline.png

パイプラインの実行

右上のRun pipelineボタンを押すと、パイプライン実行フォームが出るのでRun nowボタンをクリックして、パイプラインを実行する。

run now.png

パイプライン実行結果

Logsボタンをクリックして、hspecの成功とBuild finished successfully!.とグリーンになっていたら成功だ。

pipeline done.png

実装を再開する

最低限の環境が整ったので、実装を再開する。 これからは手動でパイプラインを実行しないでも、 Github にpushされたらパイプラインが実行される設定になっているので、git pushするたびにオートでやってくれるはずだ。

パイプラインが失敗するのを確認する

失敗するテスト(未実装のfib 1のテストコード)を書いて、パイプラインが正常に失敗するのを確認しよう。

test/LibSpec.hs
spec :: Spec
spec = do
  describe "fib :: Int -> Int" $ do
    context "fib 0" $ do
      it "should be 0" $ do
        fib 0 `shouldBe` 0
    -- ↓を追加
    context "fib 1" $ do
      it "should be 1" $ do
        fib 1 `shouldBe` 1

ローカルで失敗するのを確認する

stack testを実行して、テストが失敗するのを確認したら、git commitgit pushをして、パイプラインを動かそう。

bash
stack test

# ...略

Lib
  fib :: Int -> Int
    fib 0
      should be 0
    fib 1
      should be 1 FAILED [1]

Failures:

  test/LibSpec.hs:14:9: 
  1) Lib, fib :: Int -> Int, fib 1, should be 1
       expected: 1
        but got: 777

  To rerun use: --match "/Lib/fib :: Int -> Int/fib 1/should be 1/"

Randomized with seed 435405822

Finished in 0.0015 seconds
2 examples, 1 failure

パイプラインの失敗

pipeline failed.png

よし! 正しく失敗できたぞ!!

パイプラインが成功に戻るのを確認する

fib 1を実装して、テストをグリーンにする

fib関数の続きを実装しよう。

src/Lib.hs

fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib _ = 777

stack test を実行して、ローカルでテストが成功するのを確認する。

bash
stack test

# ...略

Lib
  fib :: Int -> Int
    fib 0
      should be 0
    fib 1
      should be 1

Finished in 0.0004 seconds
2 examples, 0 failures

pushしてパイプラインが成功するのを確認する

git commitgit pushをしたら、Buddyで新たにパイプラインが実行されるので成功するのを確認しよう。

pipeline done2.png

やった! グリーンだよ!! これで最低限のCI環境が整った(個人の感想です)。

継続的にインテグる

さらなる fib へ

あとは気の済むまでフィボナッチ関数を実装しよう。 うっかりテストを通さずにpushしても間違いに気づきやすくなるかもしれない。

test/LibSpec.hs

spec :: Spec
spec = do
  describe "fib :: Int -> Int" $ do
    context "fib 0" $ do
      it "should be 0" $ do
        fib 0 `shouldBe` 0
    context "fib 1" $ do
      it "should be 1" $ do
        fib 1 `shouldBe` 1
    context "fib 2" $ do
      it "should be 1" $ do
        fib 2 `shouldBe` 1

src/Lib.hs

fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2) -- TODO: SICPを最後まで読んで、もっと効率のいい実装にする

finished.png

依存ライブラリのビルド時間を短縮する

:thinking::thinking::thinking: この章は、オマケだ。 気が長いならやらなくてもよい。 :thinking::thinking::relaxed:

現状の単純なパイプラインだと、パイプラインが実行されるたびに依存ライブラリがビルドされてしまう。テストの実行時間より依存ライブラリのビルド時間の方が長いのは、致命的な致命傷だ。

この問題を解決するために、次のようにパイプラインを2つに分ける。

  • 依存ライブラリのビルド
  • コンパイル済み依存ライブラリのコピー → テスト実行

依存ライブラリのビルドパイプラインの作成

パイプライン一覧に行き、一覧の右のサイドメニューから Clone existing pipeline を選択して既存のパイプラインを複製する。

clone pipeline.png

作成済みのパイプラインを選択して Clone actions ボタンをクリックしてパイプラインを複製する。

依存ライブラリのみビルドする

次の手順の通りにして、依存ライブラリのみビルドされるようにする。

  1. パイプライン一覧で複製したパイプラインを選択する
  2. Actions 設定を選択する
  3. stack test していたHaskellアクションを選択する
  4. BUILD COMMANDSstack test --only-dependencies --stack-root $(pwd)/.stack-root にする

stack test -od.png

Save this action ボタンをクリックして、アクション保存する。

パイプライン設定の変更

ついでに、パイプライン名をわかりやすいものに変更し、パイプラインが走るタイミングも手動に変更する。

  1. Settings 設定をクリックする
  2. NAMEを適当なものに編集する
  3. TRIGGER MODEManualにする
  4. 下の方へスクロールしてSave settingsボタンを押して保存する。

rename stack test -od.png

今回は手動でパイプラインが走れば十分だが、定期的にパイプラインを走らせたければTRIGGER MODERecurrentlyにすると定期実行設定が選べる。

依存ライブラリのパス

依存ライブラリの保存場所は次の2ヶ所。

  • ./.stack-work
  • $HOME/.stack

Buddy上では(というかdockerコンテナ内だから?)、$HOME/.stack/root/.stackになってしまい、後からアクセスできない場所に出力されてしまうため(セキュリティ上の理由か?)、--stack-rootオプションで出力場所を作業ディレクトリーの.stack-rootに変更している。

stack test --only-dependencies --stack-root $(pwd)/.stack-root

依存ライブラリビルド用パイプラインの実行

パイプライン一覧から作成した依存ライブラリビルド用パイプラインを選択し、 右上の Run pipeline ボタンをクリックしてパイプラインを実行しよう。

run od pipeline.png

ビルドが成功し、テストが実行されていなければ(hspecのログが出ていなければ)OKだ。

ビルド済み依存ライブラリのコピー

テストを走らせるパイプラインに戻り、.stack-work.stack-rootをコピーするアクションを2つ作成する。

既存のアクションの上下にギアのアイコンの+ボタンがあるので、それをクリックするとアクションの追加ができる。

add action.png

stack-root Copyアクションの作成

フィルターテキスト入力にcopyと打って、Copy filesアクションを選択する。

add copy action.png

設定値を次の表のようにする。

設定 備考
Pipeline さっき作った依存ライブラリビルド用パイプライン コピー元のパイプラインを指定する
Provide the path to the files that you want to copy .stack-root/ コピー元のファイル Browse... で探すのが楽ちん
Provide the destination path .stack-root コピー先のパス

copy stack root.png

入力したら、フォームの下の方のAdd this actionボタンをクリックして、アクションを作成する。

stack-work Copyアクションの作成

同様にstack-work用のCopyアクションを作成する。

設定値を次の表のようにして、アクションを作成する。

設定 備考
Pipeline さっき作った依存ライブラリビルド用パイプライン コピー元のパイプラインを指定する
Provide the path to the files that you want to copy .stack-work/ コピー元のファイル Browse... で探すのが楽ちん
Provide the destination path (何も入力しないでOK) コピー先のパス

copy stack work.png

ビルド済み依存ライブラリを使うようにする

stack testアクションを選択して、BUILD COMMANDSを次のようにする。

--stack-root--allow-different-userオプションを追加している。

stack test --stack-root $(pwd)/.stack-root  --allow-different-user

stack test with root.png

--allow-different-userオプションを指定しないと、セキュリティ意識が高いのか依存ライブラリがビルドし直されてしまうので注意。

Save this actionボタンをクリックして保存する。

速くなった(たぶん)パイプラインを実行する

右上の Run pipeline ボタンをクリックして、パイプラインを実行する。上手くできていることを願う。

stack test fast.png

パイプラインの実行時間の比較

ほとんどライブラリを使っていないプロジェクトでも2分弱かかっていたのが、数秒〜十数秒に短縮された。

やったぜ!

run history.png

分割したパイプライン

見事、依存ライブラリのビルド用パイプラインとテスト実行用のパイプラインに分けることができた。

pipelines.png

今後の課題

  • deployがんばれ
12
10
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
10