HaskellとHspecでBuddy CI

# まえがき
2019年のCIは[Buddy](https://buddy.works/)を使うっぽいので、がんばる。

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

## Haskell Stack じゃなくて Cabal じゃん
Buddy 公式のクイックスタートドキュメント [CI/CD for Haskell](https://buddy.works/docs/quickstart/haskell)がstackではなくcabalを使ったやり方だったので、寝たら忘れてしまうだろうから、この記事にメモっておく。


# プロジェクトを作成する

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

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

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

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

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

# TDD しているフリをする

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

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

```bash:bash
stack test
```

## 依存ライブラリの設定
`hspec` を使うので`package.yaml`に`hspec`を追加する。

```yaml: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`を次の１行のみにすると、GHCの魔法がかかって`*Spec.hs`がなぜかimportされるようになる。

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

詳しくは、[Automatic spec discovery](http://hspec.github.io/hspec-discover.html)を参照。

## LibSpec.hsの作成

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

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

```hs: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: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`関数は消してしまおう。

```hs:src/Lib.hs
module Lib
    ( fib
    ) where

fib :: Int -> Int
fib _ = 777

```

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

```hs:app/Main.hs
module Main where

import Lib

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

```

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

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

```bash: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を実装する

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

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

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

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

```bash:bash
stack test

# ...略

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

Finished in 0.0002 seconds
1 example, 0 failures

```

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

# Buddy CI

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

「ユーザー登録」と「プロジェクトの作成」は、大雑把にしか説明しないから、詳しく知りたい人は [[CI] Buddy を始めました](https://srz-zumix.blogspot.com/2018/07/ci-buddy.html)を見よう。

## ユーザー登録

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

## プロジェクトの作成

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

## パイプラインの作成

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

<img width="932" alt="add a new pipeline.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/b2973dc1-15d0-89ec-db4e-4ad5f03e95be.png">

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

## アクションの作成

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

<img width="811" alt="new action.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/ed345a18-d9b6-daef-6e36-baaebafab06d.png">

### Haskellアクションの選択

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

<img width="811" alt="filter haskell.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/00c8ef4e-0db5-afbb-9244-cc5328acb9a0.png">

### Run 設定の編集

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

<img width="811" alt="stack test.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/d1d47959-d1e9-7cf3-80d1-e517d38d92fd.png">

### Environment 設定の編集

`Environment`ボタンをクリックして、`Image`を`fpco/stack-build-small`、`Version`を`latest`(または任意のバージョン)にする。

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

<img width="811" alt="fpco build.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/c2c68ff2-ba55-e5c9-5599-3e1affebe983.png">

### アクションの追加

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

<img width="221" alt="add this action.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/55b5a1a1-938e-1103-bd14-039787d74e9f.png">

### パイプラインの完成

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

<img width="880" alt="complete pipeline.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/9a7bea3b-26b8-b0b0-6ad3-6ee34ecbd54a.png">

## パイプラインの実行

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

<img width="879" alt="run now.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/4035e17a-cfad-d5c7-73e4-4dacda892e8f.png">

### パイプライン実行結果

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

<img width="773" alt="pipeline done.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/710f29d7-d16e-a6fa-7fef-760977ee81fd.png">

# 実装を再開する

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

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

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

```hs: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 １" $ do
        fib 1 `shouldBe` 1
```

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

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

```bash:bash
stack test

# ...略

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

Failures:

  test/LibSpec.hs:14:9: 
  1) Lib, fib :: Int -> Int, fib 1, should be １
       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

```

### パイプラインの失敗

<img width="773" alt="pipeline failed.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/619ac930-d5d1-a851-2334-b047d960b4c4.png">

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

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

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

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

```hs:src/Lib.hs

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

```

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

```bash:bash
stack test

# ...略

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

Finished in 0.0004 seconds
2 examples, 0 failures
```



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

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

<img width="773" alt="pipeline done2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/a6791ece-bb0c-e451-a564-e4f0e120f9bb.png">

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

## 継続的にインテグる

### さらなる fib へ

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

```hs: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 １" $ do
        fib 1 `shouldBe` 1
    context "fib 2" $ do
      it "should be 1" $ do
        fib 2 `shouldBe` 1

```

```hs:src/Lib.hs

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

```

<img width="773" alt="finished.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/d44ffcf9-109d-8556-42c4-a67ec988ff8d.png">

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

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

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

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

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

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

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

<img width="727" alt="clone pipeline.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/d5eca948-ec53-0163-9cb7-c10836069f03.png">


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

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

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

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

<img width="759" alt="stack test -od.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/cb0f93d8-0b3c-644c-c515-a78cecf7e519.png">

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

### パイプライン設定の変更

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

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

<img width="759" alt="rename stack test -od.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/9e1b2835-9c3e-22b3-8785-530259261bbd.png">

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

### 依存ライブラリのパス

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

- `./.stack-work`
- `$HOME/.stack`

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

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

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

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

<img width="759" alt="run od pipeline.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/eada234e-043c-b5aa-cde4-93c638b5ecaf.png">

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

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

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

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

<img width="759" alt="add action.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/94ff3342-a46a-8252-2e66-e73e32cd59b8.png">

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

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

<img width="759" alt="add copy action.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/74daf614-5db6-7d3f-7b07-1a7c45082011.png">

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

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

<img width="759" alt="copy stack root.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/b3d8fd5d-d28f-a1a4-2f7a-7af28b9fd0e3.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) | コピー先のパス |

<img width="759" alt="copy stack work.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/5726b598-1874-64a2-80b1-aeb28e63f2ca.png">

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

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

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

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

<img width="759" alt="stack test with root.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/85acc41d-cc43-449f-4255-8ff990d62c60.png">

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

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

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

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

<img width="759" alt="stack test fast.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/152fa9d4-7968-e712-8a67-7e22e2fb0292.png">

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

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

やったぜ！

<img width="766" alt="run history.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/57d47eb5-7db3-5628-f44c-cac36e379eee.png">

### 分割したパイプライン

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

<img width="886" alt="pipelines.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42122/267cfb1f-a428-9d55-d447-49ae4e6b4bc6.png">


# 今後の課題

- deployがんばれ