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 定义 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 的文件来定义针对示例代码运行查询时希望看到的结果。 或者,可以保留 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 使线程数与逻辑处理器数匹配。

有关测试查询时可使用的所有选项的完整详细信息,请参阅“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. 通过向 custom-queries/java/tests 添加包含以下内容的 qlpack.yml 文件,更新 dependencies 以匹配自定义查询的 CodeQL 包的名称,为 Java 测试创建 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. 检查要与 .actual 结果文件进行比较的 EmptyThen.expected 文件。

  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 导入 VS Code 的 CodeQL 中,执行 EmptyThen.ql,并在 Test.java 示例代码中查看结果。 有关详细信息,请参阅“管理 CodeQL 数据库”。

其他阅读材料