Skip to main content

カスタム クエリのテスト

CodeQL クエリのテストを設定して、CodeQL CLI の新しいリリースでも期待される結果が返されるようにすることができます。

この機能を使用できるユーザーについて

CodeQL は、次の種類のリポジトリで使用できます:

カスタム クエリのテストについて

CodeQL には、クエリの自動回帰テスト用の簡単なテスト フレームワークが用意されています。 クエリをテストして、期待どおりに動作することを確認します。

クエリ テストで、CodeQL は、ユーザーがクエリで生成されると期待する結果と、実際に生成されたものを比較します。 期待される結果と実際の結果が異なる場合、クエリ テストは失敗します。 テストを修正するには、実際の結果と期待される結果が正確に一致するまで、クエリと期待される結果を反復処理する必要があります。 このトピックでは、テスト ファイルを作成し、test run サブコマンドを使用してそれに対してテストを実行する方法について説明します。

カスタム クエリのテスト CodeQL パックの設定

すべての CodeQL テストは、特別な "テスト" CodeQL パックに格納する必要があります。 つまり、次を定義する qlpack.yml ファイルを含むテスト ファイルのディレクトリです。

name: <name-of-test-pack>
version: 0.0.0
dependencies:
  <codeql-libraries-and-queries-to-test>: "*"
extractor: <language-of-code-to-test>

dependencies の値によって、テストするクエリを含む CodeQL パックが指定されます。 通常、これらのパックはソースから解決されるため、パックの固定バージョンを指定する必要はありません。 extractor は、この CodeQL パックに格納されているコード ファイルからテスト データベースを作成するために CLI が使用する言語を定義します。 詳しくは、「CodeQL パックを使った分析のカスタマイズ」を参照してください。

CodeQL リポジトリでのクエリ テストの編成方法を確認すると役立つ場合があります。 各言語には、コードベースを分析するためのライブラリとクエリを含む src ディレクトリ、ql/<language>/ql/src があります。 src ディレクトリと共に、これらのライブラリとクエリのテストを含む test ディレクトリがあります。

test ディレクトリは、次の 2 つのサブディレクトリを含むテスト CodeQL パックとして構成されています。

  • query-testssrc ディレクトリに格納されているクエリのテストを含む一連のサブディレクトリです。 各サブディレクトリには、テスト コードと、テストするクエリを指定する QL 参照ファイルが含まれています。
  • library-tests は、QL ライブラリ ファイルのテストを含む一連のサブディレクトリです。 各サブディレクトリには、ライブラリの単体テストとして記述されたテスト コードとクエリが含まれています。

qlpack.yml ファイルができたら、すべての依存関係がダウンロードされ、CLI で使えるようにする必要があります。 これを行うには、qlpack.yml ファイルと同じディレクトリで次のコマンドを実行します。

codeql pack install

その結果、このパックでクエリを実行するために必要な一時的依存関係を全部指定する codeql-pack.lock.yml ファイルが作られます。 このファイルはソース管理にチェックインしてください。

クエリのテスト ファイルの設定

テストするクエリごとに、テスト CodeQL パックにサブディレクトリを作成する必要があります。 次に、test コマンドを実行する前に、次のファイルをサブディレクトリに追加します。

  • テストするクエリの場所を定義するクエリ参照ファイル (.qlref ファイル)。 場所は、クエリを含む CodeQL パックのルートを基準にして定義されます。 通常、これはテスト パックの dependencies ブロックで指定された CodeQL パックです。 詳しくは、「クエリ参照ファイル」を参照してください。

    テストするクエリがテスト ディレクトリに格納されている場合は、クエリ参照ファイルを追加する必要はありませんが、通常はテストとは別にクエリを格納することをお勧めします。 唯一の例外が QL ライブラリの単体テストで、これはアラートやパスを生成するクエリとは別に、テスト パックに格納される傾向があります。

  • クエリを実行するサンプル コード。 これは、クエリで識別するように設計されたサンプル コードを含む 1 つ以上のファイルで構成されている必要があります。

また、.expected という拡張子を持つファイルを作成することで、サンプル コードに対してクエリを実行するときに期待される結果を定義することもできます。 または、test コマンドをそのまま使用して .expected ファイルを作成することもできます。

