kurosame’s diary

フロントエンド中心です

Python と VSCode の快適な開発環境を考える

2023/06追記

パッケージ管理は、Ryeが良さそう
Rye は Flask の作者が開発している

brew install rye

プロジェクト初期化

rye init .

依存パッケージ追加

rye add [パッケージ名]
rye sync

実行

rye run python main.py

最近 Go の開発環境を VSCode で構築したので、Python の開発も IntelliJ から VSCode に移行しよう
Go と違って PythonIntelliJ の Community 版でもストレス無く開発してたが、複数 IDE だと面倒なので開発環境を VSCode に統一したい


Pipenv

Pipenv はパッケージ管理と実行環境(仮想環境)の構築を行ってくれる

brew install pipenv
export PIPENV_VENV_IN_PROJECT=true
pipenv install
pipenv --venv

デフォルトだと仮想環境は~/.local/share/の下に作られる
色々不便なので、PIPENV_VENV_IN_PROJECT=trueを設定してプロジェクト直下に作られるようにしている

VSCode の settings.json に以下を追加して、再起動

"python.pythonPath": "${workspaceFolder}/.venv/bin/python",

VSCode

Python 拡張プラグインをインストールする

おそらく VSCode の Notification で Lint やコードフォーマッターのインストールを促すメッセージが出ているので、インストールする
上記の Pipenv の設定が済んでいれば Pipenv でインストールされる、Pipenv を使ってなければ pip でインストールされる

普通にコマンドでインストールする場合は以下

pipenv install pylint --dev
pipenv install black --dev

コードフォーマッターはblackにした

"python.formatting.provider": "black"

Go と VSCode の快適な開発環境を考える

2022-05-23

以下の GOPATH と dep を使ったパッケージ管理は古いので更新

Go をインストールもしくは、最新にする

brew install go # Go 1.18

他の依存管理ツールは不要

cd bots/rss
go mod init github.com/kurosame/bots-go/bots/rss

go.modが作成される

import に依存パッケージを記載

import (
    "github.com/joho/godotenv"
    "github.com/slack-go/slack"
)
go mod tidy

go.sumが作成される

go.modはパッケージのバージョンやパスが記載されている
go.sumはパッケージのチェックサムが記載されている(パッケージをインストールした時と完全に同じことを担保する)

go.modgo.sumGitHub 管理した方がよい
https://github.com/golang/go/wiki/Modules#releasing-modules-all-versions

複数のバージョンのGoをインストール
https://go.dev/doc/manage-install

go install golang.org/dl/go1.16.15@latest
~/go/bin/go1.16.15 download
~/go/bin/go1.16.15 version

~/go/binを PATH に通しておくとよい


IntelliJ で Go を書こうと思ったが、Community 版なので Go プラグインが古いのしか無かった
なので VSCode で書こうと思って環境構築しました
けっこう色々必要かなと思って構築したので、メモっておきます


goenv

Go のバージョン管理を行うツール
Go のバージョンを簡単に切り替えることができる

brew install goenv

以下を追加

# ~/.config/fish/config.fish

# goenv
set -x PATH $HOME/.goenv/shims $PATH
source ~/.config/fish/config.fish
goenv install 1.11.4
goenv global 1.11.4
go version

direnv

ディレクトリごとに環境変数を切り替える
例えば GOPATH をディレクトリごとに変えることができる
⇒ 私の場合はghqを使っているので、ghq 配下のリポジトリごとに GOPATH を変えれるのが便利でした

brew install direnv

以下を追加

# ~/.config/fish/config.fish

export EDITOR=vim # これを設定してないと`direnv edit`でエラーになる

# direnv
eval (direnv hook fish)
source ~/.config/fish/config.fish
direnv edit .

EDITOR 環境変数で設定したエディタが開くので、以下を追加

export GOPATH=$(pwd)

GOPATH を今のディレクトリにするって感じです


dep

公式のパッケージ管理ツール

brew install dep

dep$GOPATH/src/projectというディレクトリ構成にしないといけないらしい
※ project は任意の名前を付けてください

mkdir src/default
cd src/default
dep init

結果 GOPATH 配下は、以下のようなディレクトリ構成になる
(なんかキモい構成だけど我慢しよう…)

.
├── .envrc
├── .gitignore
├── pkg
│   └── dep
│       └── sources
└── src
    └── default
        ├── Gopkg.lock
        ├── Gopkg.toml
        └── vendor

