2016年2月4日
オープンソースプロジェクトでバグを修正する方法 : あるNodeJSモジュールへの修正を例に
(2015-12-28)by Nolan Lawson
本記事は、原著者の許諾のもとに翻訳・掲載しております。
(訳注:2016/3/3、頂いたフィードバックをもとに記事を修正いたしました。)
オープンソースプロジェクトでバグを見つけたとします。まずは、慌てずに落ち着きましょう。これは実によくあることです。ソフトウェアは人間の手で書かれるし、人間はミスを犯すものです。
または、こんなふうに考えるかもしれません。「やったね、バグの修正は大好きだ」。さっと現れて、何百万人とまではいかなくても、何千人もが使っているプロジェクトのバグを修正してしまうようなヒーローになりたくない人なんていますか? オープンソースコミュニティに恩返しできたという温かな喜びを感じられる上に、一連の Github 履歴 ^(1) に追加得点を上げられるわけです。
*訳:
人気のあるプロジェクト
典型的なバグ
しかしコーディングの初心者にとっては、オープンソースプロジェクトにコントリビュートするなんて恐れ多いことに思えます。私の友人に、最近JavaScriptをWeb上のチュートリアルで学び始めた人がいるのですが、Githubのユーザインターフェースを見ると”途方に暮れる”と言っていました。
また、社会的な側面もあります。それは、”プロジェクト管理者は、私のプルリクエストを受け付けてくれるだろうか? ”とか、”批判されたり、却下されたりしたらどうしよう? ”といった不安です。これは コーディング初心者 なら誰もが感じることです。自分と他の熟練者たちと比べて、知識の差を感じているのでしょう。
しかし、臆することはありません。オープンソースに関わる人たちにとって、新規メンバーからのプルリクエストは大歓迎なのです。 First Timers Only や Your First PR といった最近の試みからも分かりますが、手助けさえしてもらえば、誰でもオープンソースプロジェクトにコントリビュートできるのです。
ただ、本稿の目的はちょっと違います。特定のプロジェクトの特定のバグについて詳細な解説をするのではなく、 不特定の プロジェクトの あらゆる バグをどのように修正するかを説明したいと思います。問題解決のプロセスを図解する際の例として、 “buffer”モジュールの最近のバグ を使うことにします。私はこのプロジェクトではほとんど何の経験もないのですが、このバグを1時間で解決しました。
私でさえ経験の無いプロジェクトでバグを修正できるのですから、あなたにもできますよ。
お膳立て
“buffer” とは、ブラウザ用Node.jsの Buffer API の、JavaScriptのインプリメンテーションです。これを使うことで、APIが存在しないブラウザでも、 Buffer
オブジェクトに依存するNode.jsモジュールを使えるようになります。(そのかわりにブラウザには、 Unit8Array や ArrayBuffer のような、バイナリデータを扱う他のAPIがあります)。
まるで神秘的で難解なライブラリのように思えるかもしれませんが、実際のところ”buffer”は 1カ月に200万回 近くダウンロードされています。というのも、 Browserify と Webpack どちらに対しても必須の依存パッケージだからです。しかし、そんなに知名度の高いプロジェクトであるのにもかかわらず、3カ月以上も解決されずに長引いている 問題 があることに気が付きました。
訳:
[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.length
を undefined
にセットしようとした時に起きます。ここです。 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を使えるようになっています。
訳:この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
残念ながら、このケースでは、上記のステップはエラーとなりました。
しかしここで、メッセージの最も重要な箇所に気が付きました。以下の部分です。
Error: Zuul tried to run tests in saucelabs,
however no saucelabs credentials were provided.
これを読むと、このプロジェクトはブラウザの自動テストを実行する際 Zuul と Saucelabs を使っていることが分かりました。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がありました。
やった! 成功です! この時点でようやく、自分のマシンでビルドしてコードをテストできたと確信しました。
注:もし、”単にプロジェクトをテストするために、こんなにいろいろ調べる必要はないのに”と思っているとしたら、それは正しいと言えます。私はまた、テストの過程をドキュメント化する プルリクエストをオープン するのにも時間をかけました。これは、新規コントリビュータがプロジェクトに対して行える、最も価値のある貢献の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
の先頭に追加しました。
驚くことに、Zuulテストページを再度読み込むと、皆が話題にしていたバグが即座に現れました。
注釈:Githubに報告されていたのと全く同じエラー
(こうなるとテストが実行さえされないことに注意しましょう。ページは緑色でなく黄色で、”失敗 0、通過 0″と表示します。)
この時点で、プロジェクト用のテストスイートを利用してバグの再現はできました。これは次の理由から 重要なステップ と言えます。
- たとえバグを解決できなくても、エラーになるテストを突き止めたことだけでもプルリクエストをオープンすることができます。これによってバグをいかに再現するかという長い議論を皆が省略できます。
- バグを 解決できた なら、この再現は自分が修正したという証明の手段になります。
幸運なことにここでは、問題を見つけ出すのに十分な既存のテストがありました。しかし時には、問題を再現するためにテストを自分で直すことも必要でしょう。その場合、私のワークフローはたいてい下記の通りです。
- 例えば
assertTrue()
をassertFalse()
に変えるなどして既存のテストの破壊を試み、テストが失敗することを確かめます(念のため、これは正常な良いチェック方法です)。 - 次に、書きたいと考えるテストに類似したテストをコピー&ペーストします。そして、それが失敗するまでその新しいテストを調整します。
しかし、今回の特定のバグについてはエラーになるテストが既に分かっていますので、次に進みましょう。
ステップ 4:バグを修正する
残念ながら、スタックトレースからはあまりヒントとなる情報は得られません。Zuulがコード変換の際にコードを切り刻んでしまうのか、行番号すらめちゃくちゃです。これは使いものになりません。
ここで、私は少し迷いましたが、次のように、筋道立てて考えるよう努めました。:オブジェクトの length
というプロパティは、setterでなくgetterのみをサポートしているが、それに対する代入によりエラーが起きている。 foo.length = bar
のような動きをする場所があるだろうか? コードの中の .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)
の条件を付けました。
(また、それが何を行っているのかは定かではありませんでしたが .parent
への代入も条件分岐にラップしました。 .length
への代入に関係しているように見えたからです)
それからブラウザテストを再度読み込むと、突然全84テストが通ったのです。明らかに、上記の調整が機能しています。
ステップ 5:プルリクエストをオープンする
ここまで来ると、自分をほめ、勝利を宣言したい誘惑にかられるでしょう。しかし、多くのブラウザライブラリにおいては、修正が完了したと言えるのは、広範囲の様々なブラウザのテストを通過してからです。今回のケースでは、 Buffer.TYPED_ARRAY_SUPPORT
の修正がChromeではうまくいったようですが、他のブラウザではどうでしょうか?
ラップトップにインストールされた全てのブラウザ(私はMacを使っているので、IEやAndroid以外)で自ら1つずつテストするよりシンプルな方法は、プロジェクトでプルリクエストをオープンすることです。活発に運営されているオープンソースの多くはプルリクエストを含む全てのコミットに自動的にテストを実行します。これはプルリクエストのプロセスで重要な点です。コントリビュータは、パッチがテストを壊すことはないと知れば安心できるからです。
プロジェクトのREADMEの中でSaucelabsバッジを見たことで、実際に”buffer”は数多くのブラウザの自動テストを使ったのだと推察できました。
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を表示します。少し待つとテストが完了し、緑色のチェックマークが現れました。全てのブラウザのテストに通りました!
注釈:テスト通過!
テストに失敗し、その原因が分からない場合は、”Show all checks”リンク、”Details”の順にクリックすれば、失敗したテストを確認できます。
注釈:テストの詳細を表示
この場合、それが Travis CI によって行われたテストであることが分かり、テストの全ログのアウトプットも閲覧できました。
安心なことに、複数のブラウザでテストが行われたことも明白です。
(テストが通らなかった場合、そのブランチにコミットをプッシュし続けると、Travisは各コミットに対して再度テストを走らせます。)
最終ステップ:プルリクエストの承認またはフィードバックを待つ
この時点で、私のプルリクエストはマージに足る候補になったと確信できます。それがコードにふるまいの変更をもたらすとしても(非Strictモードではなく、Strictモードを使用して)、反論が来ることはないと予想しました。なぜならほとんどのJavaScriptプロジェクトはひとまずStrictモードを好むからです。また、Strictのコードは非Strict環境でも走りますが、その逆は不可だからです。そのためコードを非Strictにする実際的な理由はありません。
ここで、このように思うかもしれません。「とりあえず修正コードがあるのだから、今 'use strict'
を削除してもいいのでは?」。確かに、それはあり得ます。しかし、誰かが非Strictで行うようなやり方でコードを変更すると自動テストはそれを感知できず、将来プロジェクトが後退する可能性が常にあるのです。将来の後退を防ぐことは重要です。 Dale Harvey が次のようなことを言っていました。
テストされていないコードはいつか壊れる。 – Harveyのソフトウェアにおけるエントロピーの法則
少々誇大妄想的だと思うかもしれませんが、私は、オープンソースコントリビュータとしてのキャリアを通して、それが真であると納得させられる場面を何度も何度も見てきました。テストしなければ、結局は誰かがコードをコミットし(恐らく、見た目にはコードベースには関係なさそうな部分で)、テストされていないコードはひっそりと障害を起こし始めるでしょう。
いずれにしても、レビュー・マージ担当者は私の選択に合意したようでした。彼はそのコードをマージし、数日のうちに新バージョンを公開したからです。そしてこの修正によって”buffer”プロジェクトは、公開中のプルリクエスト 0、公開中の問題 0になったのです!
やったー、プルリクエストがマージされました!
まとめ
このブログ記事で、オープンソースプロジェクトにおけるバグ修正が不可能でも特別難しいわけでもないことが分かってもらえたなら嬉しいです。オープンソースプロジェクトは他のコードとは異なるルールで動いている傾向があります(より慎重にテストが行われ、バグもオープンにされている、等)。しかし、個人やクローズドソースのプロジェクトにコードをコミットしたことがあるなら、同じスキルをオープンソースの世界に適用しない合理的な理由はありません。
“buffer”の件は、数多あるオープンソースのバグを残念なほど象徴していると思いました。モジュールそのものが大きな依存の対象であり、バグは非常に目立ちますが、多くの人がそれに注力しているにも関わらず数か月間、未解決のままでした。大勢の仲間が一時しのぎの回避策を提案しましたが、問題を根本から修正しようと試みる人はいなかったのです。
恐らく、コントリビュータである Feross Aboukhadijeh が自ら問題を解決するだろうという期待があったのだろうと思います。しかし彼のGithubページを見れば、彼が 多くの プロジェクトをメンテナンスしていることが分かります。私自身は、ほんの短時間だけ寄与しているオープンソースオーサーですが、1日に100件のGithub通知を受け取るのは普通です。Ferossがそれよりもはるかに多くを受信していることは間違いありません。
ですからもし、Ferossが全てを捨て、ある特定のバグに取り組んでくれると思っているなら、彼の作業リストにはより優先順位の高いタスクが山ほどあることを知ったほうがよいでしょう。彼はWebpackやBabelさえ使っていないのではないかと想像します(Browserifyを好んでいるようなので)。つまり、彼はこのバグ修正に大した価値があるとは考えていないのです。また、誤って非StrictコードをStrict環境で実行しようとしているのを見ると、これが実際にWebpackやBabelのバグであることも疑わしいのです。
私の結論、バグがあなたに個人的に影響を与えるなら、そしてあなたがそれに遭遇したなら、 あなた はそれを修正する最適なポジションにいるということです。作業時間が限られ、あなたの問題にさして関心のないプロジェクトのメンテナンス担当者に、あなたの説明を元にバグを再現し、修正するように依頼するのは、問題解決からほど遠い方法です。
ですから次にオープンソースプロジェクトでバグを見つけ、新しい議題をオープン(あるいは、”+1″、”同じ状況が起きています”と報告)したいと思った時は、代わりに自分で修正することを考えてください。問題を再現できるだけでも、常に新しい問題を選別し、長たらしいバグレポートの解読に努めているプロジェクトのコントリビュータたちには大きな助けになります。
オープンソースソフトウェアは天から与えられるマナではありません。何もしなくても、コードベースに魔法のように現れる自動的更新のリソースでもありません。断じて違います。 オープンソースソフトウェアは人間の勤労と知恵に支えられた不断のプロダクト なのであり、その存続にはコミュニティの力が必要です。
貢献の非対称なプロジェクト(例えば、コントリビュータよりも利益を得ている人のほうが多いプロジェクト)は、結局は終末に向かい、コントリビュータの燃え尽きによって死に絶えてしまいます。あなたはこの状況を防ぐことができるのです。同時に、ただプルリクエストをオープンすることで、コーダー仲間を助けることができ、個人的な満足感も得られるでしょう。
このようなコメントをもらうだけで良い1日になります。
利益を得ているオープンソースプロジェクトがあるなら、プルリクエストというギフトをコントリビュータに贈ることを考えてください。彼らのGithubページをチェックして、未解決の問題のリストを開き、興味を引くものがないか探してみてください。バグ修正を完了できなくても、説明を改良したり、テストプロセスを容易にしたりすることができるかもしれません。
オープンソースソフトウェアに貢献する方法は、大小問わず、たくさんあります。しかしまず試してみないことには、それがいかに簡単かは分からないのです。
脚注
-
「Githubはあなたの履歴書」の記事と並べて読みたいのは、 「なぜGitHubは履歴書ではないのか」 。Githubプロフィルが雇用の決定に影響を 及ぼす か(あるいはヒマとエネルギーを持て余す人たちを喜ばせるだけか)は議論の余地がありますが、Githubでのプレゼンスが職探しの見通しに 影響する ことは否定できないでしょう。ですから新人プログラマには「Githubプロフィルを増強せよ」は、今なお価値ある助言だと思っています(何にせよ、他のオープンソース仲間はあなたについて知るためにプロフィールを見るのですから)。 ↩
-
鋭い読者は気づいているかもしれませんが、私は”buffer”プロジェクトのコラボレーターです。しかし、Ferossは、たった2度のプルリクエストで私にコラボレーションの権限をくれました(彼はいい人だからです)。それでもなお、このバグと格闘していた時はコードベースにとっつきにくさを感じていました(例えば、テストの実行方法を覚えられませんでした。私が最後にコードを送信してから変わったのかもしれませんが)。そのため、これは不案内なリポジトリにおけるバグ修正の例としてなお適当だと考えています。 ↩
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa