Bazel 测试

范围

本文档介绍了如何定义、构建和运行仅存在于 Bazel 图中的测试。

Fuchsia 支持满足以下要求的 Bazel 主机测试:

  • 这些测试必须只能使用 Bazel 构建;也就是说,它们不依赖于先前 Ninja 调用生成的工件。

  • 这些测试仅包含可在未连接到任何 Fuchsia 设备或模拟器的兼容主机系统上运行的二进制文件和运行时文件。

Fuchsia 还支持 Bazel 设备测试,这些测试使用 fx_test() 声明,并在 Fuchsia 设备或模拟器上运行。这些模块同样要求只能使用 Bazel 构建,此外还要求:

  • 除了 environments(默认为 build 的默认测试环境)、build_only、max_log_severity(默认为 WARN)和 test_type(用于在非封闭测试领域(例如 "starnix" 或 "system")中运行)之外,测试规范字段(超时等)尚无法针对每个测试进行自定义。

fxbug.dev/564574581 跟踪 Bazel 中目标测试的剩余工作。

在 Bazel 中定义 Fuchsia 主机测试

常规 Bazel 测试规则(例如 rustc_test、go_test)与 Fuchsia 测试运行程序不兼容。fx test 无法看到这些测试,基础架构构建者也不会运行这些测试。