pkg/depdep のキャッシュで vendor に実際使ってるパッケージが入る
src/default 配下が作業ディレクトリになる

以下のように外部パッケージが必要な import があれば

import (
    "github.com/pkg/errors"
)

以下を実行するだけで、依存解決しつつ vendor ディレクトリにインストールしてくれる

dep ensure

依存を確認する場合は、Gopkg.lockを見るかdep statusを実行すると良いです


VSCode

とりあえずGo 拡張プラグインをインストールする
このプラグインは色々外部パッケージが必要なので、go getでインストールする
(たぶん VSCode の Notification でインストールを促すメッセージが出てると思います)

npm 慣れしてる私からすると Lint やコードフォーマッターなどの開発専用系のパッケージも依存管理するものだと思っていたが、dep がたぶんプログラムに import したパッケージのみを管理するようなので、それに従って開発専用系のパッケージはgo getでインストールする

また、dep 管理しているパッケージは認識してくれないので、VSCode の settings.json に以下を追加して、再起動

"go.gopath": GOPATHを設定

複数リポジトリで settings.json を使い回すことを想定していれば、"go.gopath": "${workspaceFolder}"などにした方が良いでしょう


最終的に、以下のようなディレクトリ構成になった
(なんかさらにキモい構成になった気がする…)

.
├── .envrc
├── .gitignore
├── bin -> `go get`したやつのバイナリ
├── pkg
└── src
    ├── default
    │   ├── Gopkg.lock
    │   ├── Gopkg.toml
    │   └── vendor
    ├── github.com -> `go get`したやつ
    └── golang.org -> `go get`したやつ

Go 開発はかなり久しぶりなので、開発しながら良い構成を考えていきたいと思います

今回の作業リポジトリgithub.com

また、これから作るアプリケーションのディレクトリ構成は以下を参考にしようかなと思ってます github.com

webpack + TypeScript 環境でのバンドル速度改善

会社の webpack が遅くなってきたので、TypeScript 周りでバンドル速度が改善できるか調べてみました
そして、以下の記述に見つけて、バンドル速度改善をする上で割と重要なオプションな気がしたので実装してみようと思います
GitHub - TypeStrong/ts-loader: TypeScript loader for webpack

今回実装したコードの結果は以下にあげてます github.com

では、簡単ですが実装を説明していきます


実装

ビルドプロセスを並列化する方法として、HappyPackthread-loaderのどちらかを使う方法があるそうです
ただ、HappyPack の GitHub ドキュメントを見ると、HappyPack はメンテナンスモードに入るそうなので、今回は thread-loader を使うことにします

以下のように他の loader の前に記述することで、以降の loader の処理を並列化します
thread-loader は複数の Worker スレッドで loader を実行することで並列化を実現しています

// webpack.config.js
module: {
  rules: [
    {
      test: /\.tsx?$/,
      use: [
        {
          loader: 'thread-loader',
          options: {
            workers: require('os').cpus().length - 1
          }
        },
        'babel-loader',
        'ts-loader',
        'tslint-loader',
        'stylelint-custom-processor-loader'
      ],
      exclude: /node_modules/
    }
  ]
}

thread-loader を使う設定ができたら、次は ts-loader の happyPackMode をtrueにします
happyPackMode をtrueに設定すると transpileOnly オプションが暗黙的にtrueになるみたいです
ts-loader は JS へのトランスパイルと静的型チェックを行ってくれるのですが、transpileOnly オプションをtrueにすると JS へのトランスパイルのみを行います
静的型チェックを行わない場合や他のプラグインで代替する場合は、transpileOnly オプションをtrueにすることでコンパイル速度を上げることができます

// webpack.config.js
module: {
  rules: [
    {
      test: /\.tsx?$/,
      use: [
        {
          loader: 'thread-loader',
          options: {
            workers: require('os').cpus().length - 1
          }
        },
        'babel-loader',
        {
          loader: 'ts-loader',
          options: {
            happyPackMode: true
          }
        },
        'tslint-loader',
        'stylelint-custom-processor-loader'
      ],
      exclude: /node_modules/
    }
  ]
}

