オープンソースプロジェクトでバグを修正する方法 : あるNodeJSモジュールへの修正を例に

(訳注:2016/3/3、頂いたフィードバックをもとに記事を修正いたしました。)

オープンソースプロジェクトでバグを見つけたとします。まずは、慌てずに落ち着きましょう。これは実によくあることです。ソフトウェアは人間の手で書かれるし、人間はミスを犯すものです。

または、こんなふうに考えるかもしれません。「やったね、バグの修正は大好きだ」。さっと現れて、何百万人とまではいかなくても、何千人もが使っているプロジェクトのバグを修正してしまうようなヒーローになりたくない人なんていますか? オープンソースコミュニティに恩返しできたという温かな喜びを感じられる上に、一連のGithub 履歴 1に追加得点を上げられるわけです。

Bug in the "buffer" module
訳:
人気のあるプロジェクト
典型的なバグ

しかしコーディングの初心者にとっては、オープンソースプロジェクトにコントリビュートするなんて恐れ多いことに思えます。私の友人に、最近JavaScriptをWeb上のチュートリアルで学び始めた人がいるのですが、Githubのユーザインターフェースを見ると”途方に暮れる”と言っていました。

また、社会的な側面もあります。それは、”プロジェクト管理者は、私のプルリクエストを受け付けてくれるだろうか? ”とか、”批判されたり、却下されたりしたらどうしよう? ”といった不安です。これはコーディング初心者なら誰もが感じることです。自分と他の熟練者たちと比べて、知識の差を感じているのでしょう。

しかし、臆することはありません。オープンソースに関わる人たちにとって、新規メンバーからのプルリクエストは大歓迎なのです。First Timers OnlyYour First PRといった最近の試みからも分かりますが、手助けさえしてもらえば、誰でもオープンソースプロジェクトにコントリビュートできるのです。

ただ、本稿の目的はちょっと違います。特定のプロジェクトの特定のバグについて詳細な解説をするのではなく、不特定のプロジェクトのあらゆるバグをどのように修正するかを説明したいと思います。問題解決のプロセスを図解する際の例として、“buffer”モジュールの最近のバグを使うことにします。私はこのプロジェクトではほとんど何の経験もないのですが、このバグを1時間で解決しました。

私でさえ経験の無いプロジェクトでバグを修正できるのですから、あなたにもできますよ。

お膳立て

“buffer”とは、ブラウザ用Node.jsのBuffer APIの、JavaScriptのインプリメンテーションです。これを使うことで、APIが存在しないブラウザでも、Bufferオブジェクトに依存するNode.jsモジュールを使えるようになります。(そのかわりにブラウザには、Unit8ArrayArrayBufferのような、バイナリデータを扱う他のAPIがあります)。

まるで神秘的で難解なライブラリのように思えるかもしれませんが、実際のところ”buffer”は1カ月に200万回近くダウンロードされています。というのも、BrowserifyWebpackどちらに対しても必須の依存パッケージだからです。しかし、そんなに知名度の高いプロジェクトであるのにもかかわらず、3カ月以上も解決されずに長引いている問題があることに気が付きました。

Open bug in the "buffer" project
訳:
[object Object]のプロパティの長さを設定できない #79

9月16日
Chrome( 45.0.2454.85 (Official Build) (64-bit) )でBuffer3.5.0が壊れます。
次のような状況です。

Uncaught TypeError: Cannot set property length of [object Object] which has only a getter

これは、bufferが.prototype.lengthundefinedにセットしようとした時に起きます。ここです。https://github.com/feross/buffer/blob/master/index.js#L372

私たちのアプリの新しいバージョンを公開しようとした際に発見しました。後で、ちょっとしたデモをしたいと思います。

(私はbrowserify 11.11.0でbuffer@3.5.0を使用しています)

このバグは9月に公開されましたが、3カ月たった今でもそのままになっています。

