Linux 内核自测

内核在 tools/testing/selftests/ 目录下包含一组“自测”。 这些测试旨在成为小型测试,以执行内核中的各个代码路径。 测试旨在在构建、安装和启动内核后运行。

主线的 Kselftest 可以在较旧的稳定内核上运行。 从主线运行测试可提供最佳覆盖率。 几个测试环在稳定版本上运行主线 kselftest 套件。 原因是当添加新测试来测试现有代码以回归测试错误时,我们应该能够在较旧的内核上运行该测试。 因此,重要的是保留仍然可以测试较旧内核的代码,并确保它在新版本上优雅地跳过测试。

您可以在 Kselftest wiki 上找到有关 Kselftest 框架的更多信息,以及如何使用该框架编写新测试

https://kselftest.wiki.kernel.org/

在某些系统中,热插拔测试可能会永远挂起,等待 CPU 和内存准备好脱机。 创建了一个特殊的热插拔目标来运行全范围的热插拔测试。 在默认模式下,热插拔测试以安全模式运行,范围有限。 在有限模式下,cpu-hotplug 测试在单个 CPU 上运行,而不是在所有支持热插拔的 CPU 上运行,并且内存热插拔测试在支持热插拔的内存的 2% 上运行,而不是 10%。

kselftest 作为用户空间进程运行。 可以在用户空间中编写/运行的测试可能希望使用测试工具。 需要在内核空间中运行的测试可能希望使用测试模块

关于测试的文档

有关 kselftest 本身的文档,请参见

运行自测(热插拔测试在有限模式下运行)

要构建测试

$ make headers
$ make -C tools/testing/selftests

要运行测试

$ make -C tools/testing/selftests run_tests

要使用单个命令构建和运行测试,请使用

$ make kselftest

请注意,某些测试需要 root 权限。

Kselftest 支持将输出文件保存在单独的目录中,然后运行测试。 为了将输出文件定位在单独的目录中,支持两种语法。 在这两种情况下,工作目录都必须是内核 src 的根目录。 这适用于下面的“运行自测的子集”部分。

要构建,请使用 O= 将输出文件保存在单独的目录中

$ make O=/tmp/kselftest kselftest

要构建,请使用 KBUILD_OUTPUT 将输出文件保存在单独的目录中

$ export KBUILD_OUTPUT=/tmp/kselftest; make kselftest

O= 分配优先于 KBUILD_OUTPUT 环境变量。

默认情况下,上述命令运行测试并打印完整的通过/失败报告。 Kselftest 支持“summary”选项,以便更容易理解测试结果。 如果指定了 summary 选项,请在 /tmp/testname 文件中查找每个测试的详细的单个测试结果。 这适用于下面的“运行自测的子集”部分。

要启用 summary 选项来运行 kselftest

$ make summary=1 kselftest

运行自测的子集

您可以在 make 命令行上使用“TARGETS”变量来指定要运行的单个测试,或要运行的测试列表。

要仅运行针对单个子系统的测试

$ make -C tools/testing/selftests TARGETS=ptrace run_tests

您可以指定多个测试来构建和运行

$  make TARGETS="size timers" kselftest

要构建,请使用 O= 将输出文件保存在单独的目录中

$ make O=/tmp/kselftest TARGETS="size timers" kselftest

要构建,请使用 KBUILD_OUTPUT 将输出文件保存在单独的目录中

$ export KBUILD_OUTPUT=/tmp/kselftest; make TARGETS="size timers" kselftest

此外,您可以在 make 命令行上使用“SKIP_TARGETS”变量来指定要从 TARGETS 列表中排除的一个或多个目标。

要运行所有测试,但单个子系统除外

$ make -C tools/testing/selftests SKIP_TARGETS=ptrace run_tests

您可以指定多个要跳过的测试

$  make SKIP_TARGETS="size timers" kselftest

您还可以指定要运行的受限测试列表以及专用的跳过列表

$  make TARGETS="breakpoints size timers" SKIP_TARGETS=size kselftest

有关所有可能目标的列表,请参见顶级的 tools/testing/selftests/Makefile。

运行全范围热插拔自测

要构建热插拔测试

$ make -C tools/testing/selftests hotplug

要运行热插拔测试

$ make -C tools/testing/selftests run_hotplug

请注意,某些测试需要 root 权限。

安装自测

您可以使用“make”的“install”目标(它调用 kselftest_install.sh 工具)将自测安装在默认位置(tools/testing/selftests/kselftest_install)中,或者通过 INSTALL_PATH “make”变量安装在用户指定的位置中。

要在默认位置安装自测

$ make -C tools/testing/selftests install

