自定义查询测试
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 库或检出其他分支时,不会覆盖您的自定义查询和测试。
准备查询和测试文件
-
编写查询。例如,以下简易查询可找出 Java 代码中空的
then块。import java from IfStmt ifstmt where ifstmt.getThen() instanceof EmptyStmt select ifstmt, "This if statement has an empty then." -
将查询保存为名为
EmptyThen.ql的文件,放在与其他自定义查询相同的目录中。例如,custom-queries/java/queries/EmptyThen.ql。 -
如果尚未将自定义查询加入 CodeQL 包,请立即创建一个 CodeQL 包。例如,若您的自定义 Java 查询存放在
custom-queries/java/queries,请在该目录下添加一个包含以下内容的qlpack.yml文件。name: my-custom-queries dependencies: codeql/java-queries: "*"有关 CodeQL 包的详细信息,请参阅 使用 CodeQL 包自定义分析。
-
通过在
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 应使用 Javaextractor。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: . -
在测试目录的根目录下运行
codeql pack install。这将生成一个codeql-pack.lock.yml文件,列出运行此包中查询所需的全部传递依赖项。 -
在 Java 测试包中,创建一个目录以存放与
EmptyThen.ql关联的测试文件。例如,custom-queries/java/tests/EmptyThen。 -
在新建目录中创建
EmptyThen.qlref,用于定义EmptyThen.ql的位置。查询路径须相对于包含该查询的 CodeQL 包根目录指定。在本例中,查询位于名为my-custom-queries的 CodeQL 包的顶层目录中,该包被声明为my-query-tests的依赖。因此,EmptyThen.qlref只需包含EmptyThen.ql。 -
创建一个代码片段进行测试。以下 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。
当测试运行时,它会
-
在
EmptyThen目录中发现一个测试。 -
从
EmptyThen目录中的.java文件提取 CodeQL 数据库。 -
编译
EmptyThen.qlref文件所引用的查询。如果此步骤失败,说明 CLI 找不到您的自定义 CodeQL 包。请重新运行命令并指定自定义 CodeQL 包的位置,例如
codeql test run --search-path=java java/tests/EmptyThen有关将搜索路径保存为配置的一部分的信息,请参阅 在 CodeQL 配置文件中指定命令选项。
-
通过运行查询执行测试,并生成
EmptyThen.actual结果文件。 -
检查是否存在
EmptyThen.expected文件,以与.actual结果文件进行比较。 -
报告测试结果——本例为失败:
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.expected 与 EmptyThen.actual 文件的统一 diff。此信息通常足以调试简单的测试失败。
对于更难调试的失败,您可以将 EmptyThen.testproj 导入到 VS Code 的 CodeQL 插件中,执行 EmptyThen.ql,并在示例代码 Test.java 中查看结果。更多信息请参阅 管理 CodeQL 数据库。