patorashのブログ

方向性はまだない

Amazon Linux 1でImageMagick6系の最新版を入れる

gem rmagick 3.0.0を入れようとしたら、bundle installのときにImageMagickのバージョンが古いという理由で落ちました。というわけで、ImageMagickのアップデートやります!

qiita.com

ここに書かれてることをやれば概ね大丈夫かと思いますが、最初CentOS6系のリポジトリを入れる理由がよくわからなかったので、そこを無視して色々試していたのですが、依存性解決が全然うまくいかず…。

さらにググり続けることに。今度は依存性解決できなかったsoファイル名とかで以下の記事がヒット。

qiita.com

あ〜、soファイルがあるのがCentOS6系のリポジトリなのか…と理解し、ようやく追加。

$ sudo touch /etc/yum.repos.d/CentOS-Base.repo
$ sudo vi  /etc/yum.repos.d/CentOS-Base.repo
[base]
name=CentOS-6 - Base
mirrorlist=http://mirrorlist.centos.org/?release=6&arch=x86_64&repo=os
gpgcheck=1
enabled=0
gpgkey=http://mirror.centos.org/centos/RPM-GPG-KEY-CentOS-6

古いImageMagickを削除し、新しいImageMagickをインストールしました。

$ sudo yum remove ImageMagick ImageMagick-libs ImageMagick-devel
$ sudo yum install ImageMagick6 ImageMagick6-libs ImageMagick6-devel --enablerepo=remi,epel,base

その後、bundle installでrmagick 3.0.0のインストールが成功しました。

Amazon Linux 1にPostgreSQL 11をインストールする

まず、PostgreSQLyumリポジトリを追加します。 参照先は以下。

yum.postgresql.org

Amazon Linux 1はRedHat Enterprise Linux6互換(?)のため、そのURLをコピーして追加します。そのあと、/etc/yum.repos.d/pgdg-11-redhat.repoに記載されているURLをsedで置換します。

$ sudo yum install https://download.postgresql.org/pub/repos/yum/11/redhat/rhel-6-x86_64/pgdg-redhat11-11-2.noarch.rpm
$ sudo sed -i "s/rhel-\$releasever-\$basearch/rhel-6.10-x86_64/g" "/etc/yum.repos.d/pgdg-11-redhat.repo"

あとは、インストールするだけ。クライアントだけ欲しかったのでpostgresql11, postgresql11-develのみを指定しています。

$ sudo yum install -y postgresql11 postgresql11-devel

psqlのバージョンを確認して終わり。

$ psql --version
psql (PostgreSQL) 11.2

2019-03-08 追記

gem pgのインストールでこけた…。pg_configが見つからないと言われた。

Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    current directory: /home/heroku/.rbenv/versions/2.5.3/lib/ruby/gems/2.5.0/gems/pg-1.1.4/ext
/home/heroku/.rbenv/versions/2.5.3/bin/ruby -r ./siteconf20190307-22703-1t1ti1k.rb extconf.rb
checking for pg_config... no
No pg_config... trying anyway. If building fails, please try again with
 --with-pg-config=/path/to/pg_config
checking for libpq-fe.h... no
Can't find the 'libpq-fe.h header
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
# .bash_profile
need configuration options.

/usr/pgsql-11/binにPATHを通せばいいことがわかった。

bitarts.jp

.bash_profileを編集する。

PATH=$PATH:/usr/pgsql-11/bin
export PATH

source ~/.bash_profileしてから、bundle installしたら成功した。

database.ymlのschema_search_pathにpublicを書いたらあかん

PostgreSQL 11にアップデートしようとしていたときに起きました。

docker imageを11に変更して、CircleCIでテストを流そうとしたところ、bin/rake db:structure:loadで失敗しました。

psql:/home/circleci/workspace/db/structure.sql:15: ERROR:  schema "public" already exists

該当行はCREATE SCHEMA public;をやろうとしていたのですが、publicは最初から存在するので、そりゃエラーになるわな…と。

今まで出てなかったのに、なんでだろう?と思いつつ、Railsのコードを追ってみました。

rails/postgresql_database_tasks.rb at b366be3b5b28f01c8a55d67a5161ec36f53d555c · rails/rails · GitHub