要在用户指定的位置安装自测

$ make -C tools/testing/selftests install INSTALL_PATH=/some/other/path

运行已安装的自测

在安装目录中,以及在 Kselftest tarball 中,有一个名为 run_kselftest.sh 的脚本来运行测试。

您可以简单地执行以下操作来运行已安装的 Kselftests。 请注意,某些测试需要 root 权限

$ cd kselftest_install
$ ./run_kselftest.sh

要查看可用测试的列表,可以使用 -l 选项

$ ./run_kselftest.sh -l

可以使用 -c 选项来运行来自测试集合的所有测试,或者可以使用 -t 选项来运行特定的单个测试。 这两个选项都可以多次使用

$ ./run_kselftest.sh -c size -c seccomp -t timers:posix_timers -t timer:nanosleep

有关其他功能,请参见脚本用法输出,可通过 -h 选项查看。

自测超时

自测被设计为快速的,因此每个测试都使用 45 秒的默认超时。 测试可以通过在其目录中添加设置文件并在那里设置超时变量来覆盖默认超时,以配置测试所需的上限超时。 只有少数测试使用大于 45 秒的值覆盖超时,自测力求保持这种方式。 自测中的超时不被认为是致命的,因为运行测试的系统可能会更改,这也会改变运行测试的预期时间。 如果您可以控制将运行测试的系统,则可以在这些系统上配置测试运行程序,以使用更大或更小的命令行超时,如 -o--override-timeout 参数所示。 例如,要使用 165 秒而不是 1 秒,可以使用

$ ./run_kselftest.sh --override-timeout 165

您可以查看 TAP 输出以查看是否遇到超时。 知道测试必须在特定时间内运行的测试运行程序可以选择将这些超时视为致命错误。

打包自测

在某些情况下,需要打包,例如当需要在不同的系统上运行测试时。 要打包自测,请运行

$ make -C tools/testing/selftests gen_tar

这将在 INSTALL_PATH/kselftest-packages 目录中生成一个 tarball。 默认情况下,使用 .gz 格式。 可以通过指定 FORMAT make 变量来覆盖 tar 压缩格式。 支持 tar 的自动压缩选项识别的任何值,例如

$ make -C tools/testing/selftests gen_tar FORMAT=.xz

make gen_tar 调用 make install,因此您可以使用它来打包自测的子集,方法是使用运行自测的子集部分中指定的变量

$ make -C tools/testing/selftests gen_tar TARGETS="size" FORMAT=.xz

贡献新测试

通常,自测的规则是

  • 如果您不是 root 用户,请尽可能多地执行操作;

  • 不要花费太长时间;

  • 不要破坏任何架构上的构建,并且

  • 如果您的功能未配置,则不要导致顶级的“make run_tests”失败。

  • 测试的输出必须符合 TAP 标准,以确保高质量的测试并捕获具有特定详细信息的失败/错误。 kselftest.h 和 kselftest_harness.h 标头提供用于输出测试结果的包装器。 这些包装器应该用于 pass、fail、exit 和 skip 消息。 CI 系统可以轻松解析 TAP 输出消息以检测测试结果。

贡献新测试(详细信息)

  • 在您的 Makefile 中,通过包含 lib.mk 来使用 lib.mk 中的工具,而不是重新发明轮子。 在包含 lib.mk 之前,根据需要指定标志和二进制生成标志。

    CFLAGS = $(KHDR_INCLUDES)
    TEST_GEN_PROGS := close_range_test
    include ../lib.mk
    
  • 如果编译期间生成了此类二进制文件或文件,请使用 TEST_GEN_XXX。

    TEST_PROGS、TEST_GEN_PROGS 意味着它是默认情况下测试的可执行文件。

    需要模块在测试开始之前构建的测试应使用 TEST_GEN_MODS_DIR。 该变量将包含包含模块的目录的名称。

    需要自定义构建规则并阻止使用常见构建规则的测试应使用 TEST_CUSTOM_PROGS。

    TEST_PROGS 用于测试 shell 脚本。 请确保 shell 脚本设置了其 exec 位。 否则,lib.mk run_tests 将生成警告。

    TEST_CUSTOM_PROGS 和 TEST_PROGS 将由常见的 run_tests 运行。

    TEST_PROGS_EXTENDED、TEST_GEN_PROGS_EXTENDED 意味着它不是默认情况下测试的可执行文件。

    TEST_FILES、TEST_GEN_FILES 意味着它是测试使用的文件。

    TEST_INCLUDES 类似于 TEST_FILES,它列出了在导出或安装测试时应包含的文件,但有以下区别

    • 保留指向其他目录中的文件的符号链接

    • 将文件复制到输出目录时,保留工具/测试/自测/下面的路径部分

    TEST_INCLUDES 旨在列出位于自测层次结构的其他目录中的依赖项。

  • 首先使用内核源代码和/或 git 仓库中的标头,然后使用系统标头。 应该主要关注内核版本标头,而不是系统上由发行版安装的标头,以便能够找到回归。 在 Makefile 中使用 KHDR_INCLUDES 以包含来自内核源代码的标头。

  • 如果测试需要启用特定的内核配置选项,请在测试目录中添加一个配置文件以启用它们。

    例如:tools/testing/selftests/android/config

  • 在测试目录中创建一个 .gitignore 文件,并将所有生成的对象添加到其中。

  • 在自测/Makefile 中的 TARGETS 中添加新的测试名称

    TARGETS += android
    
  • 所有更改都应通过

    kselftest-{all,install,clean,gen_tar}
    kselftest-{all,install,clean,gen_tar} O=abo_path
    kselftest-{all,install,clean,gen_tar} O=rel_path
    make -C tools/testing/selftests {all,install,clean,gen_tar}
    make -C tools/testing/selftests {all,install,clean,gen_tar} O=abs_path
    make -C tools/testing/selftests {all,install,clean,gen_tar} O=rel_path
    

