Fuchsia 测试运行程序框架

借助 Fuchsia Component Framework,开发者可以使用各种语言和运行时创建组件。Fuchsia 自己的代码使用了各种各样的组件编程语言,包括 C/C++、Rust、Dart 和 Go。

测试运行程序框架使用组件框架运行程序作为各种测试运行时与通用 Fuchsia 协议之间的集成层,用于启动测试并接收测试结果。这是一种包容性的设计,一方面,开发者可以使用自己偏好的语言和测试框架;另一方面,可以针对各种硬件在各种系统上构建和测试 Fuchsia。

测试管理器

test_manager 组件负责在 Fuchsia 设备上运行测试。测试管理器公开了 fuchsia.test.manager.RunBuilder 协议,该协议允许启动测试套件。

每个测试套件都作为测试管理器的子项启动。测试套件由测试管理器提供功能,使其能够在执行工作的同时,确保测试与系统的其余部分保持隔离。例如,密封测试可以记录消息,但无法与沙盒之外的真实系统资源进行交互。测试管理器仅使用测试领域中的一项 capability,即测试套件公开的控制器协议。这样做是为了确保封闭性(测试结果不受其预期沙盒之外的任何内容影响)和隔离性(测试不会相互影响或影响系统的其余部分)。

系统会将测试管理器控制器本身提供给其他组件,以便将测试执行与各种开发者工具集成。然后,您可以使用 fx testffx 等工具启动测试。

测试套件协议

测试套件协议 fuchsia.test.Suite 供测试管理器用于控制测试,例如调用测试用例并收集其结果。

测试作者通常不需要实现此协议。而是依赖于测试运行程序来执行此操作。例如,您可以使用 GoogleTest 框架在 C++ 中编写测试,然后在组件清单中使用 gtest_runner 与测试运行器框架集成。

测试运行程序

一个支持多种语言和运行时的框架

测试运行程序是测试运行程序框架与开发者编写测试时使用的常用语言和框架之间的可重复使用适配器。它们代表测试作者实现 fuchsia.test.Suite 协议,让开发者能够针对所选语言和框架编写符合惯例的测试。

构建规则可以生成简单单元测试的组件清单。为 v2 测试生成的组件清单将根据其 build 定义包含适当的测试运行程序。例如,依赖于 GoogleTest 库的测试可执行文件将在其生成的清单中包含 GoogleTest 运行程序

测试运行程序目录

以下测试运行程序目前可供通用使用:

GoogleTest 运行程序

使用 GoogleTest 框架编写的 C/C++ 测试的运行程序。请对使用 GoogleTest 编写的所有测试使用此方法。

支持常见的 GoogleTest 功能,例如停用测试、仅运行指定的测试、多次运行同一测试等。系统会从测试中捕获标准输出、标准错误和日志。

如需使用此运行程序,请将以下内容添加到组件清单中:

{
    include: [ "//src/sys/test_runners/gtest/default.shard.cml" ]
}

默认情况下,GoogleTest 测试用例会串行运行(一次一个测试用例)。

GoogleTest (Gunit) 运行程序

用于运行使用 GUnit 框架编写的 C/C++ 测试的运行程序。将其用于使用 GoogleTest 的 gUnit 变种编写的所有测试。

支持常见的 GoogleTest 功能,例如停用测试、仅运行指定的测试、多次运行同一测试等。系统会从测试中捕获标准输出、标准错误和日志。

如需使用此运行程序,请将以下内容添加到组件清单中:

{
    include: [ "sys/testing/gunit_runner.shard.cml" ]
}

默认情况下,测试用例会串行运行(一次一个测试用例)。

Rust 运行器

一个用于运行使用 Rust 编程语言编写且遵循 Rust 测试惯用法则的测试的运行程序。将其用于所有惯用 Rust 测试(即使用设置了属性 [cfg(test)] 的模块的测试)。

支持常见的 Rust 测试功能,例如停用测试、仅运行指定的测试、多次运行同一测试等。系统会从测试中捕获标准输出、标准错误和日志。

如需使用此运行程序,请将以下内容添加到组件清单中:

{
    include: [ "//src/sys/test_runners/rust/default.shard.cml" ]
}

默认情况下,Rust 测试用例会并行运行,一次最多 10 个用例。

Go 测试运行程序