search_pathに値があれば、--schema=値を設定するようだったので、database.ymlを確認したら、schema_search_path: publicと書いていました…。もういつ書いたのかほとんど覚えてないけれど。これをコメントアウトしてからrake db:migrateを行い、db/sctructure.sqlを再生成したところ、CREATE SCHEMA public;の表記がなくなりました。

schema_search_pathにはpublic以外のスキーマを作った時に、それらを指定したほうがよさそうです。

CircleCI ユーザーコミュニティミートアップにリモート参加した

3/5にあったCircleCIのミートアップにリモートで参加させてもらいました。

イベントのConnpassはこちら。

circleci.connpass.com

ちょうどCircleCIをPerformancePlanに移行したりしていたので、CircleCI Japanの方からZoomで参加しませんか?とtwitterのDMで連絡を頂きました。

patorash.hatenablog.com

せっかくの機会なので、参加させていただくことに。

この時に、社外の方も参加募集したのですが、悲しいことに集まらなかったので、社内のメンバー4人で参加しました。

皆同じような課題を抱えていた

弊社でもJenkinsでの並列テストを経た後にCircleCIの導入をしているので(1.0の頃から)、「あ〜、わかるわ〜」という感じでした。

うちのテスト事情の遷移

CI環境はCircleCIになるまではほぼ関与してないので、記憶で書く(間違ってるかも)

  1. ローカル環境でrspecをparallel_testsで実行する。テストが肥大化して30分くらいかかるようになって辛くなる。
  2. Jenkinsを導入。会社で空いていたMacでテストを実行するように。テストが肥大化して1時間くらいかかるようになる。
  3. Jenkinsで並列処理をするように。そのため、Mac miniを複数台購入した。テスト実行されると同時にbundle installが実行され、ネットワークが遅くなる等、問題が出始める。Mac miniの構成管理はChefとかでやってたはず。20〜40分くらいかかっていたかと思う。
  4. 並列数に限界を感じ、AWSで都度EC2を立ち上げてテストするように。スポットインスタンスが安いリージョンを探し求めていた。
  5. いろんなCIサービスを検討し始める。CircleCIのbundle installの速さがすごい!みたいなことで盛り上がっていた記憶がある。
  6. CircleCIへ移行!
  7. CircleCI 2.0に移行!(多分このあたりから自分がやった)
  8. CircleCIでparallel_testsを使う!
  9. CircleCI + parallel_tests + knapsack proで爆速化!
  10. CircleCIをPerformancePlanに移行!

高速化には色々と取り組みました。

patorash.hatenablog.com

patorash.hatenablog.com

課題を解決した後に入社した若者たち

今回の参加者は私以外は入社2年以内の若者が3名だったのだけれど、上記のような歴史を知らないので、

若者たち「Jenkinsってなに???🤔」

ってなっていたので、発表は聞きつつもCI導入の歴史を説明したりしました。まぁいい機会だったのかもしれないですねぇ。

リモート参加してみて…

現地はピザや飲み物があったりしたみたいで羨ましかったですが、実はこちらもピザを食べてました!

発表後のネットワーキングの時間とかはリモートだと参加できないので、やっはり現地がいいなぁと。まぁ岡山だとなかなか厳しいんですが。機会があったら現地参加したいと思います!

Ruby2.6の関数合成で遊んでみた

Ruby 2.6が出て2ヶ月が経過しているものの、まだ触ってなかったので新機能を調べていたら、関数合成ができるようになっていたのを知った。

techlife.cookpad.com

これを使えば多少の加工だったらmapを2周したりしなくて済むんじゃないか?と思って実験してみた。

Procを先に定義するパターン

先に定義してあるので、mapしているところは見易いしわかりやすい。けれど、やっぱり先に定義したくない。

a = -> (x) { x.upcase }
b = -> (x) { x.strip }
["abc   ", "   def", " ghi "].map(&(a >> b))
=> ["ABC", "DEF", "GHI"]

Procを定義せずにto_procを使うパターン

先に定義せずにすると、こうなる。しかし、これはこれでto_procがやかましい…。