クエリを作成してテストする方法の例については、次のをご覧ください。

注: .ql.qlref.expected のファイルの名前は一貫している必要があります。

  • test コマンドで .ql ファイル自体を直接指定する場合は、対応する .expected ファイルと同じベース名が必要です。 たとえば、クエリが MyJavaQuery.ql の場合、期待される結果ファイルは MyJavaQuery.expected である必要があります。

  • コマンドで .qlref ファイルを指定する場合は、対応する .expected ファイルと同じベース名が必要ですが、クエリ自体の名前が異なるものにできます。

  • サンプル コード ファイルの名前は、他のテスト ファイルと一致している必要はありません。 .qlref (または .ql) ファイルとサブディレクトリの横にあるすべてのサンプル コード ファイルが、テスト データベースの作成に使用されます。 そのため、わかりやすくするために、互いの先祖であるディレクトリにはテスト ファイルを保存しないことをお勧めします。

実行中 codeql test run

CodeQL クエリ テストは、次のコマンドを実行して実行されます。

codeql test run <test|dir>

<test|dir> 引数は次のいずれかになります。

  • .ql ファイルのパス。
  • .ql ファイルを参照する .qlref ファイルのパス。
  • .ql.qlref のファイルを再帰的に検索するディレクトリのパス。

次を指定することもできます。

  • --threads: 必要に応じて、クエリの実行時に使用するスレッドの数。 既定のオプションは 1 です。 クエリの実行を高速化するために、より多くのスレッドを指定できます。 0 を指定すると、スレッドの数が論理プロセッサの数と照合されます。

クエリのテスト時に使うことができるすべてのオプションについて詳しくは、「テストの実行」をご覧ください。

次の例は、Java コードで空の then ブロックを持つ if ステートメントを検索するクエリのテストを設定する方法を示します。 これには、CodeQL リポジトリのチェックアウト外で CodeQL パックを分離するために、カスタム クエリと対応するテスト ファイルを追加する手順が含まれています。 これにより、CodeQL ライブラリを更新したり、別のブランチをチェックアウトしたりしても、カスタム クエリとテストは上書きされなくなります。

クエリとテスト ファイルを準備する

  1. クエリを開発します。 たとえば、次の単純なクエリでは、Java コード内の空の then ブロックが検索されます。

    import java
    
    from IfStmt ifstmt
    where ifstmt.getThen() instanceof EmptyStmt
    select ifstmt, "This if statement has an empty then."
    
  2. 他のカスタム クエリを含むクエリを、ディレクトリ内の EmptyThen.ql という名前のファイルに保存します。 たとえば、custom-queries/java/queries/EmptyThen.ql のようにします。

  3. カスタム クエリを CodeQL パックにまだ追加していない場合は、ここで CodeQL パックを作成します。 たとえば、カスタム Java クエリが custom-queries/java/queries に格納されている場合は、次の内容の qlpack.yml ファイルを custom-queries/java/queries に追加します。

    name: my-custom-queries
    dependencies:
      codeql/java-queries: "*"
    

    CodeQL パックについて詳しくは、「CodeQL パックを使った分析のカスタマイズ」をご覧ください。

  4. 次の内容の qlpack.yml ファイルを custom-queries/java/tests に追加して、カスタム クエリの CodeQL パックの名前と一致するように dependencies を更新して、Java テストの CodeQL パックを作成します。

    次の qlpack.yml ファイルは、my-github-user/my-query-tests が 1.2.3 以上で 2.0.0 未満のバージョンの my-github-user/my-custom-queries に依存していることを示しています。 また、テスト データベースの作成時に CLI で Java extractor を使用する必要があることを宣言します。 tests: . 行では、--strict-test-discovery オプションを指定して codeql test run を実行する際に、パック内のすべての .ql ファイルをテストとして実行する必要があることを宣言します。 通常、テスト パックに version プロパティは含まれません。 これにより、それらが誤って発行されるのを防ぐことができます。

    name: my-github-user/my-query-tests
    dependencies:
      my-github-user/my-custom-queries: ^1.2.3
    extractor: java-kotlin
    tests: .
    
  5. テスト ディレクトリのルートで codeql pack install を実行します。 その結果、このパックでクエリを実行するために必要な一時的依存関係を全部指定する codeql-pack.lock.yml ファイルが作られます。

  6. Java テスト パック内で、EmptyThen.ql に関連付けられているテスト ファイルを格納するディレクトリを作成します。 たとえば、custom-queries/java/tests/EmptyThen のようにします。

  7. 新しいディレクトリで、EmptyThen.qlref を作成 して EmptyThen.ql の場所を定義します。 クエリのパスは、クエリを含む CodeQL パックのルートを基準にして指定する必要があります。 この場合、クエリは、my-query-tests の依存関係として宣言されている my-custom-queries という名前の CodeQL パックの最上位ディレクトリにあります。 したがって、EmptyThen.qlref には EmptyThen.ql のみを含める必要があります。

  8. テストするコード スニペットを作成します。 次の Java コードには、3 行目に空の if ステートメントが含まれています。 それを custom-queries/java/tests/EmptyThen/Test.java に保存します。

    class Test {
      public void problem(String arg) {
        if (arg.isEmpty())
          ;
        {
          System.out.println("Empty argument");
        }
      }
    
      public void good(String arg) {
        if (arg.isEmpty()) {
          System.out.println("Empty argument");
        }
      }
    }
    