测试模块

Kselftest 从用户空间测试内核。 有时需要从内核内部进行测试,一种方法是创建测试模块。 我们可以通过使用 shell 脚本测试运行程序将模块绑定到 kselftest 框架。 kselftest/module.sh 旨在促进此过程。 还有一个标头文件可帮助编写用于 kselftest 的内核模块

  • tools/testing/selftests/kselftest_module.h

  • tools/testing/selftests/kselftest/module.sh

请注意,测试模块应该用 TAINT_TEST 污染内核。 对于 tools/testing/ 目录中的模块,或者对于使用上面的 kselftest_module.h 标头的模块,将自动发生这种情况。 否则,您需要将 MODULE_INFO(test, "Y") 添加到您的模块源代码中。 通常,不加载模块的自测不应污染内核,但是在加载非测试模块的情况下,可以通过写入 /proc/sys/kernel/tainted 从用户空间应用 TEST_TAINT。

如何使用

在这里,我们展示了创建测试模块并将其绑定到 kselftest 的典型步骤。 我们以 lib/ 的 kselftest 为例。

  1. 创建测试模块

  2. 创建将运行(加载/卸载)模块的测试脚本,例如 tools/testing/selftests/lib/bitmap.sh

  3. 向配置文件添加行,例如 tools/testing/selftests/lib/config

  4. 将测试脚本添加到 makefile,例如 tools/testing/selftests/lib/Makefile

  5. 验证它是否有效

# Assumes you have booted a fresh build of this kernel tree
cd /path/to/linux/tree
make kselftest-merge
make modules
sudo make modules_install
make TARGETS=lib kselftest

示例模块

一个最基本的测试模块可能如下所示

// SPDX-License-Identifier: GPL-2.0+

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include "../tools/testing/selftests/kselftest_module.h"

KSTM_MODULE_GLOBALS();

/*
 * Kernel module for testing the foobinator
 */

static int __init test_function()
{
        ...
}

static void __init selftest(void)
{
        KSTM_CHECK_ZERO(do_test_case("", 0));
}

KSTM_MODULE_LOADERS(test_foo);
MODULE_AUTHOR("John Developer <jd@fooman.org>");
MODULE_LICENSE("GPL");
MODULE_INFO(test, "Y");

示例测试脚本

#!/bin/bash
# SPDX-License-Identifier: GPL-2.0+
$(dirname $0)/../kselftest/module.sh "foo" test_foo

测试工具

kselftest_harness.h 文件包含用于构建测试的有用助手。 测试工具用于用户空间测试,有关内核空间测试,请参见上面的测试模块

来自 tools/testing/selftests/seccomp/seccomp_bpf.c 的测试可以用作示例。

示例

#include "../kselftest_harness.h"

TEST(standalone_test) {
  do_some_stuff;
  EXPECT_GT(10, stuff) {
     stuff_state_t state;
     enumerate_stuff_state(&state);
     TH_LOG("expectation failed with state: %s", state.msg);
  }
  more_stuff;
  ASSERT_NE(some_stuff, NULL) TH_LOG("how did it happen?!");
  last_stuff;
  EXPECT_EQ(0, last_stuff);
}