["abc   ", "   def", " ghi "].map(&(:upcase.to_proc >> :strip.to_proc))
=> ["ABC", "DEF", "GHI"]

injectを使うパターン

配列に定義したsymbolをto_procしてinjectで合成する。カッコイイ。けれど、長い。

["abc   ", "   def", " ghi "].map(&([:upcase, :strip].map(&:to_proc).inject(&:>>)))
=> ["ABC", "DEF", "GHI"]

理想の書き方

動かないけれど、理想の書き方は…

["abc   ", "   def", " ghi "].map(&(:upcase >> :strip)) # => Not working...

みたいな感じがいいなぁと思う。

Array型をオープンクラスしてcompose関数とかを追加するのがいいのかもしれない。

class Array
  def compose
    self.map(&:to_proc).inject(&:>>)
  end
end
["abc   ", "   def", " ghi "].map(&([:upcase, :strip].compose))
=> ["ABC", "DEF", "GHI"]

やらんけど。

1つのspecファイルをparallel_testsで並列処理する方法

parallel_testsでテストの並列化していくのが好きなのですが、まだまだ課題に思っていたことがありました。それは、テスト対象ファイルがCPUコア数より少ない場合、フルに並列化できないこと…。

つまり、CPUコア数が4つで、テスト対象ファイルが1つの場合、1つだけでテストして、残りの3つは何もせずに終了するのです。しかもその1ファイルのテストにかかる時間が10分とかあると長くて辛いし勿体無い!

rspecでは、テストが落ちたときに行番号指定でテスト実行できるので、それを応用してできるんじゃないかな?と思い、やってみました。

仮説の検証

rspecは行番号指定のテストを複数渡して動くか?

rspecに一度に行番号指定のファイル一覧を渡してテストができるかどうかを検証してみたところ、成功。

paralle_testsは行番号指定のテストを複数渡して動くか?

次に、parallel_testsに対して一度に行番号指定のファイル一覧を渡してテストができるかどうかを検証してみたところ、失敗…。これは調べてみたところ、parallel_testsのテストケースの分割方法が、デフォルトでファイルサイズのため、ファイルの有無のチェックを行なっているためでした。--group-byオプションをfoundに指定したところ、成功!これでとりあえずparallel_testsでテストケース毎に分散実行できることがわかったので、あとはテストケースを抽出するだけ。

実装する

テストケースを抽出する

テストが落ちたときには行番号付きで落ちたテスト一覧が表示されるので、なんかあるだろうと考えていたところ、テスト結果の出力をjson形式にして、--dry-runオプションをつけたら、実際にはテストを実行せずにテストケースを行番号付きで取得できたので、これを加工することにした。

bin/rspec --dry-run --format json --out tmp/test-results.json spec/models/user_spec.rb

ちなみに、標準出力でjsonを取得しようとしたところ、simplecovが実行されてjson以外の文字列が混ざってしまってjsonにならなかったため、--outオプションでファイルにした。

テストケースのjsonをパースする

テストケースをparseして、テストファイル名と行数を結合すればOK、と思っていたのだけれど、it_behaves_likeを使ってテストの共通化をしているところは、テストファイル名と行番号がshared_examples_forを定義していたところになってしまっていて、ダメそうだった。以下のfile_pathline_numberのところ。

{
  "version": "3.8.0",
  "examples": [
    {
      "id": "./spec/models/user_spec.rb[1:1:3:1]",
      "description": "エラーになること",
      "full_description": "User エラーになること",
      "status": "passed",
      "file_path": "./spec/models/user_spec.rb",
      "line_number": 106,
      "run_time": 2.0e-06,
      "pending_message": null
    },
    {
      "id": "./spec/models/user_spec.rb[1:1:4:1]",
      "description": "並び順が1以下だとエラーになること",
      "full_description": "User behaves like display_order_validation 並び順が1以下だとエラーになること",
      "status": "passed",
      "file_path": "./spec/support/display_order_validation.rb",
      "line_number": 2,
      "run_time": 2.0e-06,
      "pending_message": null
    },
  ],
  # 略
}

代わりに、idがファイル名と個別にテストケースを認識するための文字だと思ったので([1:1:3:1]のやつ)、これを使ってみたところ、成功。