テストを実行する

テストを実行するには、custom-queries ディレクトリに移動して codeql test run java/tests/EmptyThen を実行します。

テストを実行すると、次のようになります。

  1. EmptyThen ディレクトリ内の 1 つのテストが検索されます。

  2. EmptyThen ディレクトリに格納されている .java ファイルから CodeQL データベースが抽出されます。

  3. EmptyThen.qlref ファイルによって参照されるクエリがコンパイルされます。

    この手順が失敗した場合、原因は CLI でカスタム CodeQL パックが見つからないためです。 コマンドを再実行し、カスタム CodeQL パックの場所を指定します。次に例を示します。

    codeql test run --search-path=java java/tests/EmptyThen

    構成の一環としての検索パスの保存について詳しくは、「CodeQL の構成ファイルでコマンド オプションを指定する」をご覧ください。

  4. クエリが実行され、EmptyThen.actual 結果ファイルが生成されて、テストが実行されます。

  5. EmptyThen.expected ファイルを確認して、.actual 結果ファイルと比較されます。

  6. テストの結果 (この場合はエラー 0 tests passed; 1 tests failed:) が報告されます。 クエリの期待される結果を含むファイルをまだ追加していないため、テストは失敗しました。

クエリ テストの出力を表示する

CodeQL では、EmptyThen ディレクトリに次のファイルが生成されます。

  • EmptyThen.actual。クエリによって生成された実際の結果を含むファイルです。
  • EmptyThen.testproj。VS Code に読み込み、失敗したテストのデバッグに使用できるテスト データベースです。 テストが正常に完了すると、このデータベースはハウスキープ処理で削除されます。 --keep-databases オプションを使用して test run を実行することで、この手順をオーバーライドできます。

この場合、エラーが予想されますが、修正は簡単です。 EmptyThen.actual ファイルを開くと、テストの結果が表示されます。


| Test.java:3:5:3:22 | stmt | This if statement has an empty then. |

このファイルには、クエリが出力する select 句の各部分の個別の列と共に、結果の場所の列を含むテーブルが含まれています。 結果は期待どおりであるため、ファイル拡張子を更新して、このテストで期待される結果 (EmptyThen.expected) として定義できます。

ここでテストを再実行すると、出力は似たものになりますが、完了時に All 1 tests passed. が報告されます。

クエリの結果が変更された場合 (たとえば、クエリの select ステートメントを変更した場合)、テストは失敗します。 失敗した結果の場合、CLI 出力には EmptyThen.expectedEmptyThen.actual のファイルの統合された差分が含まれます。 この情報で、簡単なテスト エラーを十分デバッグできる場合があります。

デバッグが困難なエラーの場合は、VS Code の CodeQL に EmptyThen.testproj をインポートし、EmptyThen.ql を実行して、結果を Test.java サンプル コードで表示できます。 詳しくは、「CodeQL データベースの管理」を参照してください。

参考資料