FIXTURE(my_fixture) {
  mytype_t *data;
  int awesomeness_level;
};
FIXTURE_SETUP(my_fixture) {
  self->data = mytype_new();
  ASSERT_NE(NULL, self->data);
}
FIXTURE_TEARDOWN(my_fixture) {
  mytype_free(self->data);
}
TEST_F(my_fixture, data_is_good) {
  EXPECT_EQ(1, is_my_data_good(self->data));
}

TEST_HARNESS_MAIN

助手

TH_LOG

TH_LOG (fmt, ...)

参数

fmt

格式字符串

...

可选参数

描述

TH_LOG(format, ...)

可选的调试日志记录功能,可用于测试。 可以通过定义 TH_LOG_ENABLED 来启用或禁用日志记录。 例如,#define TH_LOG_ENABLED 1

如果未提供定义,则默认情况下启用日志记录。

TEST

TEST (test_name)

定义测试函数并创建注册存根

参数

test_name

测试名称

描述

TEST(name) { implementation }

按名称定义测试。 名称必须是唯一的,并且测试不能并行运行。 包含块的实现是一个函数,并且应将作用域视为这样。 可以使用裸“return;”语句提前返回。

EXPECT_* 和 ASSERT_* 在 TEST() { } 上下文中有效。

TEST_SIGNAL

TEST_SIGNAL (test_name, signal)

参数

test_name

测试名称

signal

信号编号

描述

TEST_SIGNAL(name, signal) { implementation }

按名称和预期终止信号定义测试。 名称必须是唯一的,并且测试不能并行运行。 包含块的实现是一个函数,并且应将作用域视为这样。 可以使用裸“return;”语句提前返回。

EXPECT_* 和 ASSERT_* 在 TEST() { } 上下文中有效。

FIXTURE_DATA

FIXTURE_DATA (datatype_name)

包装结构名称,以便我们可以减少要传递的参数

参数

datatype_name

数据类型名称

描述

FIXTURE_DATA(datatype_name)

几乎总是,您只需要 FIXTURE()(请参见下文)。 当需要 fixture 数据的类型时,可以使用此调用。 一般来说,除非将 self 直接传递给助手,否则不需要这样做。

FIXTURE

FIXTURE (fixture_name)

每个 fixture 调用一次以设置数据并注册

参数

fixture_name

fixture 名称

描述

FIXTURE(fixture_name) {
  type property1;
  ...
};

定义提供给 TEST_F()-定义的测试的数据作为 self。 它应该使用 FIXTURE_SETUP()FIXTURE_TEARDOWN() 进行填充和清理。

FIXTURE_SETUP

FIXTURE_SETUP (fixture_name)

为 fixture 准备设置函数。 包含 _metadata,以便 EXPECT_*、ASSERT_* 等正常工作。

参数

fixture_name

fixture 名称

描述

FIXTURE_SETUP(fixture_name) { implementation }

填充 fixture 所需的“setup”函数。 使用 FIXTURE_DATA() 定义的数据类型的实例将作为 self 暴露给实现。

ASSERT_* 在此上下文中有效,并将抢占任何依赖 fixture 测试的执行。

可以使用裸“return;”语句提前返回。

FIXTURE_TEARDOWN

FIXTURE_TEARDOWN (fixture_name)

参数

fixture_name

fixture 名称

描述

包含 _metadata,以便 EXPECT_*、ASSERT_* 等正常工作。

FIXTURE_TEARDOWN(fixture_name) { implementation }

填充 fixture 所需的“teardown”函数。 使用 FIXTURE_DATA() 定义的数据类型的实例将作为 self 暴露给实现以进行清理。

可以使用裸“return;”语句提前返回。

FIXTURE_VARIANT

FIXTURE_VARIANT (fixture_name)

每个 fixture 可选择调用一次以声明 fixture 变体

参数

fixture_name

fixture 名称

描述

FIXTURE_VARIANT(fixture_name) {
  type property1;
  ...
};

定义提供给 FIXTURE_SETUP()TEST_F() 和 FIXTURE_TEARDOWN 的常量参数的类型作为 variant。 变体允许使用不同的参数运行相同的测试。

FIXTURE_VARIANT_ADD

FIXTURE_VARIANT_ADD (fixture_name, variant_name)

每个 fixture 变体调用一次以设置和注册数据

参数

fixture_name

fixture 名称

variant_name

参数集的名称

描述

FIXTURE_VARIANT_ADD(fixture_name, variant_name) {
  .property1 = val1,
  ...
};

定义测试 fixture 的变体,提供给 FIXTURE_SETUP()TEST_F() 作为 variant。 每个 fixture 的测试将为每个变体运行一次。

TEST_F

TEST_F (fixture_name, test_name)

为基于 fixture 的测试用例发出测试注册和助手

参数

fixture_name

fixture 名称

test_name

测试名称