bin/以下にrubyスクリプトを作成

今回は、bin/parallel_rspec_each_exampleというファイルを作成した。実行権限を与えておくこと。

touch bin/parallel_rspec_each_examples
chmod +x bin/parallel_rspec_each_examples

そして、ファイルには以下の内容を記述する。

parallel_testsを使ってテストケース単位でテストを分割して実行する

使う

実際に使ってみる。

テストファイルを1つ指定して並列実行

bin/parallel_rspec_each_examples spec/models/user_spec.rb

テストファイルを複数指定して並列実行

bin/parallel_rspec_each_examples spec/models/user_spec.rb spec/models/group_spec.rb spec/models/article_spec.rb

並列数を指定して並列実行

bin/parallel_rspec_each_examples -n 2 spec/models/user_spec.rb

応用編

CircleCIで落ちたテストの一覧を取得する仕組みと組み合わせる

過去に書いた、落ちたテストの取得方法は以下の記事。

patorash.hatenablog.com

ちなみに以下のはshellがfishの場合なので、他のshellを使っている場合はxargsの指定が異なるかもしれません。

bin/rails runner script/circleci_failed_spec_files.rb -j test | xargs bin/parallel_rspec_each_examples

これで、落ちたテストが1ファイルであろうと、並列実行でテストされるのでローカルでの検証時間が短くて済みます!

Capybara3にアップデートしたらハマったことをまとめる

長いことCapybara2系を使っていた。ようやくCapybara3系にアップデートする暇が取れたので作業に着手したら、めっちゃテスト落ちた…。ので、その原因と対策を書いていく。

whitespaceが含まれるようになった。

以下のブログ記事にも書かれていますが、半角スペースや改行コードが戻り値に含まれるようになりました。

journal.sooey.com

ですので、passのように、改行コードを含めて記述しなければなりません。

expect(page).to have_content 'foo bar' # => fail
expect(page).to have_content "foo\nbar" # => pass

オマケ

Capybara.default_normalize_ws = trueにすれば、今までと同様の評価になる模様。だけれど、Capybara4では消されるっぽいので、上記の対応をしておいたほうがよさそうです。

page.allがdefault_wait_timeまで待つようになった

これは元々な気もするけれど、Capybara2系のときには起きなかった事で、table要素の中身を掘ってるときに起きました。

contents  = page.all('table tbody tr').map do |row|
              row.all('th,td').map do |cell|
                if cell.all('li').count > 0
                  cell.all('li').map(&:text)
                else
                  cell.text
                end
              end
            end

テーブルのセルの中でliタグがあれば、それは配列として取得、みたいな処理をしているのですが、この処理が全く終わらなくなりました。実際には進んではいたみたいなのですが、どうもif cell.all('li').count > 0のところで、liの存在チェックをCapybara.default_max_wait_timeまで待っているようでした。mapで処理しているので、100セルあればCapybara.default_max_wait_time * 100だけ待たなければなりません…。Capybara2のときは即時評価だったと思うんですが…。なんにしろこれはマズいので、allのところのwaitを変えました。

contents  = page.all('table tbody tr').map do |row|
              row.all('th,td').map do |cell|
                if cell.all('li', wait: 0).count > 0 # => 1秒たりとも待たない!
                  cell.all('li').map(&:text)
                else
                  cell.text
                end
              end
            end

原因不明のエラー

落ちてたテストを直したら再現しなくなったのでちょっとわからないのですが、以下のissueと同じエラーが出ていました。

github.com

どうもCapybara3系とTimecopを使っている場合で起きる、とか。未来に行ったりするとよくないとかは書かれていましたが、該当のテストではそんなこともしてませんでした。落ちたテストの次のテストとの間でエラーが起きるのだろうか…など考えていましたが、もう起きなくなったので、とりあえず放置。また再現したらここに書いていることを思い出すためのメモです、これは。

苦労したこと

何気にシングルクォートで囲んでいるやつをダブルクォートに変換してさらに改行コードを入れて…というのは骨が折れる作業だった。大概は目で確認しながら正規表現で置換していったけれど。とはいえ、ずっと気になっていたバージョンアップができてよかった。