用于运行使用 Go 编程语言编写且遵循 Go 测试惯用法则的测试的运行程序。对使用 import "testing" 编写的所有 Go 测试使用此方法。

支持常见的 Go 测试功能,例如停用测试、仅运行指定的测试、多次运行同一测试等。系统会从测试中捕获标准输出、标准错误和日志。

如需使用此运行程序,请将以下内容添加到组件清单中:

{
    include: [ "//src/sys/test_runners/gotests/default.shard.cml" ]
}

默认情况下,Go 测试用例会并行运行,一次最多 10 个用例。

ELF 测试运行程序

最简单的测试运行程序 - 它会等待程序终止,然后报告测试结果:如果程序返回零,则报告测试通过;如果返回任何非零值,则报告测试失败。

如果您的测试是作为 ELF 程序(例如,使用 C/C++ 编写的可执行文件)实现的,但它不使用现有运行程序支持的通用测试框架,并且您不想实现自定义测试运行程序,请使用此测试运行程序。

如需使用此运行程序,请将以下内容添加到组件清单中:

{
    include: [ "sys/testing/elf_test_runner.shard.cml" ]
}

如果您使用树内单元测试 GN 模板,并且尚未使用具有专用测试运行程序的测试框架,请将以下内容添加到构建依赖项中:

fuchsia_unittest_package("my-test-packkage") {
    // ...
    deps = [
        // ...
        "//src/sys/testing/elftest",
    ]
}

控制测试用例的并行执行

使用 fx test 启动测试时,它们可能会按顺序运行每个测试用例,或者并行运行多个测试用例(最多不超过给定限制)。默认的并行行为由测试运行程序决定。如需手动控制要并行运行的测试用例数量,请使用测试规范:

fuchsia_test_package("my-test-pkg") {
  test_components = [ ":my_test_component" ]
  test_specs = {
    # control the parallelism
    parallel = 10
  }
}

多次运行测试

如需多次运行测试,请使用以下命令:

 fx test --count=<n> <test_url>

如果迭代超时,系统将不会执行进一步的迭代。

传递参数

您可以使用 fx test 将自定义参数传递给测试:

fx test <test_url> -- <custom_args>

各个测试运行程序对这些自定义标志有限制:

GoogleTest 运行程序

请注意以下已知行为变更:

--gtest_break_on_failure - 请改用:

fx test --break-on-failure <test_url>

以下标志受限,如果传递了任何标志,测试将失败,因为 fuchsia.test.Suite 提供了可替代它们的等效功能。

  • --gtest_filter - 请改用:
 fx test --test-filter=<glob_pattern> <test_url>

可以多次指定 --test-filter。系统会执行与给定全局通配模式匹配的测试。

  • --gtest_also_run_disabled_tests - 请改用:
 fx test --also-run-disabled-tests <test_url>
  • --gtest_repeat - 请参阅多次运行测试
  • --gtest_output - 不支持发出 gtest JSON 输出。
  • --gtest_list_tests - 不支持列出测试用例。

GoogleTest (Gunit) 运行程序

请注意以下已知行为变更:

--gunit_break_on_failure - 请改用:

fx test --break-on-failure <test_url>

以下标志受限,如果传递了任何标志,测试将失败,因为 fuchsia.test.Suite 提供了可替代它们的等效功能。

  • --gunit_filter - 请改用:
 fx test --test-filter=<glob_pattern> <test_url>

可以多次指定 --test-filter。系统会执行与给定全局通配模式匹配的测试。

  • --gunit_also_run_disabled_tests - 请改用:
 fx test --also-run-disabled-tests <test_url>
  • --gunit_repeat - 请参阅多次运行测试
  • --gunit_output - 不支持发出 gtest json/xml 输出。
  • --gunit_list_tests - 不支持列出测试用例。

Rust 运行器

以下标志受限,如果传递了任何标志,测试将失败,因为 fuchsia.test.Suite 提供了可替代它们的等效功能。

  • <test_name_matcher> - 请改用:
 fx test --test-filter=<glob_pattern> <test_url>

可以多次指定 --test-filter。系统会执行与给定全局通配模式匹配的测试。

  • --nocapture - 默认情况下会输出结果。
  • --list - 不支持列出测试用例。

Go 测试运行程序

请注意以下已知行为变更:

-test.failfast:由于每个测试用例都在不同的进程中执行,因此此标志只会影响子测试。