描述

TEST_F(fixture, name) { implementation }

定义一个依赖于 fixture 的测试(例如,是测试用例的一部分)。 与 TEST() 非常相似,除了 self 是暴露给实现使用的 fixture 的数据类型的设置实例。

_metadata 对象与所有可能的派生进程共享 (MAP_SHARED),这使它们能够使用 EXCEPT_*() 和 ASSERT_*()。

只有在使用 FIXTURE_TEARDOWN_PARENT() 而不是 FIXTURE_TEARDOWN() 时,self 对象才与可能的派生进程共享。

TEST_HARNESS_MAIN

TEST_HARNESS_MAIN

运行测试工具的简单包装器

描述

TEST_HARNESS_MAIN

使用一次将 main() 附加到测试文件。

运算符

用于 TEST()TEST_F() 中的运算符。 ASSERT_* 调用将立即停止测试执行。 EXPECT_* 调用将发出失败警告,记录它,然后继续。

ASSERT_EQ

ASSERT_EQ (expected, seen)

参数

expected

期望值

seen

测量值

描述

ASSERT_EQ(expected, measured): expected == measured

ASSERT_NE

ASSERT_NE (expected, seen)

参数

expected

期望值

seen

测量值

描述

ASSERT_NE(expected, measured): expected != measured

ASSERT_LT

ASSERT_LT (expected, seen)

参数

expected

期望值

seen

测量值

描述

ASSERT_LT(expected, measured): expected < measured

ASSERT_LE

ASSERT_LE (expected, seen)

参数

expected

期望值

seen

测量值

描述

ASSERT_LE(expected, measured): expected <= measured

ASSERT_GT

ASSERT_GT (expected, seen)

参数

expected

期望值

seen

测量值

描述

ASSERT_GT(expected, measured): expected > measured

ASSERT_GE

ASSERT_GE (expected, seen)

参数

expected

期望值

seen

测量值

描述

ASSERT_GE(expected, measured): expected >= measured

ASSERT_NULL

ASSERT_NULL (seen)

参数

seen

测量值

描述

ASSERT_NULL(measured): NULL == measured

ASSERT_TRUE

ASSERT_TRUE (seen)

参数

seen

测量值

描述

ASSERT_TRUE(measured): measured != 0

ASSERT_FALSE

ASSERT_FALSE (seen)

参数

seen

测量值

描述

ASSERT_FALSE(measured): measured == 0

ASSERT_STREQ

ASSERT_STREQ (expected, seen)

参数

expected

期望值

seen

测量值

描述

ASSERT_STREQ(expected, measured): !strcmp(expected, measured)

ASSERT_STRNE

ASSERT_STRNE (expected, seen)

参数

expected

期望值

seen

测量值

描述

ASSERT_STRNE(expected, measured): strcmp(expected, measured)

EXPECT_EQ

EXPECT_EQ (expected, seen)

参数

expected

期望值

seen

测量值

描述

EXPECT_EQ(expected, measured): expected == measured

EXPECT_NE

EXPECT_NE (expected, seen)

参数

expected

期望值

seen

测量值

描述

EXPECT_NE(expected, measured): expected != measured

EXPECT_LT

EXPECT_LT (expected, seen)

参数

expected

期望值

seen

测量值

描述

EXPECT_LT(expected, measured): expected < measured

EXPECT_LE

EXPECT_LE (expected, seen)

参数

expected

期望值

seen

测量值

描述

EXPECT_LE(expected, measured): expected <= measured

EXPECT_GT

EXPECT_GT (expected, seen)

参数

expected

期望值

seen

测量值

描述

EXPECT_GT(expected, measured): expected > measured

EXPECT_GE

EXPECT_GE (expected, seen)

参数

expected

期望值

seen

测量值

描述

EXPECT_GE(expected, measured): expected >= measured

EXPECT_NULL

EXPECT_NULL (seen)

参数

seen

测量值

描述

EXPECT_NULL(measured): NULL == measured

EXPECT_TRUE

EXPECT_TRUE (seen)

参数

seen

测量值

描述

EXPECT_TRUE(measured): 0 != measured

EXPECT_FALSE

EXPECT_FALSE (seen)

参数

seen

测量值

描述

EXPECT_FALSE(measured): 0 == measured

EXPECT_STREQ

EXPECT_STREQ (expected, seen)

参数

expected

期望值

seen

测量值

描述

EXPECT_STREQ(expected, measured): !strcmp(expected, measured)

EXPECT_STRNE

EXPECT_STRNE (expected, seen)

参数

expected

期望值

seen

测量值

描述

EXPECT_STRNE(expected, measured): strcmp(expected, measured)