8

私は現在、sableccを利用して構築された種類のコンパイラに取り組んでいます。

簡単に言うと、コンパイラは仕様ファイル (これが解析しているものです) と .class ファイルの両方を入力として取り、.class ファイルのバイトコードを計測して、.class ファイルを実行するときにいずれかの仕様が違反していません (これは jml/code 契約に少し似ていますが、より強力です)。

分析フェーズの大部分をカバーする数十のシステム テストがあります (仕様が意味を成していること、および仕様が指定することになっている .class ファイルと一致していることを確認することに関連しています)。

それらを有効なテストと無効なテストの 2 つのセットに分けました。

  • 有効なテストはソース コード ファイルで構成されており、コンパイラによってコンパイルされた場合、コンパイラ エラーや警告は表示されません。

  • 無効なテストは、コンパイラによってコンパイルされたときに、少なくとも 1 つのコンパイラ エラー/警告が表示されるはずのソース コード ファイルで構成されています。

これは、分析フェーズにある間、うまく機能しました。問題は、コード生成フェーズをテストする方法です。過去に、コンパイラコースで開発した小さなコンパイラでシステムテストを行ったことがあります。各テストは、その言語のいくつかのソース ファイルとoutput.txt. テストを実行するときは、ソース ファイルをコンパイルしてからそのメイン メソッドを実行し、出力結果が と等しいことを確認しoutput.txtます。もちろん、これはすべて自動化されていました。

さて、このより大きなコンパイラ/バイトコード計測器を扱うことは、それほど簡単ではありません。単純なコンパイラで行ったことを再現するのは簡単なことではありません。この段階では、システム テストから離れて、単体テストに集中するのがよいと思います。


コンパイラ開発者なら誰でも知っているように、コンパイラは多くの訪問者で構成されています。それらの単体テストをどのように進めるかについてはよくわかりません。私が見たところによると、ほとんどの訪問者は、その訪問者に関連するメソッドを持つ対応するクラスを呼び出しています (訪問者のために SRP を保持するという考えがあったと思います)。

コンパイラの単体テストに使用できる手法がいくつかあります。

  1. 訪問者の各メソッドを個別に単体テストします。これは、スタックレスの訪問者にとっては良い考えのようですが、1 つ (または複数) のスタックを使用する訪問者にとってはひどい考えのように見えます。次に、従来の方法で、標準 (読み取り、非ビジター) クラスの他の各メソッドの単体テストも行います。

  2. 訪問者全体を一度に単体テストします。つまり、ツリーを作成してからアクセスします。最後に、シンボル テーブルが正しく更新されたかどうかを確認します。その依存関係をモックすることは気にしません。

  3. 2) と同じですが、訪問者の依存関係をモックしています。

  4. 他には?

単体テストが sabbleCC の AST と非常に緊密に結合されるという問題がまだ残っています (これは本当に醜いです)。


