跳至主要内容

测试自定义查询

您可以为 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定义 CLI 将使用哪种语言从存储在此 CodeQL 包中的代码文件创建测试数据库。有关更多信息,请参见“使用 CodeQL 包自定义分析”。

您可能会发现查看 CodeQL 仓库 中查询测试的组织方式很有用。每种语言都有一个src目录,ql/<language>/ql/src,其中包含用于分析代码库的库和查询。在src目录旁边,还有一个test目录,其中包含这些库和查询的测试。

每个test目录都配置为一个测试 CodeQL 包,其中包含两个子目录

  • query-tests一系列包含存储在src目录中的查询测试的子目录。每个子目录都包含测试代码和一个 QL 参考文件,该文件指定要测试的查询。
  • library-tests一系列包含 QL 库文件的测试的子目录。每个子目录都包含作为库单元测试编写的测试代码和查询。

创建qlpack.yml文件后,需要确保所有依赖项都已下载并可供 CLI 使用。为此,请在与qlpack.yml文件相同的目录中运行以下命令

codeql pack install

这将生成一个codeql-pack.lock.yml文件,该文件指定运行此包中查询所需的所有传递依赖项。此文件应检入到源代码控制中。

为查询设置测试文件

对于要测试的每个查询,应在测试 CodeQL 包中创建一个子目录。然后,在运行测试命令之前,将以下文件添加到子目录

  • 查询引用文件(.qlref 文件)定义了要测试的查询位置。该位置相对于包含查询的 CodeQL 包的根目录定义。通常,这是在测试包的 dependencies 块中指定的 CodeQL 包。更多信息,请参见“查询引用文件”。

    如果要测试的查询存储在测试目录中,则无需添加查询引用文件,但通常最好将查询与测试分开存储。唯一的例外是 QL 库的单元测试,这些测试往往存储在测试包中,与生成警报或路径的查询分开。

  • 要运行查询的示例代码。这应包含一个或多个包含查询旨在识别的代码示例的文件。

您还可以通过创建扩展名为 .expected 的文件来定义运行查询时针对示例代码所期望看到的结果。或者,您可以让测试命令为您创建 .expected 文件。

有关如何创建和测试查询的示例,请参见下面的示例

注意

您的 .ql.qlref.expected 文件必须具有一致的名称。

  • 如果要在测试命令中直接指定 .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 将线程数与逻辑处理器数匹配。

有关测试查询时可以使用的所有选项的完整详细信息,请参见“test run”。

示例

以下示例演示如何为搜索 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 来创建 Java 测试的 CodeQL 包,并更新 dependencies 以匹配您的自定义查询 CodeQL 包的名称。

    以下 qlpack.yml 文件指出 my-github-user/my-query-tests 依赖于版本大于或等于 1.2.3 且小于 2.0.0 的 my-github-user/my-custom-queries。它还声明 CLI 在创建测试数据库时应使用 Java extractortests: . 行声明,当使用 --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-custom-queries 的 CodeQL 包的顶级目录中,该包被声明为 my-query-tests 的依赖项。因此,EmptyThen.qlref 应该只包含 EmptyThen.ql

  8. 创建要测试的代码片段。以下 Java 代码在第三行包含一个空的 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 目录中找到一个测试。

  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 文件的统一差异。此信息可能足以调试微不足道的测试失败。

对于难以调试的失败,您可以将 EmptyThen.testproj 导入 CodeQL for VS Code,执行 EmptyThen.ql,并在 Test.java 示例代码中查看结果。更多信息,请参见“管理 CodeQL 数据库”。

进一步阅读