マルチバグ発見

 
0
このエントリーをはてなブックマークに追加
Daichi Takayama
Daichi Takayama (高山 大地)

以下の文章はこちらの記事を翻訳したものです。

https://github.com/HypothesisWorks/hypothesis/blob/master/HypothesisWorks.github.io/_posts/2017-09-14-multi-bug-discovery.md

Hypothesisはバグを発見すると、それをよりシンプルな形式に簡略化し、同じバグを再現するよう試みます。これは多くのプロパティベースドテストライブラリに共通する機能で、それぞれに若干の違いがあります。また、スタンドアロンのテストケースリデューサーも広く利用されています。これは外部プロジェクトでのバグ報告に役立ち、大きなファイルの代わりに、良質なテストケースリデューサーはしばしばコードを数行にまで縮小することができます。

しかし、問題が生じます:最初に発見したバグが、最終的に特定したバグと同一であることをどのように確認するのでしょうか?

これは学問的な問いではありません。実際、最初に取り組んだバグから別のバグへ移行することは珍しくないのです。

例として、以下のテストを考えてみましょう:

from hypothesis import given, strategies as st

def mean(ls):
    return sum(ls) / len(ls)

@given(st.lists(st.floats()))
def test(ls):
    assert min(ls) <= mean(ls) <= max(ls)

このテストには、いくつかの興味深い失敗の可能性があります。たとえば、リストにNaN(Not a Number)を含めることができ、これによりアサーションが失敗する可能性があります。NaNは数値演算の特殊なケースであり、通常の数値比較の結果が不定になるためです。また、リストに[-float('inf'), +float('inf')]を含めることもでき、これにより無限の値が平均値の計算に影響を与えることがあります。無限大の値は平均値計算において特殊な扱いが必要になります。さらに、非常に大きい数値や非常に小さい数値を含むリストは、浮動小数点の精度の問題を引き起こす可能性があります。これらの問題は、テストの失敗を引き起こしやすく、プログラムのロバスト性に重要な洞察を提供します。

しかし、テストケースが簡略化されると、空のリストが入力として渡されることがあります。この場合、空のリストに対して最小値や最大値を求めることはできず、テストは失敗します。

これは必ずしも大きな問題ではありません。結局のところ、バグを発見しているからです。これは、テストされるコードだけでなく、テスト自体においても同様です。時にはこの方法でより多くのバグを発見することが望ましいこともあります。Hypothesisが見逃す可能性のあるバグを含む、多様な問題が明らかにされることもあります。しかし、このアプローチには問題があり、興味深く珍しいバグがより一般的で退屈なものに置き換わることがしばしばあるのです。

歴史的に見て、Hypothesisはこの問題に対して他の多くのツールよりも優れた解決策を持っていました。Hypothesisの例データベースのおかげで、すべての中間段階のバグが保存されます。これにより、テストを再実行する際に、これらのバグのいくつかが再度テストされることになります。したがって、一つのバグを修正してテストを再実行すると、それまでに単純なバグによって隠されていた他のバグが明らかになるのです。このように、Hypothesisはテストプロセスにおいて、さまざまなバグを発見し、それらを効果的に扱う方法を提供します。これにより、テストの網羅性が向上し、より多くのバグが発見される可能性が高まります。

それでも、この方法ではユーザー体験としては最適とは言えません。この方法では、利用可能な情報のごく一部しか得られず、Hypothesisが優先する順番に沿ってバグを修正しなければなりません。もしHypothesisが見つけたすべてのバグについて情報を提供し、それらをユーザーが自分で優先順位を付けられるシステムがあれば、より便利になると言えるでしょう。

(数週間前にリリースされたHypothesis 3.29.0では、このような機能が実装されました!)上記のテストを現在実行すると、以下のような結果が得られます:

Falsifying example: test(ls=[nan])
Traceback (most recent call last):
  File "/home/david/hypothesis-python/src/hypothesis/core.py", line 671, in run
    print_example=True, is_final=True
  File "/home/david/hypothesis-python/src/hypothesis/executors.py", line 58, in default_new_style_executor
    return function(data)
  File "/home/david/hypothesis-python/src/hypothesis/core.py", line 120, in run
    return test(*args, **kwargs)
  File "broken.py", line 8, in test
    def test(ls):
  File "/home/david/hypothesis-python/src/hypothesis/core.py", line 531, in timed_test
    result = test(*args, **kwargs)
  File "broken.py", line 9, in test
    assert min(ls) <= mean(ls) <= max(ls)
AssertionError

Falsifying example: test(ls=[])
Traceback (most recent call last):
  File "/home/david/hypothesis-python/src/hypothesis/core.py", line 671, in run
    print_example=True, is_final=True
  File "/home/david/hypothesis-python/src/hypothesis/executors.py", line 58, in default_new_style_executor
    return function(data)
  File "/home/david/hypothesis-python/src/hypothesis/core.py", line 120, in run
    return test(*args, **kwargs)
  File "broken.py", line 8, in test
    def test(ls):
  File "/home/david/hypothesis-python/src/hypothesis/core.py", line 531, in timed_test
    result = test(*args, **kwargs)
  File "broken.py", line 9, in test
    assert min(ls) <= mean(ls) <= max(ls)
ValueError: min() arg is an empty sequence

