『NO HARD WORK!: 無駄ゼロで結果を出すぼくらの働き方』を読んだ

『NO HARD WORK!: 無駄ゼロで結果を出すぼくらの働き方』を数日前に読了した。

DHH の何がすごいかという一面として CTO として長年にわたって会社経営にも携わっていて、Rails が切り出されるプロダクトとしての Basecamp の面ではなく、会社としての Basecamp について CEO のジェイソン・フリードとの共著で記されている。書籍にも記されているとおりすべてを真似すればハッピーになるというものではないと思うけれど、長年小さなチームで成果を出して自分たちの幸せを維持するというそのあり方は何かしらの参考になると思う。読んでいて思い起こしたのはエクストリームプログラミングの初版で当時まだ世界的に馴染みのない開発プラクティスが紹介されたように、本書でも世界的に未開的であろういくつかのプラクティスが紹介されているが、それらを組織にあわせてカスタマイズ導入を検討してみると面白い変化を作れるかもしれないな、、、とか現在と未来を考えることができるとても良い書籍だった。

ちなみにこの書籍は『It Doesn't Have to Be Crazy at Work』が原書となり、Ginza.rb で yahonda さんと masa_iwasaki さんに邦訳があることを教えてもらった。

おかげで面白い本を母国語で読むことができました。ありがとうございました。

RuboCop に JUnit フォーマッタを組み込んだ

JUnitXML フォーマッタが CircleCI 利用時に有用だけれど gem の方がメンテナンスされていなくて、コアで巻き取ってもらえないかというイシューに対応した。

github.com

次にリリースされる予定の RuboCop 0.80 から --format junit オプション (あるいは --format ju) で使えるようになる。

% rubocop --format junit
<?xml version='1.0'?>
<testsuites>
  <testsuite name='rubocop'>
    <testcase classname='example' name='Style/FrozenStringLiteralComment'>
      <failure type='Style/FrozenStringLiteralComment' message='Style/FrozenStringLiteralComment: Missing frozen string literal comment.'>
        /tmp/src/example.rb:1:1
      </failure>
    </testcase>
    <testcase classname='example' name='Naming/MethodName'>
      <failure type='Naming/MethodName' message='Naming/MethodName: Use snake_case for method names.'>
        /tmp/src/example.rb:1:5
      </failure>
    </testcase>
    <testcase classname='example' name='Lint/DeprecatedClassMethods'>
      <failure type='Lint/DeprecatedClassMethods' message='Lint/DeprecatedClassMethods: `File.exists?` is deprecated in favor of `File.exist?`.'>
        /tmp/src/example.rb:2:8
      </failure>
    </testcase>
  </testsuite>
</testsuites>

これまでの JUnit フォーマットプラグインを見たところでは以下のふたつがあり、今回は報告のあったのと利用量が多そうだった前者をベースに組み込んでいる。

master ブランチにマージされているので、次の RuboCop のリリースで入る予定。個人的には JUnit という古来からのフォーマットの活用事例ということは、コアでメンテナンスして良いという大きな判断要素だった。

本体でサポートすることになったので、gem 依存で使っているアプリケーションでは Gemfile 記載の依存を減らせるかなと思います。一方で実装としては Ruby 2.8.0-dev (Ruby 3.0) で添付から外された REXML に依存するので、以降内部的には REXML への依存が入ります。

書籍「エクストリームプログラミング」の読みかた

勤務先で話題になっていた書籍『エクストリームプログラミング』について、メンバーに話したことを書き残しておきます。

TL;DR としては、書籍『エクストリームプログラミング』は初版の内容を踏まえた方が第二版を読み進めやすいかもしれないといったものです。

現代で書籍『エクストリームプログラミング』というと kdmsnr さんが訳した第二版新訳がピックアップされたりするのですが、Kent Beck が記した『Extreme Programming Explained: Embrace Change』には初版と第二版があり、第二版は Kent Beck が初版からの5年間の人生でリライトされた (2nd Edition の付加以外) 同じタイトルでの別物です。

初版と第二版の原書邦訳を交えてタイムラインにすると次のとおりです。

例えるなら新約聖書旧約聖書を背景にしているような感じで、旧約 (初版) を背景として知っておくと旧約 (初版) から新約 (第二版) への繋がりを理解できると思います。

エクストリームプログラミング』の初版は Kent Beck が Martin Fowler や Ron Jeffries らと取り組んだ C3 プロジェクトでのソフトウェア開発の実践での知見が色濃く出ており、第二版はその裏にある思想などが掘り下げてられています。第二版だけ読んでも抽象的でとっつきづらさがある場合は、初版を読んだ具象的な知識を元に第二版を読む方が Kent Beck の 5 年間の流れとともに理解が進むかもしれません。

また『エクストリームプログラミング』の初版には用語集がついており、エクストリームプログラミングにおけるプログラマーの定義も「分析、設計、テスト、プログラミング、結合を行うチーム内の役割」と記されています。これはエクストリームプログラミングを理解するのに大事なことだと思うので特筆しておきます。

最後に引用した書籍の Amazon へのリンクをつけておきます。

『Extreme Programming Explained: Embrace Change』 (1999年)

www.amazon.co.jp

『XPエクストリーム・プログラミング入門―ソフトウェア開発の究極の手法』 (2000年)

[asin:489471275X:detail]

『Extreme Programming Explained: Embrace Change, 2nd Edition』 (2004年)

『XPエクストリーム・プログラミング入門―変化を受け入れる』 (2005年)

エクストリームプログラミング』 (2015年)

RubyKaigi 2020 へのプロポーザルが通過した

ということで RubyKaigi 2020 に pocke さんと登壇します。去年は LT での登壇だったので、本編登壇は 2018 年以来 2 年ぶり 2 回目です。よろしくお願いします。

rubykaigi.org

Code Climate Test ReporterとSimpleCov 0.18で起きるエラーを回避する

昨日あたりから RuboCop の master で CI が落ちていて、見てみたら Code Climate の cc-test-reporter でエラーが起きていることが原因だった。

$ #!/bin/bash -eo pipefail
./tmp/cc-test-reporter before-build
COVERAGE=true bundle exec rake spec
./tmp/cc-test-reporter format-coverage --output tmp/codeclimate.$CIRCLE_JOB.json
Starting test-queue master (/tmp/test_queue_157_5140.sock)

==> Summary (2 workers in 36.1115s)

    [ 1]                         8753 examples, 0 failures, 9 pending       254 suites in 36.1034s      (pid 160 exit 0 )
    [ 2]                         6772 examples, 0 failures, 2 pending       239 suites in 36.1054s      (pid 161 exit 0 )

Coverage report generated for RSpec, rspec-1, rspec-2, rspec-fork-163, rspec-fork-164, rspec-fork-165 to /home/circleci/project/coverage. 21266 / 21459 LOC (99.1%) covered.
Error: json: cannot unmarshal object into Go struct field input.coverage of type []formatters.NullInt
Usage:
  cc-test-reporter format-coverage [coverage file] [flags]

Flags:
      --add-prefix string   add this prefix to file paths
  -t, --input-type string   type of input source to use [clover, cobertura, coverage.py, excoveralls, gcov, gocov, jacoco, lcov, simplecov, xccov]
  -o, --output string       output path (default "coverage/codeclimate.json")
  -p, --prefix string       the root directory where the coverage analysis was performed (default "/home/circleci/project")

Global Flags:
  -d, --debug   run in debug mode


Exited with code exit status 255

https://circleci.com/gh/rubocop-hq/rubocop/83619

調べたところ SimpleCov 0.18 のリリースがリリースされており、cc-test-reporter がうまく組み合っていないというイシューが開かれていた。

github.com

PR がパスしているかどうかが分からないのは不便なので、対応までのワークアラウンドとして Gemfile に SimpleCov 0.18 未満を指定して乗り切ることにした。

-gem 'simplecov', '~> 0.10'
+# Workaround for cc-test-reporter with SimpleCov 0.18.
+# Stop upgrading SimpleCov until the following issue will be resolved.
+# https://github.com/codeclimate/test-reporter/issues/418
+gem 'simplecov', '~> 0.10', '< 0.18'

github.com