次にFork TS Checker Webpack Pluginを使って型チェックを行います
checkSyntacticErrors オプションをtrueにしてますが、HappyPack や thread-loader を使用している場合は必須の設定のようです
Fork TS Checker Webpack Plugin は Syntax エラーと Semantic エラーの両方をチェックする機能を備えているのですが、checkSyntacticErrors オプションはデフォルトはfalseなので Semantic エラーのみをチェックします
しかし、transpileOnly オプションがtrueでも Syntax エラーはチェックするので、checkSyntacticErrors オプションをtrueにする必要は本来は無いのですが、happyPackMode をtrueにした場合は、Syntax エラーはチェックされないみたいなので、checkSyntacticErrors オプションをtrueにする必要があります

分かりづらいですね、、しかも今回やろうとしてる速度改善とはあまり関係がありません
でもとりあえず設定しておいた方が良さそうです
ちなみにこのことは以下の checkSyntacticErrors の説明の所に書いてました
GitHub - Realytics/fork-ts-checker-webpack-plugin: Webpack plugin that runs typescript type checker on a separate process.
翻訳間違ってたらすみません

// webpack.config.js
const ForkTsChecker = require('fork-ts-checker-webpack-plugin')

module: {
  rules: [
    {
      test: /\.tsx?$/,
      use: [
        {
          loader: 'thread-loader',
          options: {
            workers: require('os').cpus().length - 1
          }
        },
        'babel-loader',
        {
          loader: 'ts-loader',
          options: {
            happyPackMode: true
          }
        },
        'tslint-loader',
        'stylelint-custom-processor-loader'
      ],
      exclude: /node_modules/
    }
  ]
},
plugins: [
  new ForkTsChecker({ checkSyntacticErrors: true })
]

実装は以上です


検証

とりあえず実行
f:id:kurosame-th:20190421150748p:plain
約 15 秒
あまり速くなってない。。

試しに thread-loader の Worker の数を 1 つにして実行
f:id:kurosame-th:20190421151046p:plain
こっちの方が倍速いじゃないか!

thread-loader の Worker の数を 2 つにして実行
f:id:kurosame-th:20190421151028p:plain
Worker の数を増やす度に遅くなってる。。😇

原因として今回検証に利用したReact Boilerplateはかなり小規模な検証環境だったので Worker の起動やプロセス間通信にかかるオーバーヘッドの方が大きかったのかなと思います
会社のプロジェクトであれば、そちらはかなり肥大化しているので、今度試してみます

ロト6 の当選番号を機械学習を使って予測する

はじめに

機械学習Python もまともに触ったことないし、数式にも馴染みがないけど
時代に取り残されないために勉強がてら scikit-learn を使ってロト6 の当選番号を予測してみます

一応この本の 8 章までは読んでます www.oreilly.co.jp

ロト6 を選んだのは、なんとなく実装が簡単そうだったからです

ちなみに「ロト6 機械学習」でググっても予測は難しいと言ってる人がほとんどなので、予測の難易度はかなり高いと思われます
あくまで勉強の一環です


ロト6 概要

一応ロト6 の簡単な概要

  • 1〜43 の中から 6 つの数字を選び、抽選結果の数字と一致している数によって当選金が分配される
  • 1 等約 2 億円(約と言ってるのは当選人数やキャリーオーバーによって変動するため)
  • 年中無休で購入可能
  • 抽選日は毎週月曜と木曜

機械学習モデルの選択

回帰モデルで訓練データが少なくてもいけそうなやつがいいかなって思ってましたが、ググってるとサポートベクターマシンSVM)で予測されてる方が何人かいたので scikit-learn の SVR クラスを使うことにしました

カーネルトリックを使ってみたいと思って、訓練データが少ない場合は、ガウス RBF カーネルが良さそうなのでそれを使います
訓練データは過去のロト6 の当選番号全てです
アウトプットは次回の抽選回の当選番号予測です
また、ボーナス数字の予測はしません


訓練データの作成

本数字 1〜6 をそれぞれ別の訓練データとして分割しています
最初は手っ取り早く過去の当選番号を全て 1 つの訓練データとして訓練させようとしてましたが、やめました

訓練データを作成するプログラム

import pandas as pd

url = 'http://sougaku.com/loto6/download/loto6.zip'

train = pd.read_csv(url, encoding='cp932')

X = train['抽せん回']
X_train = X[:, np.newaxis]
y_train = [train['本数字1'].values,
           train['本数字2'].values,
           train['本数字3'].values,
           train['本数字4'].values,
           train['本数字5'].values,
           train['本数字6'].values]

