著作一覧 |
おとといのバグ。
バグ自体はくだらないのだが、原因追究に無駄な時間を使ったのでメモ。
次のようなプログラムを作った。
void Write(string templ, params object[] args) { string msg = string.Format(templ, args); ...//IOを伴ういろいろな処理 } void FooBar(object a, object b, object c, object d) { ... Write("{0} {1} {2} {3}", a, b, c, d); ... }
が、テンプレートのパターンと引数に齟齬があり、例外が投げられるバグが見つかった(たとえば、{}内で特定の型を指定しているのに、引数がそれとは合わないなど)。
そこで、以下のようにした。
void WriteAsABCD(object a, object b, object c, object d) { Write("{0} {1} {2} {3}", a, b, c, d); } void WriteAsABDC(object a, object b, object c, object d) { Write("{0} {1} {3} {2}", a, b, c, d); } void FooBar(object a, object b, object c, object d) { ... WriteAsABCD(a, b, c, d); ... }
ところが、実際にはこの修正では引数の型指定がテンプレートと合わなければ修正前と同じことが起きる道理だ。
したがって(実は予想もつかない引数を与えるやつがいるのが原因でやはり)例外が発生する(このメソッド自体が型をアンマーシャルするので、パラメータの型はobjectにせざるを得ないとか、いろいろ理由があって型はobject以外にできない)。
このときReleaseモードでコンパイルしているため、メソッド名と行番号のみがバックトレースに入る。
ArgumentError: ... Foo::Write ... Foo::FooBar ...
ここですっかりだまされてしまった。
FooBarがWriteを呼び出すのは、修正前のバージョンだ。修正後のバージョンであれば、Foo::FooBarとFoo::Writeの間にFoo::WriteAsABCDが呼ばれているはずだ。したがって、例外を起こしているプログラムは修正前のバージョンに違いない。
だが、どう調べても実行時に呼ばれているのは修正後のバージョンに見える。PATHにも間違いはなさそうだ(多分)。が、実行時にカレントディレクトリを変えているとか、そこがTemporaryでそこにバックアップされた古いプログラムが存在するとかか?
だが、どう実行しても(実行環境は固有の環境で、デバッガなどは使えない。ネットワークから隔離されているのでリモートデバッガもあり得ない。また、開発環境は実行環境固有の環境を持ちえないので、開発環境でテストすることもあまり意味がない(ユニットテストとモックがあるわけだが、ここではそれをしていないのが大間違いだが、後の祭りである)例外になるし、バックトレースは上記の内容だし、実行しているのは修正後としか思えない。
こういうとき、何を疑うべきだろうか?
もちろんソースコードを疑うべきだ。そして修正前のプログラムにバグがあるのはわかっている。
まあ、瞬時にわからなかったのはしょうがないかなぁとは思うが、それにしても数分で気づけよとは思う。(実際には数十分かかった)
これがトライ&エラーが簡単な環境ならば、printfデバッグですぐにわかるわけだが、それができないときは頭だけが頼りだな(いや、その前にユニットテストをちゃんと作っとけというのはあるけど)。
ジェズイットを見習え |