以下标志受限,如果传递了任何标志,测试将失败,因为 fuchsia.test.Suite 提供了可替代它们的等效功能

  • -test.run - 请改用:
 fx test --test-filter=<glob_pattern> <test_url>

可以多次指定 --test-filter。系统会执行与给定全局通配模式匹配的测试。

与运行时无关、包含运行时的测试框架

Fuchsia 旨在实现包容性,例如,开发者可以使用自己选择的语言和运行时创建组件(及其测试)。测试运行程序框架本身在设计上就与语言无关,各个测试运行程序专门针对特定编程语言或测试运行时,因此支持多种语言。任何人都可以创建和使用新的测试运行程序。

创建新的测试运行程序相对容易,并且可以在不同的运行程序之间共享代码。例如,GoogleTest 运行程序和 Rust 运行程序共享与启动 ELF 二进制文件相关的代码,但在将命令行参数传递给测试和解析测试结果的代码方面有所不同。

临时存储

如需在测试中使用临时存储空间,请将以下内容添加到组件清单中:

{
    include: [ "//src/sys/test_runners/tmp_storage.shard.cml" ]
}

在运行时,您的测试将拥有对 /tmp 的读/写访问权限。测试开始时,此目录中的内容将为空,并会在测试完成后被删除。

未指定自定义清单的测试(而依赖于构建系统生成其组件清单)可以添加以下依赖项:

fuchsia_unittest_package("foo-tests") {
  deps = [
    ":foo_test",
    "//src/sys/test_runners:tmp_storage",
  ]
}

导出自定义文件

如需从测试中导出自定义文件,请使用 custom_artifacts 存储功能。系统会在测试结束时复制 custom_artifacts 的内容。

如需在测试中使用 custom_artifacts,请将以下内容添加到组件清单中:

{
    use: [
        {
            storage: "custom_artifacts",
            rights: [ "rw*" ],
            path: "/custom_artifacts",
        },
    ],
}

在运行时,您的测试将拥有对 /custom_artifacts 的读/写访问权限。测试开始时,此目录中的内容将为空,并会在测试完成后被删除。

请参阅自定义工件测试示例。如需运行该测试,请将 //examples/tests/rust:tests 添加到 build 中,然后运行以下命令:

fx test --ffx-output-directory <output-dir> custom_artifact_user

测试结束后,<output-dir> 将包含测试生成的 artifact.txt 文件。

Hermeticity

在软件测试领域,密封性是指将测试或测试套件与外部因素和依赖项隔离,确保无论周围环境如何变化,测试都能生成一致且可靠的结果。密封测试是自包含的,不依赖于可能意外更改的外部系统或数据,从而导致不稳定或非确定性的测试结果。

密封性并不意味着能防范来自稳定平台或路由功能/API 的攻击。如果 API Surface 对系统中的某些组件不稳定,那么它们对测试和依赖组件也不稳定,并且有助于捕获因对系统 API Surface 进行任何直接或间接更改而导致的回归问题。

能够编写完全可证明的密封测试是 Fuchsia 的测试超能力。测试密封性有两种类型:

  • 功能:测试不会使用提供测试根的父级的任何功能。这些测试无法访问任何可能影响更大系统的系统功能。由于密封测试具有此特性,因此它们可以并行运行,并且不会因串扰或共享状态而出错,这有助于提高测试的稳定性和性能。

  • 软件包:测试不会解析测试软件包之外的任何组件。密封打包的测试与平台软件包没有任何隐式协定。这样,您就可以在不影响系统软件包的情况下更新它们,并避免依赖于不兼容的打包依赖项。

密封性不适用于可供系统中所有组件使用的平台 API/功能。例如

  • 时钟
  • 内核提供的标识符,例如 koid。
  • 向所有组件提供的框架功能,允许客户更改组件管理器状态。虽然这不是严格的密封状态,但确保隔离是组件管理器的责任

测试应谨慎使用这些 API/功能。

除非另有明确说明,否则测试默认是封闭式测试。

适用于测试的密封功能

有些功能可供所有测试使用,且不会违反测试密封性:

协议 说明
fuchsia.boot.WriteOnlyLog 写入内核日志
fuchsia.logger.LogSink 写入 syslog
fuchsia.process.Launcher 从测试软件包启动子进程
fuchsia.diagnostics.ArchiveAccessor 读取测试中组件的诊断输出