next_round = X.tail(1).values[0] + 1

X_train は過去の抽選回全て
y_train は本数字ごとの過去の当選番号全て
next_round は予測を行う回(つまり次回の抽選回)
です


学習と予測

以下のコードは本数字 1 のみを学習(fit)して、予測(predict)するプログラムです
本数字は 6 つあるので、ループするなどして計 6 回学習と予測を実行します

from sklearn.svm import SVR

svr_rbf = SVR(kernel='rbf', gamma=..., C=..., epsilon=...)
svr_rbf.fit(X_train, y_train[0]).predict(np.array([[next_round]]))

γ、C、ε の 3 つのハイパーパラメータを調整して実行します


ハイパーパラメータの最適化

scikit-learn のグリッドサーチを使えば簡単にできます
SVMSVR のハイパーパラメータの範囲は広いので 2 のべき乗の値をパラメータの候補として設定するそうです

パラメータ 候補の範囲 候補の数 Python で書くと
γ 2^{-20} 〜 2^{10} 31 np.logspace(-20, 10, 31, base=2)
C 2^{-5} 〜 2^{10} 16 np.logspace(-5, 10, 16, base=2)
ε 2^{-10} 〜 2^0 11 np.logspace(-10, 0, 11, base=2)

だいぶ絞られましたが、それでも 3 つのパラメータの組み合わせは31 x 16 x 11 = 5456通りもあります

以下のコードは本数字 1 のみを対象にグリッドサーチを使い、5456 通りのパラメータの組み合わせ全てを学習・評価し、精度が良いものを出力する例です
ただし、学習データの量にもよりますが、かなり時間がかかる(むしろ終わらない)ため、ある程度 5456 通りから更に候補を絞る必要は出てきます

param_grid = {
    'kernel': ['rbf'],
    'gamma': np.logspace(-20, 10, 31, base=2),
    'C': np.logspace(-5, 10, 16, base=2),
    'epsilon': np.logspace(-10, 0, 11, base=2)
}

grid_search = GridSearchCV(SVR(), param_grid, iid=False, cv=5)
### かなり時間がかかるため、パラメータの候補を更に絞った方が良い ###
grid_search.fit(X_train, y_train[0])
gs = grid_search.best_params_

print('grid-search result: {}'.format(gs))
# grid-search result: {'C': 32.0, 'epsilon': 0.5, 'gamma': 1.0, 'kernel': 'rbf'}

予測と当選結果確認をスケジューリング

毎回プログラムを実行したり当選結果を確認するのは面倒なので、ある程度自動化しておきます

以下のコードは、ロト6 の当選番号が載ってるサイトから抽選回、当選番号、ボーナス番号をスクレイピングしています

import bs4
import re
from urllib.request import urlopen

url = 'http://www.takarakuji-loto.jp/tousenp.html'

html = bs4.BeautifulSoup(urlopen(url).read(), 'html.parser')
section = html.find('section', class_='tousenno')
tbody = section.find_all('tbody')

current_round = re.match('第[0-9]+回', section.find('div').string).group()
winning_number = [i.get('alt') for i in tbody[0].find_all('img')]
bonus_number = tbody[1].find('img').get('alt')

以下のコードは、CircleCI のスケジューリング機能を使ってロト6 の当選番号を予測するプログラム(svr.py)を呼ぶ predict ジョブと当選結果をスクレイピングするプログラム(res.py)を呼ぶ result ジョブを設定しています
毎週月曜と木曜が抽選日なので、predict ジョブは前日の日曜日と水曜日に実行し、result ジョブは翌日の火曜日と金曜日に実行するようにしています

references:
  commands:
    setup-docker: &setup-docker
      docker:
        - image: kurosame/circleci-python

version: 2
jobs:
  predict:
    <<: *setup-docker
    steps:
      - checkout
      - run:
          name: Predict
          command: python3 svr.py
  result:
    <<: *setup-docker
    steps:
      - checkout
      - run:
          name: Get result
          command: python3 res.py

workflows:
  version: 2
  nightly-predict:
    triggers:
      - schedule:
          cron: '00 0 * * 0,3'
          filters:
            branches:
              only:
                - master
    jobs:
      - predict
  nightly-result:
    triggers:
      - schedule:
          cron: '00 0 * * 2,5'
          filters:
            branches:
              only:
                - master
    jobs:
      - result

CircleCI で使っている Docker は以下です
Python3 とプログラム実行に必要なライブラリをインストールしたイメージを使っています hub.docker.com

また、各プログラムの最後で Slack に通知するように実装しています

import slackweb

url = 'https://hooks.slack.com/services/...' # SlackのWebhook URL
slackweb.Slack(url).notify(text='ここに予測結果とか当選番号とか色々設定しておく')

流れ的には

  1. 日曜日と水曜日に predict ジョブが cron 実行され、Slack に予測結果を通知
  2. ロト6 を購入
  3. 月曜日と木曜日に抽選が行われる
  4. 火曜日と金曜日に result ジョブが cron 実行され、Slack に当選結果を通知

さいごに

現在 2 回予測結果でロト6 を購入しましたが外れました(2 回とも 2 個までは数字は的中しました)
とりあえずランダムで買った時とあまり結果が変わらないなーって思ったらやめます
ちなみに次は競馬予測をやってみようかなと思ってます

Vue.js の data と methods と computed の使い分けについて

Vue.js で変数や関数を定義する時に data と methods と computed のどのオプションを使って書くのか
たまに迷うことがあるので、現状の理解の簡単なメモです


data と methods と computed の違い

  • data はコンポーネントの(編集可能な)ローカル変数
  • methods は関数
  • computed はプロパティ(値に変数も関数も定義できる)

基本的に computed を使うで良い

  • computed はデフォルトは getter 関数のみを提供しているため、Read-Only なプロパティを定義できる
    ※ ちなみに setter 関数も書けます

  • computed で定義したプロパティはキャッシュされる
    computed は Vue インスタンスを作成する際に、Watcher を生成している
    computed 内で data や props など を参照していた場合、data や props などの変更を Watcher に通知している
    ⇒ Vue インスタンス生成時にプロパティをキャッシュしておき、Watcher に通知があった時のみプロパティを再評価する仕組み

まとめると computed を使うと良い点は

  • 簡単に Read-Only なプロパティを定義できる
  • プロパティの中身で外部(data や props など)参照してても、キャッシュされる
    もちろん単純な定数的なプロパティもきちんとキャッシュされる
  • watch を使っている箇所を減らせるかも
    例えば props を watch して data の変数を更新する処理など computed のみで実装可能なパターンがけっこうあるかも

methods を使っているケース

  • 引数を必要とする
    data や computed などで定義されてない Vue.js 外の変数や関数を引数として渡す場合、computed ではキャッシュできないので、methods に書いている

  • 関数内でイベントを発火している
    キャッシュ時に emit とかでイベントが発火されると困る場合は、methods に書いている また、関数が呼ばれる度に常に実行したい処理があるとき

まとめると methods を使うケースは

  • Vue.js で管理していないパラメータを引数として渡すとき
  • キャッシュされると困る処理があるとき

data を使っているケース

  • 変更が多い変数
    キャッシュしても変更が多い変数はキャッシュする意味があまり無いので、data に書いてる
  • リアクティブな変数を宣言したい時
    変更をリアクティブに画面反映させたい時

まとめると data を使うケースは

  • 変更が多くキャッシュしても意味が無い変数を定義したいとき
  • リアクティブな変数を定義したいとき

E2E テストを Jest + Puppeteer から Cypress へ移行する

はじめに

Jest + Puppeteer で E2E テストを書いてて、使いやすかったのだけど、CircleCI 上で(コード変えてないのに)ちょいちょい落ちるので Cypress を試してみることにした
Cypress にした理由は、Twitter とかで最近名前をよく聞いて、評判も良さそうイメージだったから
Cypress は Chrome のみに対応しているテスト専用のツール
Selenium や Puppeteer とかは、ブラウザを操作して色々やるツールなので、テスト専用ってわけではない


導入

ここ見ながらやってみる docs.cypress.io

yarn add -D cypress
# package.json
"scripts": {
  "e2e": "cypress open"
}
yarn e2e

いくつかサンプルテストを追加してくれたらしい f:id:kurosame-th:20190201145301p:plain

全部のテストを実行する時はこっち f:id:kurosame-th:20190201145845p:plain

特定のテストを実行する時はこっち f:id:kurosame-th:20190201150321p:plain