これはささやかな障害というわけではありませんし、”buffer”がある特定のブラウザ(特にChrome 43+)やある特定のビルドステップ(特にBabelでエラーになってしまう現象を引き起こす致命的なバグです。多くの人が、この問題を確認しにスレッドを閲覧しました(”+1″するか、”同じ状況が起きています”と報告するなど)。プロジェクトに特化したWebpackのコンフィギュレーションを使って作業するという回避策を提案する人も何人かいました。しかし、修正する人は誰もいませんでした。

そこで私は、このバグに挑戦してみようと決心しました。ちょっと見たところ、皆が言うほどには難しくないように思えたからです。また、なじみのないコードベースでバグを修正するのはいい経験になるだろうし、どうやって解決したかを記録しようと思ったからです。

注:公平のために言いますと、私は以前、”buffer”にコントリビュート したことがあります。ただし、とてもささやかなプルリクエストでしたし、私はこのプロジェクトの経験は無いに等しいと思っています。このバグを取り上げるにあたり、プロジェクトに関していろいろ思い出すために、基本的にゼロから始めなければなりませんでした。2

ステップ1:コードをダウンロードする

バグに取り掛かる前に、コードを構築してテストを実行できるかどうか確認する必要がありました。これは重要なステップです。というのも、先に述べましたが、プロジェクトのテストが自分のマシンの現在のセットアップで実行できることを確認しなければならないからです。

例を挙げてみましょう。このプロジェクトにおける、正しいバージョンのNodeを使っているか? npmのバージョンは正しいか?インストールが必要なグローバルな依存パッケージ(LinterまたはTest Runnerのような)はあるか? 稼動環境はMacか、Linuxか、Windowsか? もしこうしたことを確認せずに、現状のマシンのままでバグを修正しようとすれば、始める前から落とし穴に落ちて終わってしまいます。

注:以下のステップは、JavaScriptに特化していますが、他の言語にも適用できます。典型的なパッケージマネージャやLinterやTest Runnerなど、使っている言語の慣習を知っておくと役に立ちます。

最初にコードをcloneしました。通常、プロジェクトのトップにGitのURLが記載されていて、コミット権が無くてもHTTPSを使えるようになっています。

Where to find the Git URL in the Github UI

訳:このURLをコピー&ペーストします。プロジェクトの管理者でない場合は、このHTTPSを使います。

URLが分かったら、自分の端末に(私の場合はiTermです)、次のように入力します。

git clone https://github.com/feross/buffer.git
cd buffer

これで自分のマシンに、リモートGitリポジトリのmasterブランチを表すコードが複製されました。

ステップ2:テストを実行する

コードを入手できたら、テスト方法を確認する必要があります。
通常この情報はREADME.mdで提供されていますが、今回の場合は“test”という単語で検索したところ、何も見つけられませんでした。CONTRIBUTING.mdもチェックしましたが(コントリビュータに詳細を知らせるドキュメントです)、このプロジェクトには無いようでした。

今回は、言語とエコシステムの知識が役に立つケースです。たまたま、多くのJavaScriptのプロジェクトがnpmで配布されていて、以下のように入力すればインストールしてテストできると分かりました。

npm install
npm test

残念ながら、このケースでは、上記のステップはエラーとなりました。

Test output showing Saucelabs failure
しかしここで、メッセージの最も重要な箇所に気が付きました。以下の部分です。

Error: Zuul tried to run tests in saucelabs, 
  however no saucelabs credentials were provided.

これを読むと、このプロジェクトはブラウザの自動テストを実行する際ZuulSaucelabsを使っていることが分かりました。Saucelabsはブラウザのリモートテストサービスですが、私は、環境変数で定義されたSaucelabsのユーザ名やパスワードは持っていませんでした。そのため、テストが実行できなかったのです。

さらに言いますと、私は今回のケースでSaucelabsを使いたくありませんでした。ただ単に、自分のマシンの自分のブラウザでテストを実行したいだけなのです。そこで、どうしたら実現できるのか考えました。

幸運なことに、多くのJavaScriptのプロジェクトでは、package.jsonのファイルを調べて、"scripts"セクションには他にどんなコマンドがあるのか見ることができます。今回は、package.jsonの中で、以下のものを見つけました。

...
"scripts": {
  "test": "standard && node ./bin/test.js",
  "test-browser": "zuul -- test/*.js test/node/*.js",
  "test-browser-local": "zuul --local -- test/*.js test/node/*.js",
...

なるほど、 test-browser-localですか。これならうまくいきそうです。
そこで、以下のように実行しました。

npm run test-browser-local

そして今度は以下のようなアウトプットになりました。

open the following url in a browser:
http://localhost:62466/__zuul

ChromeでこのURLを開くと、テスト全てがうまくいったと思われるUIがありました。

tests passing in Chrome
やった! 成功です! この時点でようやく、自分のマシンでビルドしてコードをテストできたと確信しました。

注:もし、”単にプロジェクトをテストするために、こんなにいろいろ調べる必要はないのに”と思っているとしたら、それは正しいと言えます。私はまた、テストの過程をドキュメント化するプルリクエストをオープンするのにも時間をかけました。これは、新規コントリビュータがプロジェクトに対して行える、最も価値のある貢献の1つです。というのも、ベテランのコントリビュータはワークフローにすっかり慣れているので、新しい参加者向けの基本的な説明を記載するのを忘れてしまうことがあるからです。

ステップ 3:エラーになるテストを突き止める

次に、問題が再現できたことを確かめるため、エラーになるテストを見つけます(この工程はテスト駆動開発の肝であり、多くのオープンソースプロジェクトの心臓部です)。

今回のケースでは、Githubのスレッドを読み、問題の原因を理解しようと試みました。そこでの議論によると、あるツールのコンビネーション(特にBabelとWebpack)によってJavaScriptモジュールがStrictモードでの実行を強制されているのが、その原因のようでした。”buffer”は明らかにStrictモードでは記述されていないのですが、Chrome 43+では、ブラウザのStrictモードの解釈のためにエラーが起こるのです。

この情報を元に、単純に'use strict'index.jsファイルの先頭に追加すれば問題を再現できると考えました(package.json"main"フィールドをチェックすることで、index.jsがソースファイルだと分かりました。このプロジェクトにおいて唯一最上位にあるJavaSrciptファイルなので、ほぼ明らかでしたが)。

そこで'use strict'index.jsの先頭に追加しました。

adding "use strict" to index.js
驚くことに、Zuulテストページを再度読み込むと、皆が話題にしていたバグが即座に現れました。

test failure
注釈:Githubに報告されていたのと全く同じエラー

(こうなるとテストが実行さえされないことに注意しましょう。ページは緑色でなく黄色で、”失敗 0、通過 0″と表示します。)

この時点で、プロジェクト用のテストスイートを利用してバグの再現はできました。これは次の理由から重要なステップと言えます。

  1. たとえバグを解決できなくても、エラーになるテストを突き止めたことだけでもプルリクエストをオープンすることができます。これによってバグをいかに再現するかという長い議論を皆が省略できます。
  2. バグを解決できたなら、この再現は自分が修正したという証明の手段になります。

幸運なことにここでは、問題を見つけ出すのに十分な既存のテストがありました。しかし時には、問題を再現するためにテストを自分で直すことも必要でしょう。その場合、私のワークフローはたいてい下記の通りです。

  1. 例えばassertTrue()assertFalse()に変えるなどして既存のテストの破壊を試み、テストが失敗することを確かめます(念のため、これは正常な良いチェック方法です)。
  2. 次に、書きたいと考えるテストに類似したテストをコピー&ペーストします。そして、それが失敗するまでその新しいテストを調整します。

しかし、今回の特定のバグについてはエラーになるテストが既に分かっていますので、次に進みましょう。

ステップ 4:バグを修正する

残念ながら、スタックトレースからはあまりヒントとなる情報は得られません。Zuulがコード変換の際にコードを切り刻んでしまうのか、行番号すらめちゃくちゃです。これは使いものになりません。

unhelpful stacktrace
ここで、私は少し迷いましたが、次のように、筋道立てて考えるよう努めました。:オブジェクトのlengthというプロパティは、setterでなくgetterのみをサポートしているが、それに対する代入によりエラーが起きている。foo.length = barのような動きをする場所があるだろうか? コードの中の.length =という例を探してみよう。

searching the code for instances of "length"
.lengthの設定されている場所が3カ所見つかりました。最も面白いのは最初のものです。なぜなら、Buffer.TYPED_ARRAY_SUPPORT上の条件if/elseにラップされているからです。TYPED_ARRAY_SUPPORTが意味するところは分かりませんが、.lengthへの代入はある1つのケースでは制限されているが、他の2つでは制限されていない、という勘がすぐに働きました。

TYPED_ARRAY_SUPPORTが何であるかを見極めようとしたところ、次のコードに行き当たりました。

if (Buffer.TYPED_ARRAY_SUPPORT) {
  Buffer.prototype.__proto__ = Uint8Array.prototype
  Buffer.__proto__ = Uint8Array
}

なるほど、つまりTYPED_ARRAY_SUPPORT(これが何であれ)によって、Bufferオブジェクト(index.jsが出力するもの)のプロトタイプを、ビルトインのUint8Arrayのプロトタイプと同様に設定するようです。ならば、場合によっては、同じprototype.lengthを自分で設定しているはずです。ChromeがStrictモードの内蔵オブジェクトのプロトタイプを修正させないようにしているのでしょうか。バグの仮説が具体化し始めました。

そこで非常に簡単な修正をしてみました。lengthが代入されている2つのケースを取り、両方にif (!Buffer.TYPED_ARRAY_SUPPORT)の条件を付けました。

wrapping the offending code in an if () check
(また、それが何を行っているのかは定かではありませんでしたが.parentへの代入も条件分岐にラップしました。.lengthへの代入に関係しているように見えたからです)

それからブラウザテストを再度読み込むと、突然全84テストが通ったのです。明らかに、上記の調整が機能しています。

ステップ 5:プルリクエストをオープンする

ここまで来ると、自分をほめ、勝利を宣言したい誘惑にかられるでしょう。しかし、多くのブラウザライブラリにおいては、修正が完了したと言えるのは、広範囲の様々なブラウザのテストを通過してからです。今回のケースでは、Buffer.TYPED_ARRAY_SUPPORTの修正がChromeではうまくいったようですが、他のブラウザではどうでしょうか?

ラップトップにインストールされた全てのブラウザ(私はMacを使っているので、IEやAndroid以外)で自ら1つずつテストするよりシンプルな方法は、プロジェクトでプルリクエストをオープンすることです。活発に運営されているオープンソースの多くはプルリクエストを含む全てのコミットに自動的にテストを実行します。これはプルリクエストのプロセスで重要な点です。コントリビュータは、パッチがテストを壊すことはないと知れば安心できるからです。

プロジェクトのREADMEの中でSaucelabsバッジを見たことで、実際に”buffer”は数多くのブラウザの自動テストを使ったのだと推察できました。

The Saucelabs badge in the "buffer" README.

Saucelabsバッジが、様々なブラウザでのテストの現状を示しています。

しかし、プルリクエストをオープンする前に、自分のコードがプロジェクトスタイル、今回は、どこか大胆な名称のスタイル、Standardに沿っているかをチェックしなければなりません。個人的にはStandardは好きではありませんが(だから…と言えればいいのですが)、これは私のプロジェクトではありませんから、古くからの助言「郷に入っては郷に従え」に従います。

コードがStandardのスタイルに準拠しているかをチェックするには、ただ次のコードを走らせます。

npm install -g standard && standard

これに通れば、プロジェクトのスタイルガイドに沿っていることが分かります。

次に、修正をコミットするため、Gitブランチを別に作成します。それをIssue Numberの79と呼ぶことにします。ブランチには問題にちなむ名前を付けるのが私の習慣です。

git checkout -b 79
git add index.js
git commit -m 'Proper strict mode support. Fixes #79'

それから、プロジェクトを分離し、hubを使ってプルリクエストを送信します。これはGithub仕様のツールと連携した便利なgitラッパーです。

hub fork
git push nolanlawson 79
hub pull-request

ここで、hubはプルリクエストを作成し、ブラウザで閲覧できるようURLを表示します。少し待つとテストが完了し、緑色のチェックマークが現れました。全てのブラウザのテストに通りました!

Github UI showing the test results
注釈:テスト通過!

テストに失敗し、その原因が分からない場合は、”Show all checks”リンク、”Details”の順にクリックすれば、失敗したテストを確認できます。

Github UI showing all checks
注釈:テストの詳細を表示

この場合、それがTravis CIによって行われたテストであることが分かり、テストの全ログのアウトプットも閲覧できました。

Travis CI UI
安心なことに、複数のブラウザでテストが行われたことも明白です。

Travis CI output
(テストが通らなかった場合、そのブランチにコミットをプッシュし続けると、Travisは各コミットに対して再度テストを走らせます。)

最終ステップ:プルリクエストの承認またはフィードバックを待つ

この時点で、私のプルリクエストはマージに足る候補になったと確信できます。それがコードにふるまいの変更をもたらすとしても(非Strictモードではなく、Strictモードを使用して)、反論が来ることはないと予想しました。なぜならほとんどのJavaScriptプロジェクトはひとまずStrictモードを好むからです。また、Strictのコードは非Strict環境でも走りますが、その逆は不可だからです。そのためコードを非Strictにする実際的な理由はありません。

ここで、このように思うかもしれません。「とりあえず修正コードがあるのだから、今'use strict'を削除してもいいのでは?」。確かに、それはあり得ます。しかし、誰かが非Strictで行うようなやり方でコードを変更すると自動テストはそれを感知できず、将来プロジェクトが後退する可能性が常にあるのです。将来の後退を防ぐことは重要です。Dale Harveyが次のようなことを言っていました。

テストされていないコードはいつか壊れる。 – Harveyのソフトウェアにおけるエントロピーの法則

少々誇大妄想的だと思うかもしれませんが、私は、オープンソースコントリビュータとしてのキャリアを通して、それが真であると納得させられる場面を何度も何度も見てきました。テストしなければ、結局は誰かがコードをコミットし(恐らく、見た目にはコードベースには関係なさそうな部分で)、テストされていないコードはひっそりと障害を起こし始めるでしょう。

いずれにしても、レビュー・マージ担当者は私の選択に合意したようでした。彼はそのコードをマージし、数日のうちに新バージョンを公開したからです。そしてこの修正によって”buffer”プロジェクトは、公開中のプルリクエスト 0、公開中の問題 0になったのです!

PR was merged!
やったー、プルリクエストがマージされました!

まとめ

このブログ記事で、オープンソースプロジェクトにおけるバグ修正が不可能でも特別難しいわけでもないことが分かってもらえたなら嬉しいです。オープンソースプロジェクトは他のコードとは異なるルールで動いている傾向があります(より慎重にテストが行われ、バグもオープンにされている、等)。しかし、個人やクローズドソースのプロジェクトにコードをコミットしたことがあるなら、同じスキルをオープンソースの世界に適用しない合理的な理由はありません。

“buffer”の件は、数多あるオープンソースのバグを残念なほど象徴していると思いました。モジュールそのものが大きな依存の対象であり、バグは非常に目立ちますが、多くの人がそれに注力しているにも関わらず数か月間、未解決のままでした。大勢の仲間が一時しのぎの回避策を提案しましたが、問題を根本から修正しようと試みる人はいなかったのです。

恐らく、コントリビュータであるFeross Aboukhadijehが自ら問題を解決するだろうという期待があったのだろうと思います。しかし彼のGithubページを見れば、彼が多くのプロジェクトをメンテナンスしていることが分かります。私自身は、ほんの短時間だけ寄与しているオープンソースオーサーですが、1日に100件のGithub通知を受け取るのは普通です。Ferossがそれよりもはるかに多くを受信していることは間違いありません。

ですからもし、Ferossが全てを捨て、ある特定のバグに取り組んでくれると思っているなら、彼の作業リストにはより優先順位の高いタスクが山ほどあることを知ったほうがよいでしょう。彼はWebpackやBabelさえ使っていないのではないかと想像します(Browserifyを好んでいるようなので)。つまり、彼はこのバグ修正に大した価値があるとは考えていないのです。また、誤って非StrictコードをStrict環境で実行しようとしているのを見ると、これが実際にWebpackやBabelのバグであることも疑わしいのです。

私の結論、バグがあなたに個人的に影響を与えるなら、そしてあなたがそれに遭遇したなら、あなたはそれを修正する最適なポジションにいるということです。作業時間が限られ、あなたの問題にさして関心のないプロジェクトのメンテナンス担当者に、あなたの説明を元にバグを再現し、修正するように依頼するのは、問題解決からほど遠い方法です。

ですから次にオープンソースプロジェクトでバグを見つけ、新しい議題をオープン(あるいは、”+1″、”同じ状況が起きています”と報告)したいと思った時は、代わりに自分で修正することを考えてください。問題を再現できるだけでも、常に新しい問題を選別し、長たらしいバグレポートの解読に努めているプロジェクトのコントリビュータたちには大きな助けになります。

オープンソースソフトウェアは天から与えられるマナではありません。何もしなくても、コードベースに魔法のように現れる自動的更新のリソースでもありません。断じて違います。オープンソースソフトウェアは人間の勤労と知恵に支えられた不断のプロダクトなのであり、その存続にはコミュニティの力が必要です。

貢献の非対称なプロジェクト(例えば、コントリビュータよりも利益を得ている人のほうが多いプロジェクト)は、結局は終末に向かい、コントリビュータの燃え尽きによって死に絶えてしまいます。あなたはこの状況を防ぐことができるのです。同時に、ただプルリクエストをオープンすることで、コーダー仲間を助けることができ、個人的な満足感も得られるでしょう。

a job well done
このようなコメントをもらうだけで良い1日になります。

利益を得ているオープンソースプロジェクトがあるなら、プルリクエストというギフトをコントリビュータに贈ることを考えてください。彼らのGithubページをチェックして、未解決の問題のリストを開き、興味を引くものがないか探してみてください。バグ修正を完了できなくても、説明を改良したり、テストプロセスを容易にしたりすることができるかもしれません。

オープンソースソフトウェアに貢献する方法は、大小問わず、たくさんあります。しかしまず試してみないことには、それがいかに簡単かは分からないのです。

脚注


  1. 「Githubはあなたの履歴書」の記事と並べて読みたいのは、「なぜGitHubは履歴書ではないのか」。Githubプロフィルが雇用の決定に影響を及ぼすか(あるいはヒマとエネルギーを持て余す人たちを喜ばせるだけか)は議論の余地がありますが、Githubでのプレゼンスが職探しの見通しに影響することは否定できないでしょう。ですから新人プログラマには「Githubプロフィルを増強せよ」は、今なお価値ある助言だと思っています(何にせよ、他のオープンソース仲間はあなたについて知るためにプロフィールを見るのですから)。 

  2. 鋭い読者は気づいているかもしれませんが、私は”buffer”プロジェクトのコラボレーターです。しかし、Ferossは、たった2度のプルリクエストで私にコラボレーションの権限をくれました(彼はいい人だからです)。それでもなお、このバグと格闘していた時はコードベースにとっつきにくさを感じていました(例えば、テストの実行方法を覚えられませんでした。私が最後にコードを送信してから変わったのかもしれませんが)。そのため、これは不案内なリポジトリにおけるバグ修正の例としてなお適当だと考えています。