キーワード引数の分離への対応にRuby 2.8.0-devを使う

先日のパッチ会で kamipo さんにもらったアドバイスを書き残しておく。

TL;DR としては表題そのまま。キーワード引数の分離への対応にRuby 2.8.0-devを使うというもの。

Ruby 2.7.0 を使ってキーワード引数の分離への警告のみでそれを抑制しようとする場合は、スーパーハードモードルビーとパッチ会で呼ばれた変更箇所の特定が難しいケースになる場合がある。

スーパーハードモード (Ruby 2.7.0)

Ruby 3.0 に向けてキーワード引数の分離が必要になる場合は、Ruby 2.7.0 を使うと以下のような警告が表示される。

% ruby -v
ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-linux]

% bundle exec rake
(略)

/home/vagrant/.rvm/gems/ruby-2.7.0/bundler/gems/rails-80e72c5eb7d4/activerecord/lib/active_record/migration.rb:907: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/home/vagrant/src/oracle-enhanced/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb:199: warning: The called method `create_table' is defined here

まず warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call となっている Active Record のコードの場所を示す。

    def method_missing(method, *arguments, &block)
      arg_list = arguments.map(&:inspect) * ", "

      say_with_time "#{method}(#{arg_list})" do
        unless connection.respond_to? :revert
          unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method)
            arguments[0] = proper_table_name(arguments.first, table_name_options)
            if [:rename_table, :add_foreign_key].include?(method) ||
              (method == :remove_foreign_key && !arguments.second.is_a?(Hash))
              arguments[1] = proper_table_name(arguments.second, table_name_options)
            end
          end
        end
        return super unless connection.respond_to?(method)
->      connection.send(method, *arguments, &block) # ここが `activerecord/lib/active_record/migration.rb:907`
      end
    end
    ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)

次に warning: The called methodcreate_table' is defined here` となっている Oracle enhanced adapter のコードの場所を示す。

->      def create_table(table_name, id: :primary_key, primary_key: nil, force: nil, **options) # ここが `oracle_enhanced/schema_statements.rb:199`
          create_sequence = id != false
          td = create_table_definition(
            table_name, **options.extract!(:temporary, :options, :as, :comment, :tablespace, :organization)
          )

結論としてはどちらもキーワード引数の分離への変更対象ではない。メソッドの定義側も呼び出し側いずれのコードも期待から外れたものではなかった。

これが kamipo さんの "warningみてもどこ直せばいいかまったくといっていいほどわからんからな" の一例である。

次に Ruby 2.8.0-dev を使うと警告ではなくエラーになることでバックトレースが見えるイージーモードに切り替えてみよう。

イージーモード (Ruby 2.8.0-dev)

Ruby 2.8.0-dev を使った結果は以下となる。

Failures:
  1) OracleEnhancedAdapter schema dump tables should not include ignored
  table names in schema dump
     Failure/Error:
               def create_table(table_name, id: :primary_key, primary_key: nil, force: nil, **options)
                 create_sequence = id != false
                 td = create_table_definition(
                   table_name, **options.extract!(:temporary, :options, :as, :comment, :tablespace, :organization)
                 )

                 if id && !td.as
                   pk = primary_key || Base.get_primary_key(table_name.to_s.singularize)

                   if pk.is_a?(Array)

     ArgumentError:
            wrong number of arguments (given 2, expected 1)
     # ./lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb:199:in `create_table'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/migration.rb:907:in `block in method_missing'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/migration.rb:875:in `block in say_with_time'
     # /home/travis/.rvm/rubies/ruby-head/lib/ruby/2.8.0/benchmark.rb:293:in `measure'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/migration.rb:875:in `say_with_time'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/migration.rb:896:in `method_mi
     # ./spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb:24:in `block in create_test_posts_table'
     # ./spec/spec_helper.rb:121:in `instance_eval'
     # ./spec/spec_helper.rb:121:in `block (2 levels) in schema_define'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/migration.rb:884:in `suppress_messages'
     # ./spec/spec_helper.rb:120:in `block in schema_define'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/schema.rb:50:in `instance_eval'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/schema.rb:50:in `define'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/schema.rb:46:in `define'
     # ./spec/spec_helper.rb:119:in `schema_define'
     (以下略)