使いやすい!


Cypress のディレクトリ構造

├── fixtures
│   ├── example.json
│   ├── profile.json
│   └── users.json
├── integration
│   └── examples
│       ├── actions.spec.js
│       ├── aliasing.spec.js
│       ├── assertions.spec.js
│       ├── connectors.spec.js
│       ├── cookies.spec.js
│       ├── cypress_api.spec.js
│       ├── files.spec.js
│       ├── local_storage.spec.js
│       ├── location.spec.js
│       ├── misc.spec.js
│       ├── navigation.spec.js
│       ├── network_requests.spec.js
│       ├── querying.spec.js
│       ├── spies_stubs_clocks.spec.js
│       ├── traversal.spec.js
│       ├── utilities.spec.js
│       ├── viewport.spec.js
│       ├── waiting.spec.js
│       └── window.spec.js
├── plugins
│   └── index.js
├── screenshots
│   └── All Specs
│       └── my-image.png
└── support
    ├── commands.js
    └── index.js
  • fixtures
    スタブ的な感じ
  • integration
    ここにディレクトリ作って、テスト書いていくっぽい
  • plugins
    Cypress 自体の拡張プラグインを設定できる
  • screenshots
    スクリーンショットが置かれる
  • support
    Cypress で定義している関数をオーバーライドしたり、新しく関数を定義できる
    使う時は cy からメソッドチェーンで実行できる
    これかなり便利そう

基本的には、新しくテスト書き始める時は、integration の中にファイルかディレクトリ作って、examples ディレクトリにあるサンプル見ながらテスト書けば良さそう

さいごに

こちらの Vue.js で書かれたリポジトリの E2E テストを Cypress に移行しました github.com

今回は簡単なテストしか作ってませんが、規模が大きくなっても使っていけそうな感じはあります
CircleCI などのCI上で動かしたい時は Cypress の Docker イメージがいくつか用意してあるので、それを使うと楽できます
https://github.com/cypress-io/cypress-docker-images

ローカルで開発中は watch モードがほしいので、cypress openを使い、それ以外の CI で動かす時などはcypress runで良さそうです

実際にテスト書いてみた感想として

  • GUI とコマンドのどちらからクローズしても、GUI やプロセスがちゃんと kill される
    (当たり前のことかもしれませんが、Puppeteer 使ってる時はプロセスが残ってることが多かったので)
  • キャッシュが効いてるので、(変更した箇所以外の)2 回目以降のテストが早い
  • テストコードが少なく、1 つ 1 つのケースが分かりやすい
  • スクリーンショットが多機能だと思った

暇な時に React を使ってるプロジェクトにも導入してみようと思います

CircleCI で Lighthouse を定期的に実行して Slack に通知する

計測ツールの選定

調べると以下の 3 つが良さそうだった

  • WebPageTest
  • PageSpeed Insights
  • Lighthouse

今回は、「CI 上で定期的に計測ツールの API を叩いて、結果を Slack に通知する」ということがやりたい

WebPageTest は非同期的に計測が行われるため、テスト中のステータスをチェックする testStatusAPI のポーリング処理を実装するのが面倒なためパス
PageSpeed Insights は結果が JSON 形式で、Slack に通知する時に加工する必要がありそうなのでパス(HTML で見れると良い)
↑ でもちゃんと調べてないので、いいやり方あるかもしれない

今回は、Chrome DevTools で良く使っており、CircleCI 上で簡単に実行できそうな Lighthouse を使うことにした

ただ、将来的には WebPageTest を使ってがっつり計測するのがいいのかなと思う
そもそも Lighthouse は WebPageTest に組み込まれているので


CircleCI の定期実行の設定

## .circleci/config.yml
---
workflows:
  version: 2
  nightly:
    triggers:
      - schedule:
          cron: '00 0 * * 1'
          filters:
            branches:
              only:
                - master
    jobs:
      - lighthouse

時間は UTC なので、上記だと午前 9 時に実行される


CircleCI の Lighthouse 実行の設定

CI 上でヘッドレスブラウザで Lighthouse を動かす想定なので、必要なプラグインを用意してあげる必要がある

以下を参考に設定 github.com

yarn add -D lighthouse
## .circleci/config.yml
version: 2
jobs:
  lighthouse:
  ...
      - run:
          name: Lighthouse
          command: ./node_modules/.bin/lighthouse --chrome-flags="--headless" --output-path=./lighthouse-results.html https://github.com
  ...