You can add @seed(67388524433957857561882369659879357765) to this test to reproduce this failure.
Traceback (most recent call last):
  File "broken.py", line 12, in <module>
    test()
  File "broken.py", line 8, in test
    def test(ls):
  File "/home/david/hypothesis-python/src/hypothesis/core.py", line 815, in wrapped_test
    state.run()
  File "/home/david/hypothesis-python/src/hypothesis/core.py", line 732, in run
    len(self.falsifying_examples,)))
hypothesis.errors.MultipleFailures: Hypothesis found 2 distinct failures.

(スタックトレースは少し騒がしいですね。これについて整理するための課題がオープンされています)。

異なるバグは同時に最小化され、Hypothesisの例の縮小機能を最大限に活用しています。その結果、発見された各バグは、他にバグが見つからなかった場合と同じくらいの読みやすさを持っています。

しかし、完璧ではありません。Hypothesisが2つのバグを同一とみなすために使用されるヒューリスティックは、例外の種類が同じであり、例外が同じ行から投げられているかどうかを基準にしています。これにより、実際には異なるバグがいくつか混同される可能性があります。例えば、[float('nan')][-float('inf'), float('inf')][3002399751580415.0, 3002399751580415.0, 3002399751580415.0]はそれぞれテストのアサーションを引き起こしますが、これらは「異なる」バグであると言えるでしょう。

Hypothesisが使っているこのヒューリスティックは、意図的に設計されています。主な目的は、2つのケースが同一のバグかどうかを判別することではなく、見つけたバグの例が互いに十分異なっていて、それぞれが興味深い価値を持つかどうかを見極めることです。この手法は、確かにその目的を達しています。

私が知る限り、このようなアプローチはプロパティベースドのテストライブラリでは新しい試みです(一方で、ファジングツールではよく見られる方法で、theftプロジェクトも似た機能の開発を進めています)。また、Erlang QuickCheckに関連するいくつかの他の面白い研究も存在しますが、それらはこの話題とはまた別の側面を探っています。

また、このアプローチは実装も驚くほど簡単でした。

この機能の開発は多くの面でうまくいきました。技術的な面だけでなく、社会的な面、そしてその中間的な側面もそうです。

技術的な面は非常にシンプルで直接的でした。Hypothesisの中核となるモデルがこの機能に非常に適していることがわかりました。Hypothesisには一元的な中間表現があり、その表現を使って単純化が行われます。そのため、複数の要素を同時に縮小するようHypothesisを適用するのは比較的容易でした。新しいバグが見つかった場合、それを既知の最良の例と比較し、新しい例がより優れていれば置き換え、新しいバグを発見した場合にはそれに対応しました。その後、既知のすべてのバグに対して縮小プロセスを繰り返し適用し、完全に縮小されるまで続けました。

驚くべきことではないかもしれませんが、マルチ縮小問題に対する私たちの取り組みは、Hypothesisの現在のモデルに大きく影響を与えています。これについては以前から考えていたことで、実際にHypothesisで具体化するのはこれが初めてです。

このプロジェクトの社会的な面は、技術面よりもさらに面白いかもしれません。というのも、このプロジェクトにはストライプからの資金提供を受けて行われたPandasコードのテストが関係しています。Pandasでのテスト中に見つかった多くのバグ(実際にはPandas自体のバグではなく、PandasとHypothesisの統合に関するバグ)は、この新しいアプローチを考えるきっかけとなりました。

さらに、Smarketsからの資金提供で開発された新しい機能も、このマルチ縮小機能によって大きく簡素化されました。期限機能とバグの相互作用に関する予想された多くの複雑さが、複数のバグを効果的に扱えるようになることで、解決されたのです。

技術的な側面だけでなく、コミュニティとの協力や資金提供の重要性も再認識する貴重な経験となりました。これらの要素が組み合わさり、Hypothesisの進化に大きく貢献しています。

Hypothesisの開発においては、実用的な問題が理論的な開発を引き起こすというのが一貫したテーマです。例えば、Djangoのテストを効果的にサポートすることはHypothesisの基本的なモデルの存在理由の一つと言えます。そのため、StripeやSmarketsからの最近の資金提供は、直接関係がないように見える開発を促進し、資金提供された作業の範囲外でも、Hypothesisをより使いやすくする方法となりました。

ここで特に役立ったのが、私たちのレビュープロセスであり、特にZacからのレビューが重要でした。

この機能は当初、計画していたものとは異なっていました。最初はもっと単純な機能としてスタートし、同じようなメカニズムを使い、新しいエラーへの移行を完全に回避することを目指していました。しかし、Zacがこの方法が本当に適切かどうかについて重要な質問を投げかけたことで、いくつかの実験とフィードバックを経て、最終的にはすべてのエラーを表示するデザインにたどり着きました。

私たちのレビューハンドブックでは、コードレビューに関して、単なるチェックではなく、共同でのデザイン作業である点を重視しています。これは、その理念が実際に機能した例だと考えています。私たちは、素晴らしいコードレビュー文化を築いており、そのおかげで多くの恩恵を受けているのです。レビューに意欲的な人はいつでも歓迎しています。

今回のプロジェクトについては、全体的に非常に満足しています。最初から正しい方向で進めたことが、素晴らしい新機能を生み出すきっかけとなりました。

実際に使われる様子を見るのが楽しみです。特に興味深い使い方を見つけたら、ぜひ私に教えてください。または、あなた自身でその体験を記事にしてみてはいかがでしょうか。

info-outline

お知らせ

K.DEVは株式会社KDOTにより運営されています。記事の内容や会社でのITに関わる一般的なご相談に専門の社員がお答えしております。ぜひお気軽にご連絡ください。