跳至主要内容

自定义查询测试

验证您的自定义 CodeQL 查询,并在新版本 CodeQL CLI 发布后导致代码扫描结果受影响之前捕获破坏性更改。

谁可以使用此功能?

CodeQL 可用于以下仓库类型

自定义查询测试

CodeQL 提供了一个简洁的测试框架,用于对查询进行自动化回归测试。测试您的查询,以确保其行为符合预期。

在查询测试期间,CodeQL 会将用户期望查询产生的结果与实际产生的结果进行比较。如果期望结果与实际结果不一致,查询测试将失败。要修复该测试,您需要反复修改查询和期望结果,直至实际结果与期望结果完全匹配。本章节展示了如何创建测试文件并使用 test run 子命令执行测试。

为自定义查询设置测试 CodeQL 包

所有 CodeQL 测试必须存放在特殊的 “test” 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 文件的路径。
  • 指向一个目录的路径,CLI 将递归搜索其中的 .ql.qlref 文件。

您还可以指定

  • --threads: 可选参数,指定运行查询时使用的线程数。默认值为 1。您可以指定更多线程以加快查询执行。设为 0 时,线程数将匹配逻辑处理器的数量。

有关测试查询时可使用的全部选项的完整说明,请参阅 test run

示例

下面的示例展示了如何为一个搜索 Java 代码中 if 语句且其 then 块为空的查询设置测试。示例包括将自定义查询及对应测试文件添加到 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 文件。

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

    有关 CodeQL 包的详细信息,请参阅 使用 CodeQL 包自定义分析

  4. 通过在 custom-queries/java/tests 中添加以下内容的 qlpack.yml 文件,为您的 Java 测试创建一个 CodeQL 包,并将 dependencies 更新为对应的自定义查询 CodeQL 包名称。

    下面的 qlpack.yml 文件声明 my-github-user/my-query-tests 依赖于 my-github-user/my-custom-queries,版本范围为 ≥ 1.2.3 且 < 2.0.0。它还声明在创建测试数据库时,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 文件的统一 diff。此信息通常足以调试简单的测试失败。

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

延伸阅读

© . This site is unofficial and not affiliated with GitHub, Inc.