当然ブラウザも必要なので、Chromium をインストールする
CI でフロントエンドのツールを動かす時は Node.js がインストールされた Docker イメージを使って実行しているので、以下のように Dockerfile にコマンドを付け足す

## Dockerfile
RUN apt-get install chromium

Lighthouse が動かず苦戦。。

たぶん上記の設定で動く人は動くのだと思うが、私の場合は全然動かなったので、動かすまでにやったことを備忘録として書いておく

謎エラー

method <= browser ERR:error Browser.getVersion  +1ms

Chromium 起因で起きてるのは、間違いないのだが調べても原因不明

以下を参考にヘッドレスではなく、フルブラウザモードで試してみる github.com

chromium-browser 入らない問題

こんな感じで入れてみる

## Dockerfile
RUN apt-get install chromium-browser xvfb

docker buildすると以下のエラー

E: Package 'chromium-browser' has no installation candidate

え?無いの?

エラーを調べると同じエラーに遭遇している人を発見 stackoverflow.com

wgetChromedeb パッケージをダウンロードして、インストールするようにした

## Dockerfile
RUN apt-get install -yq --no-install-recommends \
    gconf-service libasound2 libatk1.0-0 libcairo2 libcups2 libfontconfig1 libgdk-pixbuf2.0-0 \
    libgtk-3-0 libnspr4 libpango-1.0-0 libxss1 fonts-liberation libappindicator1 libnss3 \
    lsb-release xdg-utils
RUN wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
RUN dpkg -i google-chrome-stable_current_amd64.deb; apt-get -fy install

ヘッドレスモードじゃないと動かない

ヘッドレスモードを解除すると以下エラーになる

ChromeLauncher:error [0127/120527.937238:ERROR:nacl_helper_linux.cc(310)] NaCl helper process running without a sandbox!
ChromeLauncher:error Most likely you need to configure your SUID sandbox correctly

やっぱりヘッドレスモードにする(--chrome-flags="--headless"

sandbox のエラー

Running as root without --no-sandbox is not supported. See https://crbug.com/638180.

--chrome-flags="--no-sandbox"を付けることにする

ようやく動いた

今回使用した Dockerfile です github.com For Puppeteer って所は、今回関係ないので無視してください

CircleCI で Lighthouse を実行している部分です

## .circleci/config.yml
version: 2
jobs:
  lighthouse:
  ...
      - run:
          name: Lighthouse
          command: ./node_modules/.bin/lighthouse --chrome-flags="--headless --no-sandbox" --output-path=./lighthouse-results.html https://github.com
  ...

Slack へ通知

## .circleci/config.yml
version: 2
jobs:
---
- store_artifacts:
    path: ./lighthouse-results.html
- run:
    name: Send to Slack
    command: |
      PAYLOAD=`cat << EOF
      {
        "attachments": [
          {
            "pretext": "Lighthouse result",
            "text": "https://$CIRCLE_BUILD_NUM-XXXXXXXXX-gh.circle-artifacts.com/0/root/project/lighthouse-results.html",
          }
        ]
      }
      EOF`
      curl -X POST -d "payload=$PAYLOAD" $SLACK_WEBHOOK_URL

store_artifacts で Lighthouse の結果を CircleCI 上に保存し、Artifacts というタブから Web 上で見れるようにする

後は、https://$CIRCLE_BUILD_NUM-XXXXXXXXX-gh.circle-artifacts.com/0/root/project/lighthouse-results.htmlのように Artifacts の URL を作って Slack に送信する
CIRCLE_BUILD_NUM は、CircleCI にデフォルトで定義してある現在のビルド番号を持つ環境変数
XXXXXXXXX の部分は、CircleCI のリポジトリごとに振られる番号が入るっぽいので、1 回 Artifacts を作って確かめてみてください


動作確認

CircleCI 全体のコードはこちらにあります github.com

CircleCI の定期ジョブで lighthouse ジョブが実行されると

CircleCI の Artifacts タブに Lighthouse の実行結果 HTML が保存され f:id:kurosame-th:20190129154028p:plain

もちろん結果も見れて f:id:kurosame-th:20190129154140p:plain

実行結果 HTML が Slack に通知されました f:id:kurosame-th:20190129161021p:plain