Ruby 2.7.0 で表示されている警告は ArgumentError からのバックトレースの以下2行となる。そして、Ruby 2.8.0-dev では警告ではなくエラーになることでその先もバックトレースとして表示されている。

     ArgumentError:
            wrong number of arguments (given 2, expected 1)
     # ./lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb:199:in `create_table'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/migration.rb:907:in `block in method_missing'

これが示すことはバックトレースで問題の箇所を探索することができるようになるということ。この例だとバックトレース中に示されている spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb:24 が修正対象となるものだった。

これはテストコード中の以下を示している。

  def create_test_posts_table(options = {})
    options[:force] = true
    schema_define do
->    create_table :test_posts, options do |t| # ここが `oracle_enhanced/schema_dumper_spec.rb:24`
        t.string :title
        t.timestamps null: true
      end
      add_index :test_posts, :title
    end
  end

変更するべきはこのテストコードで次のようになる。

diff --git a/spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb b/spec/active_record/connec
index f3b1df0..8330a95 100644
--- a/spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb
+++ b/spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb
@@ -21,7 +21,7 @@ describe "OracleEnhancedAdapter schema dump" do
   def create_test_posts_table(options = {})
     options[:force] = true
     schema_define do
-      create_table :test_posts, options do |t|
+      create_table :test_posts, **options do |t|
         t.string :title
         t.timestamps null: true
       end

これによって解決している PR が以下です。

github.com

このように、キーワード引数の分離への対応には Ruby 2.8.0-dev を使った方がよりヒントを得られるケースがあるので、困った時は rbenv install 2.8.0-dev などで master の Ruby を使ってみることを考慮してみると良いでしょう。

この問題に2ヶ月間取り組んだ kamipo さんからの知見に感謝します。


次回のパッチ会は 2020年2月27日(木)です。

blog.agile.esm.co.jp

Ginza.rb 第79回

Ginza.rb 第79回 Rails6の新しい定数自動読み込みZeitwerkのコードを読もう!に参加した。会場はメドピアさん。

ginzarb.doorkeeper.jp

表題のとおり今回は Zeitwerk がテーマ。余談だが自分は発音できないのでカタカナ読みでツァイトヴェルクと読んでいたが、それでも読みづらいので Rails コミュニティで略称として使われている zloader (ゼットローダー) の方で読んでいたりする。

オートローダーとは Railsrequire を書かなくてもクラスが使える機構を支えているものだが、Ruby 標準の autoload を Rails が拡張した autoload が Rails 5.2 以前に択一だったもので、Rails 6 で追加されたオートローダーが Zeitwerk となる。従来のオートローダーは classic というもので Active Support が提供している。なお、Rails では Active Support が Zeitwerk に依存してる

autoload はクラスが見つからなかったときにファイルをロードするが、Zeitwerk はその逆でファイル名をもとにクラスをロードする。なのでファイル名とクラス名のマッピングが不確かだと NameError が起きるのでそのあたりはひとつの注意点としましょうとなっている。例としては html_parser.rb は HtmlParserマッピングされるため HTMLParser としたい場合は infrection を使ったマッピングを行う。

github.com

また Zeitwerk はスレッドセーフな実装になっているので、autoload を使っていたときにレースコンディションでエラーになるようなケースが解消されるなどのメリットがあるとのこと。

実装として面白かったのはクラスの reload だが、自分だったら愚直に load メソッド (require と異なり同じファイルを読み直す) を使うのが第一感となるが、require 済みのファイルパスを保持する $LOADED_FEATURES から対象のパスを reject! で破壊的に除いて 再び setup を実行するとかマジかと思って面白かった。このコードは面白かった。印象的なことだったので2度言った。

あと TracePoint が実装として使われていたりもするライブラリなので、TracePoint ファンの人は読んでみると面白いと思う。

今回 willnet さんの主導で README を読んで実装のコード解説に入っていたけれど、時間内にしゅっと本筋のコードを眺めてどういったものかを解説する様はまさに匠の技だった。Zeitwerk への理解が深まりました。ありがとうございました。