这些功能经过精心管理,不会允许测试影响测试环境之外的系统组件或其他测试的行为,因此能够保持密封性。

如需使用这些功能,应向测试的清单文件添加使用声明:

// my_test.cml
{
    use: [
        ...
        {
            protocol: [
              "fuchsia.logger.LogSink"
            ],
        },
    ],
}

测试还提供一些默认的存储功能,这些功能会在测试完成执行后销毁。

存储功能 说明 路径
data 独立的数据存储目录 /data
cache 隔离的缓存存储目录 /cache
tmp 隔离的内存临时存储目录 /tmp

在测试的清单文件中添加使用声明,以使用这些功能。

// my_test.cml
{
    use: [
        ...
        {
            storage: "data",
            path: "/data",
        },
    ],
}

该框架还为所有组件提供了一些功能,并且可供测试组件在需要时使用。

密封组件解析

密封式测试组件会在使用密封式组件解析器的领域中启动。此解析器不允许解析测试软件包之外的网址。这对于强制执行密封性至关重要,因为我们不希望系统或关联的软件包服务器中任意组件的可用性影响测试结果。

如果尝试解析测试软件包中不存在的组件,则会在 syslog 中收到 PackageNotFound 错误和以下消息:

failed to resolve component fuchsia-pkg://fuchsia.com/[package_name]#meta/[component_name]: package [package_name] is not in the set of allowed packages...

您可以通过将测试依赖的所有组件添加到测试软件包中来避免此错误(如需查看实现方法示例,请参阅此 CL),也可以使用子软件包

# BUILD.gn
import("//build/components.gni")


fuchsia_test_package("simple_test") {
  test_components = [ ":simple_test_component" ]
  subpackages = [ "//path/to/subpackage:subpackage" ]
}
 // test.cml
 {
...
    children: [
        {
            name: "child",
            url: "subpackage#meta/subpackaged_component.cm",
        },
    ],
...
}

如需查看使用子软件包的示例,请参阅此 CL

// my_component_test.cml

{
...

    facets: {
        "fuchsia.test": {
            "deprecated-allowed-packages": [ "non_hermetic_package" ],
        },
    },
...
}

非封闭测试

这些测试可以访问测试领域之外的一些预定义功能。非容器化测试从其测试领域外部访问的 capability 称为系统 capability

如需使用系统 capability,测试必须明确标记自己以在非密封区域中运行,如下所示。

# BUILD.gn (in-tree build rule)

fuchsia_test_component("my_test_component") {
  component_name = "my_test"
  manifest = "meta/my_test.cml"
  deps = [ ":my_test_bin" ]

  # This runs the test in "system-tests" non-hermetic realm.
  test_type = "system"
}

与 build 规则集成后,可以按以下方式执行测试

fx test <my_test>

或者,对于非树内开发者

ffx test run --realm <realm_moniker> <test_url>

在上述示例中,realm_moniker 应替换为 /core/testing/system-tests

test_type 的可能值:

说明
chromium Chromium 测试 Realm
ctf CTF 测试王国
device 设备测试
drm DRM 测试
starnix Starnix 测试
system_validation 系统验证测试
system 旧版非密封大区,可访问某些系统功能。
test_arch 测试架构测试
vfs-compliance VFS 合规性测试
vulkan Vulkan 测试

了解如何创建自己的测试王国。

非封闭式旧版测试领域

这些是Test Manager as a Service 推出之前创建的旧版测试领域。我们正在移植这些领域。如果您的测试依赖于其中一个领域,则应明确标记自己以在旧版领域中运行,如下所示。

// my_component_test.cml

{
    include: [
        // Select the appropriate test runner shard here:
        // rust, gtest, go, etc.
        "//src/sys/test_runners/rust/default.shard.cml",

        // This includes the facet which marks the test type as 'starnix'.
        "//src/devices/testing/starnix_test.shard.cml",
    ],
    program: {
        binary: "bin/my_component_test",
    },
    
    use: [
        {
            protocol: [ "fuchsia.vulkan.loader.Loader" ],
        },
    ],
}

该分片在清单文件中包含以下细分:

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [
        "//src/starnix/tests/starnix_test_common.shard.cml",
        "//src/storage/fxfs/test-fxfs/meta/test-fxfs.shard.cml",
    ],
    offer: [
        {
            storage: "data",
            from: "self",
            to: [ "#container" ],
        },
        {
            protocol: "fuchsia.fxfs.CryptManagement",
            from: "#test-fxfs",
            to: [ "#container" ],
        },
    ],
    expose: [
        {
            protocol: "fuchsia.test.Suite",
            from: "self",
        },
    ],
}