因此,如需在 Bazel 中定义与 Fuchsia 兼容的主机测试,请执行以下操作:

  • 使用 //build/bazel/rules/host_tests:host_<xxx>_test.bzl 中特定于语言的主机测试封装容器,例如 host_rustc_test()、host_go_test() 等。

    这些规则旨在直接替换常规 Bazel 测试规则,并带有 test_data、test_args 等额外参数,以便 Fuchsia 测试运行程序可以使用正确的运行时环境和参数调用它们。

  • 只有在没有适用于您的测试的特定语言封装容器时,才使用 //build/bazel/rules/host_tests:host_test.bzl 中的通用 host_test() 封装容器。例如:

    load("//build/bazel/rules/host_tests:host_test.bzl", "host_test")
    load("@rules_sh//sh:sh_binary.bzl", "sh_binary")
    
    sh_binary(
        name = "test_bin",
        ...
    )
    
    host_test(
        name = "shell_test",
        binary = ":test_bin",
    )
    
  • 请勿假设测试二进制文件的执行位置(例如,来自 out/default),因为它是从确切路径不可预测的目录启动的。

    Bazel 主机测试必须是封闭的,并明确列出其所有运行时依赖项,就像 GN 主机测试一样。运行时依赖项可以通过以下机制之一提供:

    • 作为 test_data 依赖项添加到 host_test_data_files() 或 host_test_data_map() 目标(请参阅 //build/bazel/rules/host_tests:host_test_data.bzl)。这些依赖项使文件在运行时可在相对于测试执行目录的固定位置使用,类似于 GN host_test_data() 模板。

    • 作为 host_xxx_test() 目标的 data 属性。 在这种情况下,这些文件将添加到测试的 runfile 中,并且可以使用 runfile 库进行访问。

  • 使用 test_suite() 而不是 filegroup() 来定义测试组。 测试套件可以依赖于测试和其他测试套件。

将 Bazel 主机测试导出到 fx test 和基础架构构建器

仅定义 host_xxx_test() 目标不会使其对 fx test 可见,更重要的是,对基础架构构建者不可见。

如需让 fx test 和基础架构构建器能够看到 Bazel 主机测试,请在 GN 中编写一个 bazel_test_suite 目标,其中列出一个或多个 Bazel 主机测试或套件的 Bazel 标签,并将该目标添加到 GN 中的“tests”组。例如:

//src/foo/bar/BUILD.bazel:

load("//build/bazel/platforms:constraints.bzl", "HOST_OS_CONSTRAINTS")

test_suite(
  name = "host_tests",
  host_tests = [
    "//src/foo/bar/lib:lib_tests",
    "//src/foo/bar/util:util_tests",
  ]
  target_compatible_with = HOST_OS_CONSTRAINTS,
)

//src/foo/bar/BUILD.gn:

import("//build/bazel/bazel_test_suite.gni")

bazel_test_suite("bazel_tests") {
  host_tests = [
    # This references the target in BUILD.bazel.
    "//src/foo/bar:host_tests",
  ]
}

group("tests") {
  deps = [
    ":bazel_tests",
    ...
  ]
}

这将导致 Bazel 测试的条目显示在 out/default/tests.json 中。如有疑问,请在 fx set 或 fx gen 之后检查此文件的内容。Bazel 测试的 label 字段以 @ 开头。

在开发期间,您可以在定义测试目标后,按照上述说明导出测试目标之前,构建并运行测试目标。请参阅下面的直接使用 Bazel 运行测试部分。

最佳做法

建议避免对 Bazel test_suite 目标进行深度嵌套。而是应该为每个源代码子区域(例如主机工具或 Fuchsia 组件)仅使用一个 test_suite Bazel 目标和一个对应的 bazel_test_suite GN 目标。

在 Bazel 中定义 Fuchsia 设备测试

设备测试是 Fuchsia 软件包中的组件,与 GN build 中的组件完全相同。 使用 fx_test_component() 声明测试组件,使用 fx_package() 将其打包,并使用 fx_test() 将其公开为测试:

//src/foo/bar/BUILD.bazel:

load("//build/bazel/rules/packages:fx_packaged_binary.bzl", "fx_packaged_binary")
load(
    "//build/bazel/rules/components:fx_component.bzl",
    "fx_component_manifest",
    "fx_test_component",
)
load("//build/bazel/rules/packages:fx_package.bzl", "fx_package")
load("//build/bazel/rules/testing:fx_test.bzl", "fx_test")

fx_packaged_binary(
    name = "bar_test_packaged_bin",
    testonly = True,
    binary = ":bar_test_bin",
    binary_name = "bar_test",
)

fx_component_manifest(
    name = "bar_test_manifest",
    testonly = True,
    component_name = "bar_test",
    includes = ["//src/sys/test_runners/gtest:default.shard.cml"],
    manifest = "meta/bar_test.cml",
)

fx_test_component(
    name = "bar_test_component",
    compiled_manifest = ":bar_test_manifest",
    component_name = "bar_test",
    deps = [":bar_test_packaged_bin"],
)

fx_package(
    name = "bar_tests_package",
    package_name = "bar_tests",
    test_components = [":bar_test_component"],
)

fx_test(
    name = "bar_tests",
    package = ":bar_tests_package",
    # Optional: defaults to the build's default test environments when omitted.
    environments = [
        "//build/testing/environments:aemu_env",
        "//build/testing/environments:nuc11_env",
    ],
)

test_components 中的每个组件都会成为一个单独的测试,其名称是其软件包网址,例如 fuchsia-pkg://fuchsia.com/bar_tests#meta/bar_test.cm。

如需自定义测试在哪些目标环境中运行,请将 environments 作为环境目标列表传递给 fx_test()。//build/testing/environments 为 //build/testing/environments.gni 中的每个单独环境提供一个目标,并使用相同的名称(例如 :emu_env 或 :nuc11_env)。省略 environments 时,测试会在 build 的默认测试环境(GN 的 basic_envs)中运行,就像未设置 environments 的 GN 测试一样。如需构建测试而不运行测试,请改为设置 build_only = True。

如需向其中一个环境添加基础架构设置(例如标记),请使用 fx_test_environment_variant():

load(
    "//build/bazel/rules/testing:fx_test_environment.bzl",
    "fx_test_environment_variant",
)

fx_test_environment_variant(
    name = "nuc11_isolated_env",
    base = "//build/testing/environments:nuc11_env",
    env_tags = ["e2e-isolated"],
)

新环境应该很少见;大多数测试都应使用 //build/testing/environments 中的某个现有环境。需要新环境时,请将其添加到 //build/testing/environments.gni 和 //build/testing/environments/BUILD.bazel,而不是在测试旁边使用 fx_test_environment() 进行定义。环境的 Swarming 维度是与基础架构的接口,因此它们会保存在一个位置。

如需在非封闭测试领域(例如 "starnix"、"system" 或 "vulkan")中运行测试,请将 test_type 传递给 fx_test_component()(或传递给 fx_test(),以应用于软件包中的所有测试组件)。有效的 test_type 值与 //build/components/fuchsia_test_component.gni 中的 type_moniker_map 匹配。

将 Bazel 设备测试导出到 fx test 和基础架构构建器

与主机测试一样,设备测试也必须在 GN bazel_test_suite() 目标中列出,并使用 target_tests 参数(而非 host_tests)列出:

//src/foo/bar/BUILD.gn:

import("//build/bazel/bazel_test_suite.gni")

bazel_test_suite("bazel_tests") {
  target_tests = [
    # This references the target in BUILD.bazel.
    "//src/foo/bar:bar_tests",
  ]
}

设备测试与主机测试分开跟踪,因为它们是在 fuchsia_platform Bazel 配置(而非主机配置)中查询和构建的。

除了 tests.json 条目之外,这还会写入 ${root_build_dir}/bazel_test_packages.list,用于枚举 build 图中所有 Bazel 测试软件包的软件包清单。完整的 fx build 调用会将此列表合并到 all_package_manifests.list 中,并将其软件包发布到 amber-files 中,而 fx test 也会在按需构建特定测试目标时直接合并此列表。

在两个构建系统中测试库

如果某个库被两个构建系统中的目标使用,请在两个构建系统中运行其测试。当库的用户仅使用 Bazel 时,请仅在 Bazel 中运行其测试。对于仅在 Bazel 中构建的目标(例如已迁移的主机工具、没有剩余 GN 用户的库,以及不向 GN 导出库的叶集成测试套件),其测试仅在 Bazel 中运行。

迁移到 Bazel 的库会一直在 GN 中定义(通常由 bazel2gn 生成),直到没有 GN 目标依赖于它。在此期间,库的 GN 和 Bazel build 可能会存在差异,只有测试才能发现这些差异。编译器标志、build 变体(例如排错程序)和仅限 GN 的 bazel2gn 替换项在两个 build 系统之间都存在差异。在转换期间,某些属性也会被丢弃,而 GN 目标是未迁移的依赖项实际链接到的目标。在两个 build 系统中运行每个测试会消耗额外的 build 时间和测试容量,但只有当库在两个 build 系统中都有用户时才会如此。

实际应用:

  • 在 Bazel 中定义测试,并让 bazel2gn 生成 GN 副本。迁移 BUILD.gn 由 bazel2gn 生成的库时,也请迁移其测试,并且不要在测试上放置 # @bazel2gn:skip。bazel2gn 会转换 rustc_test()、go_test()、host_rustc_test()、host_go_test()、fx_unittest_package() 以及 Rust 规则的 with_unit_tests、with_host_unit_tests 和 test_deps 属性。如上所述,使用 bazel_test_suite() 导出 Bazel 测试,并在列出原始 GN 测试的 GN 测试组中列出生成的 GN 副本。

  • 设备测试软件包在 GN 中有不同的名称。GN 测试软件包及其 Bazel 副本不能共用软件包名称,因为两者都会发布到同一个 Package Repository。bazel2gn 会将 -gn 附加到 fx_unittest_package() 的 GN 副本的软件包名称,而 Bazel 测试会保留原始名称,因此其测试网址在您迁移时不会发生变化。

  • 如果 bazel2gn 尚无法转换测试的打包,而您希望继续在 GN 中运行该测试,请使用 bazel2gn 转换测试二进制文件,并在 BUILD.gn 中使用 package_name = "<name>-gn" 在 ## BAZEL2GN SENTINEL 上方手动编写 GN 测试软件包。这适用于使用 fx_unittest_package(resources = [...]) 的设备测试(将 # @bazel2gn:skip 放在 fx_unittest_package() 和 fuchsia_package_resource_group() 上,而不是放在测试二进制文件上),或适用于自定义 .cml 清单的显式 fx_package() 和 fx_test_component() 目标(bazel2gn 已在没有 # @bazel2gn:skip 的情况下忽略这些目标)。

  • 何时使用 # @bazel2gn:skip 选择退出。默认情况下,如果 bazel2gn 可以自动转换库的测试,请继续在两个构建系统中运行这些测试。在以下情况下,可以对测试目标或测试属性添加 # @bazel2gn:skip(并附上简短注释说明原因),并且仅在 Bazel 中运行:

    • 被测目标仅在 Bazel 中构建(例如,同一 BUILD.bazel 文件中本身具有 # @bazel2gn:skip 的二进制文件或库)。
    • bazel2gn 不支持测试的规则或属性(例如 cc_test()、host_cc_test()、host_py_test()、host_shac_test()、host_test()、具有 data / test_data / test_label 的主机测试、具有 resources 的 fx_unittest_package() 或具有跨软件包 srcs/crate 或冲突 rustc_env 的 rustc_test()),并且在哨兵上方维护手写的 GN 测试目标不值得增加额外的复杂性。
    • 在两个 build 系统中运行或维护测试的成本超过了仅 GN 回归的风险。请根据以下标准来权衡利弊:
    • 目标奇偶校验:库的 BUILD.bazel 是否在依赖项、配置或编译器标志上使用 # @bazel2gn:raw_overwrite、# @bazel2gn:path_overwrite 或 # @bazel2gn:skip,或者它是否是在 GN 中跨多个配置编译的 IDK/SDK 库?如果生成的 GN 目标是 Bazel 目标的 1:1 转换,则 Bazel 测试已经执行了相同的代码。
    • 未迁移的 GN 依赖项:库是否依赖于经常更改的仅限 GN 的目标?预提交受影响测试分析不会从 GN 目标跟踪到 Bazel 测试,因此对仅限 GN 的依赖项所做的更改不会在预提交中触发仅限 Bazel 的测试。
    • 测试权重:快速、独立的单元测试镜像成本较低;缓慢或资源密集型集成测试(或其剩余 GN 用户正在积极迁移的库)非常适合仅在 Bazel 中运行。
  • 如果 Bazel 尚无法表达某个测试,请将其保留在 ## BAZEL2GN SENTINEL 上方的 GN 中,并在注释中说明原因。例如,端到端测试、需要 Ninja 生成的输入的测试、bootfs_test() 以及跨多个 C++ 标准编译的 sdk_cpp_unittest_package()。

  • 移除 GN 副本和库。当库的最后一个 GN 用户迁移到 Bazel 时,请删除该库及其测试的 GN 定义,并从列出这些测试的 GN 测试组中移除这些 GN 测试。

运行 Fuchsia Bazel 设备测试

导出后,通过软件包网址或软件包名称运行 Bazel 设备测试,方法与 GN 设备测试相同:

fx test fuchsia-pkg://fuchsia.com/bar_tests#meta/bar_test.cm

fx test 通过直接调用 Bazel(作为 fx build --fuchsia_platform bazel_label)来构建测试,然后在运行测试之前发布 bazel_test_packages.list 中列出的软件包清单。与 GN 设备测试一样,需要运行软件包服务器和已连接的设备或模拟器。

运行 Fuchsia Bazel 主机测试

本地测试

使用 fx test 运行测试

如需使用 fx test 运行 Bazel 主机测试,必须按照上述将 Bazel 主机测试导出到 fx test 和基础架构构建器部分中的说明导出这些测试。

导出测试后,使用 fx test --host <name_or_label> 在本地构建和运行 Bazel 主机测试,就像使用 GN 定义的主机测试一样。

唯一的区别在于,测试将通过直接调用 Bazel 按需构建,完全跳过 Ninja。

请注意,使用 fx test 时:

  • <name_or_label> 将进行模糊匹配,fx test 将在不明确的情况下报告多个候选地点。

  • <name_or_label> 无法引用 Bazel test_suite() 标签,就像它无法引用 GN group() 标签一样。

如需详细了解如何使用 fx test,请参阅 fx test 用户指南。

直接使用 fx bazel test 运行测试

您可以使用 fx bazel test --config=host <target_pattern> 直接通过 Bazel 在本地构建和运行测试。这不需要额外的管道来使测试对 fx test 可见(如上一部分所述),并且具有以下优势:

  • <target_pattern> 是一组 Bazel 目标模式,可以匹配“给定软件包中的所有测试目标”,在这种情况下,Bazel 会自动跳过非测试目标。

  • <target_pattern> 可以匹配 test_suite() 标签,这便于在开发期间重复运行一组主机测试。

  • bazel test 会缓存测试结果,并且只会重新运行自上次调用以来受最新更改影响的测试。

    (使用 --nocache_test_results 标志可停用此功能)。

  • <target_pattern> 可以引用在tests.json中不可见的标签。 在开发过程中添加新测试时,这会非常方便。

基础架构测试

只有 tests.json 中列出的主机测试才能在基础架构测试机器人上启动。这同样适用于 GN 和 Bazel 主机测试。因此,您必须按照上文的将 Bazel 主机测试导出到 fx test 和基础架构构建器部分,确保它们在 tests.json 中可见,否则它们将不会在基础架构上持续进行测试。

Infra 使用 fint 构建 Bazel 测试,该命令会在运行 ninja 后直接调用 bazel build,以构建 bazel_test_suite GN 目标中列出的所有 Bazel 测试。

在基础架构测试运行程序上运行 GN 和 Bazel 定义的主机测试时,或在收集结果时,两者之间没有明确的区别。具体而言,infra 不会使用 bazel test 运行测试。相反,它会获取 bazel build 生成的二进制文件并直接运行它们,就像运行来自 ninja 的主机测试一样。

调试符号传播

使用调试符号编译的主机测试二进制文件必须注册其未剥离的 ELF 二进制文件,以便工具可以符号化回溯并关联 LLVM 代码覆盖率配置文件 (-profile-correlate=binary)。

如需了解 Fuchsia 如何管理 Bazel 调试符号的一般背景信息,请参阅关于调试符号生成的技术说明。

编写带有调试符号的主机测试规则

通用 host_test() 规则在其 FuchsiaHostTestInfo 提供程序中公开 unstripped_binary 字段:

  • 如果未明确设置 unstripped_binary,host_test() 会检查底层 binary 属性:

    • 如果 binary 提供 DebugPackageInfo(C++ 目标),则使用 DebugPackageInfo.unstripped_file。
    • 如果 binary 提供 CrateInfo(Rust 目标),则使用 CrateInfo.output。
  • 如果测试使用封装容器脚本启动器(例如 host_rustc_test() 中的 rust_test_parser),则宏必须通过 unstripped_binary 明确转发原始未剥离的二进制文件目标:

    host_test(
        name = name,
        binary = wrapper_script,
        unstripped_binary = ":" + binary_name,
        ...
    )
    

传播到基础设施和 debuginfod

将 Bazel 主机测试导出到 GN build 时:

  1. 测试查询:bazel_tests_utils.py 使用 //build/bazel/starlark/FuchsiaHostTestInfo.cquery 运行 bazel cquery,为所有已注册的主机测试提取 unstripped_binary 的 execroot 路径。

  2. 主机测试清单:路径会相对于 Ninja build 目录进行标准化,并写入 ${root_build_dir}/bazel_host_tests.debug_symbols.json。

  3. GN Build API 集成:BUILD.gn 中的 //:bazel_host_test_suites 会附加指向 bazel_host_tests.debug_symbols.json 的 debug_symbol_manifests 元数据,该元数据由 build_api_module("debug_symbols") 目标使用。

  4. Artifactory 上传:在 CI/CQ build 中,Artifactory 会处理 debug_symbols.json 并将未剥离的 ELF 二进制文件上传到云存储 / debuginfod 服务器,从而允许 covargs 等工具解析 build ID。

本地开发和 fx coverage

在本地运行覆盖率测试时,未提交的二进制文件会生成新的 build ID,这些 ID 不存在于 debuginfod 服务器上。

为了支持本地符号关联,fx coverage 会运行:

python3 ${FUCHSIA_DIR}/build/bazel/scripts/copy_bazel_debug_symbols.py ${FUCHSIA_BUILD_DIR}

此脚本会读取 bazel_host_tests.debug_symbols.json,计算每个 ELF 二进制文件的 GNU build ID,并将未剥离的二进制文件复制或符号链接到 ${root_build_dir}/.build-id/xx/yyyyyyyy.debug。然后,ffx coverage 和 llvm-profdata 等工具会通过 .symbol-index.json 解析这些二进制文件。

常见问题解答

Bazel 测试兼容性

如果 test_suite()(或其任何依赖项)中的任何测试与当前配置不兼容(例如,使用 target_compatible_with),则在评估套件期间,系统会跳过该测试,而不是导致 build 失败。

在命令行中使用 //... 和 :all 等模式时也是如此。

如需了解详情,请参阅不兼容目标的处理。

运行 test_suite() 或使用目标模式时,开发者必须验证相关测试是否报告为 PASSED(而非 SKIPPED)。