現在、新しいテストを行っていませんが、システムをテストしないことは、遅かれ早かれ戻ってきて私たちを噛むモンスターに餌を与えることと同じであると確信しているため、列車を軌道に戻したいと思います。私たちが最も期待していないときにお尻;-(

コンパイラーのテストを行った経験がある人はいますか? 私はここでちょっと迷っています!

4

1 に答える 1

7

私は、Eclipseコンパイラを使用してJava ASTを別の言語であるOpenCLに変換するプロジェクトに携わっていますが、同様の問題があります。

私はあなたのための魔法の解決策を持っていませんが、それが役立つ場合に備えて私の経験を共有します。

期待される出力(output.txtを使用)でテストするあなたのテクニックも私が始めた方法ですが、それはテストの絶対的なメンテナンスの悪夢になりました。何らかの理由でジェネレーターまたは出力を変更しなければならなかったとき(これは数回発生しました)、予想されるすべての出力ファイルを書き直さなければなりませんでした-そしてそれらは大量にありました。すべてのテストに失敗することを恐れて、出力をまったく変更したくありませんでしたが(これは悪いことでした)、最終的にはそれらを破棄し、代わりに結果のASTでテストを行いました。これは、出力を「大まかに」テストできることを意味しました。たとえば、ifステートメントの生成をテストしたい場合は、生成されたクラスで唯一のifステートメントを見つけて(この一般的なASTのすべてを実行するヘルパーメソッドを作成しました)、それについていくつかのことを確認し、終わり。そのテストはしませんでした クラスの名前の付け方や、追加の注釈やコメントがあるかどうかを気にします。テストがより集中されたので、これは非常にうまく機能することになりました。欠点は、テストがコードとより緊密に結合されていることです。そのため、Eclipseコンパイラ/ ASTライブラリを取り除いて他のものを使用したい場合は、すべてのテストを書き直す必要があります。結局、コード生成は時間とともに変化するので、私はその代償を払っても構わないと思っていました。

また、統合テスト(生成されたコードをターゲット言語で実際にコンパイルして実行するテスト)にも大きく依存しています。これらのタイプのテストは、単体テストよりもはるかに多く、より有用でより多くの問題をキャッチできるように見えたためです。

訪問者のテストについても、さらに統合スタイルのテストを行います。非常に小さい/特定のJavaソースファイルを取得し、Eclipseコンパイラでロードし、訪問者の1人を実行して結果を確認します。Eclipseコンパイラーを呼び出さずにテストする他の唯一の方法は、AST全体をモックアウトすることですが、これは実現不可能でした。ほとんどの訪問者は重要であり、メインクラスからアノテーションを読み取るため、完全に構築された有効なJavaASTが必要でした。 。ほとんどの訪問者は、小さなOpenCLコードフラグメントを生成したか、単体テストで検証できるデータ構造を構築したため、この方法でテスト可能でした。

はい、私のすべてのテストはEclipseコンパイラと非常に緊密に結合されています。しかし、私たちが書いている実際のソフトウェアもそうです。他のものを使用するということは、とにかくプログラム全体を書き直さなければならないことを意味するので、それは私たちがかなり喜んで支払う代償です。解決策は1つではないと思います。密結合のコストと、テストの保守性/シンプルさを比較検討する必要があります。

また、デフォルト設定でEclipseコンパイラをセットアップする、メソッドツリーの本体ノードを引き出すコードなど、かなりの量のテストユーティリティコードがあります。テストをできるだけ小さくするようにしています(これはおそらく常識ですが、おそらく言及する価値があります)。


(コメントへの応答における以下の編集/追加-コメントの応答よりも読みやすく/フォーマットしやすい)

「私は統合テストにも大きく依存しています。生成されたコードをターゲット言語で実際にコンパイルして実行するテストです」これらのテストは実際に何をしましたか?output.txtテストとはどのように異なりますか?

(もう一度編集:質問を読み直した後、私たちのアプローチは同じであることがわかりましたので、これは無視してください)

統合テストでは、ソースコードを生成して最初に行った期待される出力と比較するのではなく、OpenCLコードを生成し、コンパイルして実行します。生成されたすべてのコードが出力を生成し、その出力が比較されます。

たとえば、ジェネレータが正しく機能する場合、2つのバッファの値を合計して3番目のバッファに値を配置するOpenCLコードを生成するJavaクラスがあります。最初は、予想されるOpenCLコードを使用してテキストファイルを作成し、テストでそれを比較していました。ここで、統合テストはコードを生成し、OpenCLコンパイラーを介して実行し、実行してから、テストで値をチェックします。

「訪問者のテストについても、統合スタイルのテストをさらに行います。非常に小さい/特定のJavaソースファイルを取得し、Eclipseコンパイラでロードし、訪問者の1人を実行して、結果を確認します。」訪問者の1人と一緒に、またはテストしたい訪問者まですべての訪問者を実行しますか?

訪問者のほとんどは、互いに独立して実行することができます。可能な場合は、テストしている訪問者のみで実行するか、他の訪問者に依存している場合は、最小限の訪問者セットが必要です(通常は他の1人だけが必要です)。訪問者は互いに直接話すことはありませんが、渡されるコンテキストオブジェクトを使用します。これらは、テストで人為的に構築して、物事を既知の状態にすることができます。

他の質問ですが、このプロジェクトではモックを使用していますか?さらに、他のプロジェクトでモックを定期的に使用していますか?私は話している相手について明確な画像を取得しようとしています:P

このプロジェクトでは、テストの約5%でモックを使用しますが、おそらくそれよりも少なくなります。そして、私はEclipseコンパイラーのものをモックアウトしません。

モックの問題は、私がモックアウトしているものをよく理解する必要があるということですが、Eclipseコンパイラの場合はそうではありません。呼び出されるビジターメソッドはたくさんありますが、どちらを呼び出すべきかわからない場合があります(たとえば、ExtendedStringLiteralにアクセスするか、StringLiteralにアクセスして文字列リテラルを呼び出しますか?)これをモックアウトして、どちらかを想定した場合、これは現実に対応していない可能性があり、テストに合格してもプログラムは失敗します-望ましくありません。私たちが行う唯一のモックは、アノテーションプロセッサAPIのカップル、Eclipseコンパイラアダプタのカップル、および独自のコアクラスのいくつかです。

Java EEのものなどの他のプロジェクトでは、より多くのモックが使用されましたが、私はまだそれらの熱心なユーザーではありません。APIがより明確に、理解され、予測可能であるほど、モックの使用を検討する可能性が高くなります。

プログラムの最初のフェーズは、通常のコンパイラと同じです。ソースファイルから情報を抽出し、(大きくて複雑な!)シンボルテーブルを埋めます。これをシステムテストするにはどうしますか?理論的には、ソースファイルとsymbolTableに関するすべての情報を含むsymbolTable.txt(または.xmlなど)を使用してテストを作成できますが、それを行うのは少し複雑だと思います。これらの統合テストのそれぞれは、達成するのが複雑なことになるでしょう!

一度にロット全体ではなく、シンボルテーブルの小さなビットをテストするアプローチをとろうと思います。Javaツリーが正しく構築されているかどうかをテストしている場合、次のようになります。

  • ifステートメントのための1つのテスト:

    • 1つのifステートメントを含む1つのメソッドを持つソースコードを持っている
    • このソースからシンボルテーブル/ツリーを構築します
    • メインクラスのメソッド本体のみからステートメントツリーを引き出します(メソッド本体が1より大きいか、メソッド本体がない場合、クラスが見つかった場合、メソッド本体の最上位ステートメントノードの場合はテストに失敗します)
    • ifステートメントのノード属性(条件、本体)をプログラムで比較する
  • 同様のスタイルで、他の種類のステートメントに対して少なくとも1つのテスト。

  • 他のテスト、多分複数のステートメントなど、または必要なものは何でも

このアプローチは統合スタイルのテストですが、各統合テストはシステムのごく一部のみをテストします。

基本的に、私はテストをできるだけ小さく保つように努めます。ツリーのビットを引き出すためのテストコードの多くは、テストクラスを小さく保つためにユーティリティメソッドに移動できます。

シンボルテーブルを使用して、対応するソースファイルを出力するきれいなプリンターを作成できるかもしれないと思いました(すべてがうまくいけば、元のソースファイルと同じようになります)。問題は、元のファイルが私のきれいなプリンターが印刷するものとは異なる順序である可能性があることです。このアプローチでは、ワームの別の缶を開けているだけかもしれません。私はコードの一部を執拗にリファクタリングしてきましたが、バグが目立ち始めています。軌道に乗せるには、統合テストが本当に必要です。

それはまさに私が取ったアプローチです。しかし、私のシステムでは、ものの順序はあまり変わりません。基本的にJavaASTノードに応答してコードを出力するジェネレーターがありますが、ジェネレーターが再帰的に自分自身を呼び出すことができるという点で少し自由があります。たとえば、JavaIfステートメントのASTノードに応答して起動される'if'ジェネレーターは'if('を書き出すことができます。次に、他のジェネレーターに条件をレンダリングするように依頼し、次に'){'を書き込み、他のジェネレーターに書き込みを依頼します。本体を取り出して、「}」と書きます。

于 2011-08-01T05:04:14.933 に答える