fuchsia.test.type 的可能值:

说明
hermetic 密封领域
chromium-system Chromium 系统测试领域
google Google 测试王国

受限日志

默认情况下,如果测试记录的严重级别为 ERROR 或更高,则测试将失败。如需了解详情,请参阅此指南

性能

编写用于启动进程的测试运行程序时,该运行程序需要提供库加载器实现。

测试运行程序通常会在单独的进程中启动单个测试用例,以便在测试用例之间实现更高级别的隔离。不过,这可能会带来巨大的性能开销。为缓解此问题,上面列出的测试运行程序使用了缓存加载器服务,从而减少了每个启动的进程的额外开销。

测试角色

测试领域中的组件可以在测试中扮演各种角色,如下所示:

  • 测试根:测试组件树顶部的组件。测试的网址用于标识此组件,测试管理器将调用此组件公开的 fuchsia.test.Suite 来驱动测试。
  • 测试驱动程序:实际运行测试并实现(直接或通过测试运行程序fuchsia.test.Suite 协议的组件。请注意,测试驱动程序和测试根可能但不一定是同一组件:例如,测试驱动程序可以是测试根的子组件,用于重新公开其 fuchsia.test.Suite
  • Capability 提供程序:提供测试将以某种方式执行的 capability 的组件。该组件可以提供用于测试的功能的“虚构”实现,也可以提供等同于生产环境使用的“真实”实现。
  • 被测组件:执行要测试的某些行为的组件。 这可能与生产环境中的组件相同,也可能是专门为测试编写的组件,用于模拟生产环境中的行为。

问题排查

本部分介绍了您在使用 Test Runner 框架开发测试组件时可能会遇到的常见问题。如果某个测试组件无法运行,您可能会在 fx test 中看到如下错误:

Test suite encountered error trying to run tests: getting test cases
Caused by:
    The test protocol was closed. This may mean `fuchsia.test.Suite` was not configured correctly.

如需解决此问题,请尝试以下方案:

测试使用了错误的测试运行程序

如果您在测试枚举期间遇到此错误,则可能是因为您使用的测试运行程序不正确。

例如:您的 Rust 测试文件在运行测试时可能未使用 Rust 测试框架(即它是一个具有自己的 main 函数的简单 Rust 二进制文件)。在这种情况下,请更改测试清单文件以使用 elf_test_runner

详细了解内置的测试运行程序

测试未能向测试管理器公开 fuchsia.test.Suite

如果测试根目录未能从测试根目录公开 fuchsia.test.Suite,就会发生这种情况。简单的解决方法是添加 expose 声明:

// test_root.cml
expose: [
    ...
    {
        protocol: "fuchsia.test.Suite",
        from: "self",  // If a child component is the test driver, put `from: "#driver"`
    },
],

测试驱动程序未能将 fuchsia.test.Suite 公开给根

如果未正确公开 fuchsia.test.Suite 协议,您的测试可能会失败,并显示类似于以下内容的错误:

ERROR: Failed to route protocol `/svc/fuchsia.test.Suite` from component
`/test_manager/...`: An `expose from #driver` declaration was found at `/test_manager/...`
for `/svc/fuchsia.test.Suite`, but no matching `expose` declaration was found in the child

如果测试驱动程序和测试根是不同的组件,则测试驱动程序还必须将 fuchsia.test.Suite 公开给其父级测试根。

如需解决此问题,请确保测试驱动程序组件清单包含以下 expose 声明:

// test_driver.cml
expose: [
    ...
    {
        protocol: "fuchsia.test.Suite",
        from: "self",
    },
],

测试驱动程序不使用测试运行程序

测试驱动程序必须使用与编写测试所用语言和测试框架对应的适当测试运行程序。例如,Rust 测试的驱动程序需要以下声明:

// test_driver.cml
include: [ "//src/sys/test_runners/rust/default.shard.cml" ]

此外,如果测试驱动程序是测试根的子级,您需要将其提供给驱动程序:

// test_root.cml
offer: [
    {
        runner: "rust_test_runner",
        to: [ "#driver" ],
    },
],

深入阅读