3.2自定义错误消息
每个KUNIT_EXPECT和KUNIT_ASSERT宏都有一个_MSG变体。它们采用格式字符串和参数来为自动生成的错误消息提供附加上下文。
char some_str[41]; generate_sha1_hex_string(some_str); /* Before. Not easy to tell why the test failed. */ KUNIT_EXPECT_EQ(test, strlen(some_str), 40); /* After. Now we see the offending string. */ KUNIT_EXPECT_EQ_MSG(test, strlen(some_str), 40, "some_str='%s'", some_str);
或者,可以通过使用来完全控制错误消息KUNIT_FAIL()
,例如
/* Before */ KUNIT_EXPECT_EQ(test, some_setup_function(), 0); /* After: full control over the failure message. */ if (some_setup_function()) KUNIT_FAIL(test, "Failed to setup thing for testing");
测试套件
我们需要许多涵盖所有单元行为的测试用例。有许多类似的测试是很常见的。为了减少这些密切相关的测试中的重复,大多数单元测试框架(包括 KUnit)都提供了 测试套件的概念。测试套件是一个代码单元的测试用例的集合,具有可选的设置和拆卸功能,这些功能在整个套件和/或每个测试用例之前/之后运行。
例如:
KUNIT_CASE(example_test_foo), KUNIT_CASE(example_test_bar), KUNIT_CASE(example_test_baz), {} }; static struct kunit_suite example_test_suite = { .name = "example", .init = example_test_init, .exit = example_test_exit, .suite_init = example_suite_init, .suite_exit = example_suite_exit, .test_cases = example_test_cases, }; kunit_test_suite(example_test_suite);
在上面的示例中,测试套件example_test_suite将首先运行example_suite_init,然后运行测试用例example_test_foo、example_test_bar和example_test_baz。每个人都会example_test_init在它之前立即调用并example_test_exit在它之后立即调用。最后,example_suite_exit将在其他一切之后被调用。kunit_test_suite(example_test_suite)向 KUnit 测试框架注册测试套件。
笔记——即使或 失败,exit
和函数也会运行。确保他们可以处理任何可能因遇到错误或提前退出而导致的不一致状态。suite_exitinitsuite_initinitsuite_init
kunit_test_suite(...)late_init
是一个宏,它告诉链接器将指定的测试套件放在特殊的链接器部分中,以便 KUnit在加载测试模块之后或加载测试模块时(如果测试是作为模块构建的)来运行它。
为其他架构编写测试
最好将在 UML 上运行的测试编写为仅在特定体系结构下运行的测试。最好将在 QEMU 或其他易于获得(且免费)的软件环境下运行的测试编写到特定的硬件上。
尽管如此,仍然有充分的理由来编写特定于体系结构或硬件的测试。例如,我们可能想测试真正属于arch/some-arch/*
.即便如此,请尝试编写测试,使其不依赖于物理硬件。我们的一些测试用例可能不需要硬件,只有少数测试实际上需要硬件来测试它。当硬件不可用时,我们可以跳过它们,而不是禁用测试。
现在我们已经精确地缩小了硬件特定位的范围,编写和运行测试的实际过程与编写正常的 KUnit 测试相同。
3.3常见模式
隔离行为
单元测试将测试中的代码量限制为单个单元。它控制当被测单元调用函数时运行哪些代码。函数作为 API 的一部分公开,以便可以更改该函数的定义而不影响代码库的其余部分。在内核中,这来自两个构造:类,它们是包含实现者提供的函数指针的结构,以及特定于体系结构的函数,它们具有在编译时选择的定义。
类
类不是 C 编程语言中内置的结构;而是 C 语言中内置的结构。然而,这是一个很容易导出的概念。因此,在大多数情况下,每个不使用标准化面向对象库(如 GNOME 的 GObject)的项目都有自己稍微不同的面向对象编程方式; Linux 内核也不例外。
内核面向对象编程的核心概念是类。在内核中,类是包含函数指针的结构。这在实现者和用户之间创建了契约,因为它迫使他们使用相同的函数签名而不必直接调用该函数。要成为一个类,函数指针必须指定指向该类的指针(称为类句柄)作为参数之一。因此,成员函数(也称为方法)可以访问成员变量(也称为字段),从而允许同一实现具有多个实例。
通过将父类嵌入到子类中,类可以被子类覆盖 。然后,当调用子类方法时,子实现知道传递给它的指针是子类中包含的父类的指针。因此,子进程可以计算指向自身的指针,因为指向父进程的指针始终与指向子进程的指针有固定的偏移量。此偏移量是子结构中包含的父级的偏移量。例如:
int (*area)(struct shape *this); }; struct rectangle { struct shape parent; int length; int width; }; int rectangle_area(struct shape *this) { struct rectangle *self = container_of(this, struct rectangle, parent); return self->length * self->width; }; void rectangle_new(struct rectangle *self, int length, int width) { self->parent.area = rectangle_area; self->length = length; self->width = width; }
在此示例中,从指向父级的指针计算指向子级的指针是通过 完成的container_of
。
伪造类
为了对调用类中方法的一段代码进行单元测试,该方法的行为必须是可控的,否则测试就不再是单元测试,而变成集成测试。
假类实现的一段代码与生产实例中运行的代码不同,但从调用者的角度来看,其行为相同。这样做是为了替换难以处理或缓慢的依赖项。例如,实现一个假 EEPROM,将“内容”存储在内部缓冲区中。假设我们有一个代表 EEPROM 的类:
struct eeprom { ssize_t (*read)(struct eeprom *this, size_t offset, char *buffer, size_t count); ssize_t (*write)(struct eeprom *this, size_t offset, const char *buffer, size_t count); };
我们想要测试缓冲写入 EEPROM 的代码:
struct eeprom_buffer { ssize_t (*write)(struct eeprom_buffer *this, const char *buffer, size_t count); int flush(struct eeprom_buffer *this); size_t flush_count; /* Flushes when buffer exceeds flush_count. */ }; struct eeprom_buffer *new_eeprom_buffer(struct eeprom *eeprom); void destroy_eeprom_buffer(struct eeprom *eeprom);
我们可以通过伪造底层 EEPROM 来测试此代码:
struct fake_eeprom { struct eeprom parent; char contents[FAKE_EEPROM_CONTENTS_SIZE]; }; ssize_t fake_eeprom_read(struct eeprom *parent, size_t offset, char *buffer, size_t count) { struct fake_eeprom *this = container_of(parent, struct fake_eeprom, parent); count = min(count, FAKE_EEPROM_CONTENTS_SIZE - offset); memcpy(buffer, this->contents + offset, count); return count; } ssize_t fake_eeprom_write(struct eeprom *parent, size_t offset, const char *buffer, size_t count) { struct fake_eeprom *this = container_of(parent, struct fake_eeprom, parent); count = min(count, FAKE_EEPROM_CONTENTS_SIZE - offset); memcpy(this->contents + offset, buffer, count); return count; } void fake_eeprom_init(struct fake_eeprom *this) { this->parent.read = fake_eeprom_read; this->parent.write = fake_eeprom_write; memset(this->contents, 0, FAKE_EEPROM_CONTENTS_SIZE); }
我们现在可以用它来测试:structeeprom_buffer
struct eeprom_buffer_test { struct fake_eeprom *fake_eeprom; struct eeprom_buffer *eeprom_buffer; }; static void eeprom_buffer_test_does_not_write_until_flush(struct kunit *test) { struct eeprom_buffer_test *ctx = test->priv; struct eeprom_buffer *eeprom_buffer = ctx->eeprom_buffer; struct fake_eeprom *fake_eeprom = ctx->fake_eeprom; char buffer[] = {0xff}; eeprom_buffer->flush_count = SIZE_MAX; eeprom_buffer->write(eeprom_buffer, buffer, 1); KUNIT_EXPECT_EQ(test, fake_eeprom->contents[0], 0); eeprom_buffer->write(eeprom_buffer, buffer, 1); KUNIT_EXPECT_EQ(test, fake_eeprom->contents[1], 0); eeprom_buffer->flush(eeprom_buffer); KUNIT_EXPECT_EQ(test, fake_eeprom->contents[0], 0xff); KUNIT_EXPECT_EQ(test, fake_eeprom->contents[1], 0xff); } static void eeprom_buffer_test_flushes_after_flush_count_met(struct kunit *test) { struct eeprom_buffer_test *ctx = test->priv; struct eeprom_buffer *eeprom_buffer = ctx->eeprom_buffer; struct fake_eeprom *fake_eeprom = ctx->fake_eeprom; char buffer[] = {0xff}; eeprom_buffer->flush_count = 2; eeprom_buffer->write(eeprom_buffer, buffer, 1); KUNIT_EXPECT_EQ(test, fake_eeprom->contents[0], 0); eeprom_buffer->write(eeprom_buffer, buffer, 1); KUNIT_EXPECT_EQ(test, fake_eeprom->contents[0], 0xff); KUNIT_EXPECT_EQ(test, fake_eeprom->contents[1], 0xff); } static void eeprom_buffer_test_flushes_increments_of_flush_count(struct kunit *test) { struct eeprom_buffer_test *ctx = test->priv; struct eeprom_buffer *eeprom_buffer = ctx->eeprom_buffer; struct fake_eeprom *fake_eeprom = ctx->fake_eeprom; char buffer[] = {0xff, 0xff}; eeprom_buffer->flush_count = 2; eeprom_buffer->write(eeprom_buffer, buffer, 1); KUNIT_EXPECT_EQ(test, fake_eeprom->contents[0], 0); eeprom_buffer->write(eeprom_buffer, buffer, 2); KUNIT_EXPECT_EQ(test, fake_eeprom->contents[0], 0xff); KUNIT_EXPECT_EQ(test, fake_eeprom->contents[1], 0xff); /* Should have only flushed the first two bytes. */ KUNIT_EXPECT_EQ(test, fake_eeprom->contents[2], 0); } static int eeprom_buffer_test_init(struct kunit *test) { struct eeprom_buffer_test *ctx; ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL); KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx); ctx->fake_eeprom = kunit_kzalloc(test, sizeof(*ctx->fake_eeprom), GFP_KERNEL); KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx->fake_eeprom); fake_eeprom_init(ctx->fake_eeprom); ctx->eeprom_buffer = new_eeprom_buffer(&ctx->fake_eeprom->parent); KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx->eeprom_buffer); test->priv = ctx; return 0; } static void eeprom_buffer_test_exit(struct kunit *test) { struct eeprom_buffer_test *ctx = test->priv; destroy_eeprom_buffer(ctx->eeprom_buffer); }
针对多个输入进行测试
仅测试一些输入不足以确保代码正常工作,例如:测试哈希函数。
我们可以编写一个辅助宏或函数。每个输入都会调用该函数。例如,要测试sha1sum(1)
,我们可以编写:
#define TEST_SHA1(in, want) \ sha1sum(in, out); \ KUNIT_EXPECT_STREQ_MSG(test, out, want, "sha1sum(%s)", in); char out[40]; TEST_SHA1("hello world", "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"); TEST_SHA1("hello world!", "430ce34d020724ed75a196dfc2ad67c77772d169");
请注意使用 的_MSG
版本KUNIT_EXPECT_STREQ
来打印更详细的错误并使帮助宏中的断言更清晰。
当多次调用相同的期望(在循环或辅助函数中)时,这些_MSG
变体非常有用,因此行号不足以识别失败的内容,如下所示。
在复杂的情况下,与辅助宏变体相比,我们建议使用表驱动测试,例如:
int i; char out[40]; struct sha1_test_case { const char *str; const char *sha1; }; struct sha1_test_case cases[] = { { .str = "hello world", .sha1 = "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed", }, { .str = "hello world!", .sha1 = "430ce34d020724ed75a196dfc2ad67c77772d169", }, }; for (i = 0; i < ARRAY_SIZE(cases); ++i) { sha1sum(cases[i].str, out); KUNIT_EXPECT_STREQ_MSG(test, out, cases[i].sha1, "sha1sum(%s)", cases[i].str); }
涉及更多样板代码,但它可以:
当有多个输入/输出(由于字段名称)时更具可读性。例如,请参见fs/ext4/inode-test.c
。
如果测试用例在多个测试之间共享,则可以减少重复。例如:如果我们想测试sha256sum
,我们可以添加一个sha256
字段并重用cases
。
转换为“参数化测试”。
参数化测试
表驱动测试模式很常见,KUnit 对其有特殊支持。通过重用上面的相同cases
数组,我们可以将测试编写为“参数化测试”,如下所示。
// This is copy-pasted from above. struct sha1_test_case { const char *str; const char *sha1; }; const struct sha1_test_case cases[] = { { .str = "hello world", .sha1 = "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed", }, { .str = "hello world!", .sha1 = "430ce34d020724ed75a196dfc2ad67c77772d169", }, }; // Need a helper function to generate a name for each test case. static void case_to_desc(const struct sha1_test_case *t, char *desc) { strcpy(desc, t->str); } // Creates `sha1_gen_params()` to iterate over `cases`. KUNIT_ARRAY_PARAM(sha1, cases, case_to_desc); // Looks no different from a normal test. static void sha1_test(struct kunit *test) { // This function can just contain the body of the for-loop. // The former `cases[i]` is accessible under test->param_value. char out[40]; struct sha1_test_case *test_param = (struct sha1_test_case *)(test->param_value); sha1sum(test_param->str, out); KUNIT_EXPECT_STREQ_MSG(test, out, test_param->sha1, "sha1sum(%s)", test_param->str); } // Instead of KUNIT_CASE, we use KUNIT_CASE_PARAM and pass in the // function declared by KUNIT_ARRAY_PARAM. static struct kunit_case sha1_test_cases[] = { KUNIT_CASE_PARAM(sha1_test, sha1_gen_params), {} };
分配内存
在您可能使用 的地方kzalloc
,您可以改为使用kunit_kzalloc
KUnit,然后确保测试完成后释放内存。这很有用,因为它允许我们使用KUNIT_ASSERT_EQ
宏提前退出测试,而不必担心记住调用kfree
.例如:
void example_test_allocation(struct kunit *test) { char *buffer = kunit_kzalloc(test, 16, GFP_KERNEL); /* Ensure allocation succeeded. */ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, buffer); KUNIT_ASSERT_STREQ(test, buffer, ""); }
注册清理操作
如果您需要执行一些超出简单使用的清理工作kunit_kzalloc
,您可以注册一个自定义的“延迟操作”,这是一个在测试退出时运行的清理函数(无论是干净地退出,还是通过失败的断言)。操作是没有返回值和单个void*
上下文参数的简单函数,并且履行与 Python 和 Go 测试中的“清理”函数、支持它们的语言中的“延迟”语句以及(在某些情况下)RAII 中的析构函数相同的角色语言。
这些对于从全局列表中取消注册事物、关闭文件或其他资源或释放资源非常有用。
例如:
static void cleanup_device(void *ctx) { struct device *dev = (struct device *)ctx; device_unregister(dev); } void example_device_test(struct kunit *test) { struct my_device dev; device_register(&dev); kunit_add_action(test, &cleanup_device, &dev); }
请注意,对于像 device_unregister 这样只接受单个指针大小参数的函数,可以直接将该函数转换为 akunit_action_t
而不是编写包装函数,例如:
kunit_add_action(test, (kunit_action_t *)&device_unregister, &dev);
kunit_add_action例如,如果系统内存不足,则可能会失败。kunit_add_action_or_reset如果无法推迟操作,您可以使用立即运行该操作的替代方法。
如果您需要更多地控制何时调用清理函数,可以使用 提前触发它kunit_release_action,或者使用 完全取消它kunit_remove_action。
测试静态函数
如果我们不想公开函数或变量进行测试,一种选择是有条件地将#include
测试文件放在 .c 文件的末尾。例如:
/* In my_file.c */ static int do_interesting_thing(); #ifdef CONFIG_MY_KUNIT_TEST #include "my_kunit_test.c" #endif
注入仅测试代码
与上图类似,我们可以添加特定于测试的逻辑。例如:
/* In my_file.h */ #ifdef CONFIG_MY_KUNIT_TEST /* Defined in my_kunit_test.c */ void test_only_hook(void); #else void test_only_hook(void) { } #endif
通过访问当前代码,可以使此仅测试代码变得更有用,kunit_test如下一节:访问当前测试所示。
访问当前测试
在某些情况下,我们需要从测试文件外部调用仅测试代码。例如,当提供函数的假实现或使错误处理程序中的任何当前测试失败时,这很有用。我们可以通过kunit_test中的字段来完成此操作task_struct,我们可以使用kunit_get_current_test()中的函数访问该字段kunit/test-bug.h。
kunit_get_current_test()即使 KUnit 未启用,也可以安全调用。如果 KUnit 未启用,或者当前任务中没有运行测试,它将返回NULL。这会编译为无操作或静态密钥检查,因此在没有运行测试时对性能的影响可以忽略不计。
下面的示例使用它来实现函数的“模拟”实现foo:
#include <kunit/test-bug.h> /* for kunit_get_current_test */ struct test_data { int foo_result; int want_foo_called_with; }; static int fake_foo(int arg) { struct kunit *test = kunit_get_current_test(); struct test_data *test_data = test->priv; KUNIT_EXPECT_EQ(test, test_data->want_foo_called_with, arg); return test_data->foo_result; } static void example_simple_test(struct kunit *test) { /* Assume priv (private, a member used to pass test data from * the init function) is allocated in the suite's .init */ struct test_data *test_data = test->priv; test_data->foo_result = 42; test_data->want_foo_called_with = 1; /* In a real test, we'd probably pass a pointer to fake_foo somewhere * like an ops struct, etc. instead of calling it directly. */ KUNIT_EXPECT_EQ(test, fake_foo(1), 42); }
在此示例中,我们使用priv
成员作为将数据从 init 函数传递到测试的方式。一般来说是可用于任何用户数据的指针。这比静态变量更可取,因为它避免了并发问题。struct
kunitpriv
如果我们想要更灵活的东西,我们可以使用命名的kunit_resource
.每个测试可以有多个具有字符串名称的资源,提供与成员相同的灵活性priv
,而且还允许辅助函数创建资源而不会相互冲突。还可以为每个资源定义一个清理函数,从而轻松避免资源泄漏。
当前测试失败
如果我们想让当前测试失败,我们可以使用 在 中定义的并且不需要拉入。例如,我们可以选择对某些数据结构启用一些额外的调试检查,如下所示:kunit_fail_current_test(fmt,
args...)<kunit/test-bug.h><kunit/test.h>
#include <kunit/test-bug.h> #ifdef CONFIG_EXTRA_DEBUG_CHECKS static void validate_my_data(struct data *data) { if (is_valid(data)) return; kunit_fail_current_test("data %p is invalid", data); /* Normal, non-KUnit, error reporting code here. */ } #else static void my_debug_function(void) { } #endif
kunit_fail_current_test()
即使 KUnit 未启用,也可以安全调用。如果 KUnit 未启用,或者当前任务中没有运行测试,则它将不执行任何操作。这会编译为无操作或静态密钥检查,因此在没有运行测试时对性能的影响可以忽略不计。
四、Kunit常见问题
这与Autotest、kselftest 等有何不同?
KUnit 是一个单元测试框架。 Autotest、kselftest(和其他一些)不是。
单元测试应该单独测试单个代码单元,因此称为单元测试。单元测试应该是最细粒度的测试,并且应该允许在被测代码中测试所有可能的代码路径。只有当被测代码很小并且没有测试控制之外的任何外部依赖项(如硬件)时,这才有可能。
目前还没有可用于内核的测试框架不需要在测试机或虚拟机中安装内核。所有测试框架都要求在用户空间中编写测试并在被测内核上运行。对于 Autotest、kselftest 和其他一些框架来说,情况确实如此,因此它们都没有资格被视为单元测试框架。
KUnit 是否支持在 UML 以外的体系结构上运行?
是的,大部分是。
在大多数情况下,KUnit 核心框架(我们用来编写测试的框架)可以编译为任何架构。它像内核的另一部分一样进行编译,并在内核启动时运行,或者在加载模块时构建为模块时运行。但是,有些基础设施(例如 KUnit Wrapper ( tools/testing/kunit/kunit.py
))可能不支持某些架构。
简而言之,是的,您可以在其他体系结构上运行 KUnit,但它可能比在 UML 上使用 KUnit 需要更多的工作。
单元测试和其他类型的测试有什么区别?
大多数现有的 Linux 内核测试都被归类为集成测试或端到端测试。
- 单元测试应该单独测试单个代码单元。单元测试应该是最精细的测试粒度,因此允许在被测代码中测试所有可能的代码路径。只有当被测代码很小并且没有测试控制之外的任何外部依赖项(如硬件)时,这才有可能。
- 集成测试测试最少的组件集(通常只有两个或三个)之间的交互。例如,有人可能编写集成测试来测试驱动程序和硬件之间的交互,或者测试内核提供的用户空间库和内核本身之间的交互。然而,这些测试之一可能不会测试整个内核以及硬件交互和与用户空间的交互。
- 端到端测试通常从被测代码的角度来测试整个系统。例如,某人可能会通过在具有生产用户空间的生产硬件上安装内核的生产配置来为内核编写端到端测试,然后尝试执行某些取决于硬件、内核和硬件之间交互的行为。用户空间。
KUnit 无法工作,我该怎么办?
不幸的是,有很多事情可能会被破坏,但这里有一些可以尝试的事情。
- 使用参数运行 。这可能会显示 kunit_tool 解析器隐藏的详细信息或错误消息。
./tools/testing/kunit/kunit.py
run--raw_output
- 不要运行,而是尝试独立运行、 、 和。这可以帮助追踪问题发生的位置。 (如果您认为解析器有问题,您可以针对或带有 . 的文件手动运行它。)
kunit.py
runkunit.py
configkunit.py
buildkunit.py
execstdinkunit.py
parse
- 直接运行 UML 内核通常会发现问题或错误消息,但
kunit_tool
请忽略。这应该像./vmlinux
构建 UML 内核后运行一样简单(例如,通过使用)。请注意,UML 有一些不寻常的要求(例如安装了 tmpfs 文件系统的主机),并且过去在静态构建且主机启用了 KASLR 时遇到了问题。 (在较旧的主机内核上,您可能需要运行 来禁用 KASLR。)kunit.py
buildsetarch
`uname
-m`
-R
./vmlinux
- 确保内核 .config 具有
CONFIG_KUNIT=y
至少一项测试(例如CONFIG_KUNIT_EXAMPLE_TEST=y
)。 kunit_tool 将保留其 .config,因此您可以在运行后看到使用了哪些配置。它还保留您可能进行的任何配置更改,因此您可以启用/禁用或类似的功能,然后重新运行 kunit_tool。kunit.py
runmake
ARCH=um
menuconfig
- 跑步前先尝试跑步。这可能有助于清理任何可能导致问题的残留配置项。
make
ARCH=um
defconfigkunit.py
run
- 最后,尝试在 UML 之外运行 KUnit。 KUnit 和 KUnit 测试可以内置到任何内核中,也可以构建为模块并在运行时加载。这样做应该可以让您确定 UML 是否导致了您所看到的问题。当测试是内置的时,它们将在内核启动时执行,模块在加载时将自动执行相关测试。测试结果可以从 收集,并可以用 进行解析。
/sys/kernel/debug/kunit/<test
suite>/resultskunit.py
parse
五、运行KUnit测试的技巧
使用(“kunit 工具”)kunit.py
run
5.1从任何目录运行
创建一个 bash 函数会很方便,例如:
function run_kunit() { ( cd "$(git rev-parse --show-toplevel)" && ./tools/testing/kunit/kunit.py run "$@" ) }
笔记——早期版本kunit.py
(5.6 之前)除非从内核根运行,否则无法工作,因此使用子 shell 和cd
.
运行测试的子集
kunit.py
run
接受一个可选的 glob 参数来过滤测试。格式为"<suite_glob>[.test_glob]"
.
假设我们想要运行 sysctl 测试,我们可以通过以下方式执行:
$ echo -e 'CONFIG_KUNIT=y\nCONFIG_KUNIT_ALL_TESTS=y' > .kunit/.kunitconfig $ ./tools/testing/kunit/kunit.py run 'sysctl*'
我们可以通过以下方式筛选出“写入”测试:
$ echo -e 'CONFIG_KUNIT=y\nCONFIG_KUNIT_ALL_TESTS=y' > .kunit/.kunitconfig $ ./tools/testing/kunit/kunit.py run 'sysctl*.*write*'
我们正在付出构建比我们需要的更多测试的成本,但这比摆弄.kunitconfig
文件或注释掉 要容易kunit_suite
。
但是,如果我们想以一种不太特别的方式定义一组测试,则下一个技巧很有用。
定义一组测试
kunit.py run(与build、 和 一起config)支持 --kunitconfig标志。因此,如果您有一组想要定期运行的测试(特别是如果它们具有其他依赖项),您可以为.kunitconfig它们创建一个特定的测试。
例如kunit有一个测试:
$ ./tools/testing/kunit/kunit.py run --kunitconfig=lib/kunit/.kunitconfig
或者,如果您遵循命名文件的约定.kunitconfig
,则可以只传递目录,例如
$ ./tools/testing/kunit/kunit.py run --kunitconfig=lib/kunit
5.2设置内核命令行参数
您可以用来--kernel_args
传递任意内核参数,例如
$ ./tools/testing/kunit/kunit.py run --kernel_args=param=42 --kernel_args=param2=false
在UML下生成代码覆盖率报告,这与在 Linux 内核中使用 gcov中记录的获取覆盖率信息的“正常”方式不同。
CONFIG_GCOV_KERNEL=y
我们可以设置这些选项,而不是启用:
CONFIG_DEBUG_KERNEL=y CONFIG_DEBUG_INFO=y CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT=y CONFIG_GCOV=y
将其组合成可复制粘贴的命令序列:
# Append coverage options to the current config $ ./tools/testing/kunit/kunit.py run --kunitconfig=.kunit/ --kunitconfig=tools/testing/kunit/configs/coverage_uml.config # Extract the coverage information from the build dir (.kunit/) $ lcov -t "my_kunit_tests" -o coverage.info -c -d .kunit/ # From here on, it's the same process as with CONFIG_GCOV_KERNEL=y # E.g. can generate an HTML report in a tmp dir like so: $ genhtml -o /tmp/coverage_html coverage.info
如果您安装的 gcc 版本无法运行,您可以调整以下步骤:
$ ./tools/testing/kunit/kunit.py run --make_options=CC=/usr/bin/gcc-6 $ lcov -t "my_kunit_tests" -o coverage.info -c -d .kunit/ --gcov-tool=/usr/bin/gcov-6
手动运行测试
在不使用的情况下运行测试也是一个重要的用例。目前,如果您想在 UML 以外的体系结构上进行测试,这是您唯一的选择。kunit.py
run
由于在 UML 下运行测试相当简单(配置和编译内核,运行二进制文件./linux
),因此本节将重点测试非 UML 体系结构。
运行内置测试
将测试设置为 时=y
,测试将作为引导的一部分运行,并将结果以 TAP 格式打印到 dmesg。因此,您只需将测试添加到您的 .config
、 构建并正常启动内核即可。
因此,如果我们使用以下命令编译内核:
CONFIG_KUNIT=y CONFIG_KUNIT_EXAMPLE_TEST=y
然后我们会在 dmesg 中看到这样的输出,表明测试已运行并通过:
TAP version 14 1..1 # Subtest: example 1..1 # example_simple_test: initializing ok 1 - example_simple_test ok 1 - example
作为模块运行测试
根据测试,您可以将它们构建为可加载模块。
例如,我们将配置选项从之前更改为
CONFIG_KUNIT=y CONFIG_KUNIT_EXAMPLE_TEST=m
然后启动内核后,我们可以通过以下方式运行测试
$ modprobe kunit-example-test
这将导致它将 TAP 输出打印到标准输出。
笔记——如果任何测试失败(截至 5.13) ,则不modprobe会有非零退出代码。但是会,请参见下文。kunit.py parse
漂亮的打印结果
您可以使用解析 dmesg 来获取测试输出,并以熟悉的格式打印结果。kunit.pyparsekunit.pyrun
$ ./tools/testing/kunit/kunit.py parse /var/log/dmesg
检索每个套件的结果
无论您如何运行测试,您都可以 CONFIG_KUNIT_DEBUGFS
公开每个套件的 TAP 格式的结果:
CONFIG_KUNIT=y CONFIG_KUNIT_EXAMPLE_TEST=m CONFIG_KUNIT_DEBUGFS=y
每个套件的结果将在 下公开/sys/kernel/debug/kunit/<suite>/results
。所以使用我们的示例配置:
$ modprobe kunit-example-test > /dev/null $ cat /sys/kernel/debug/kunit/example/results ... <TAP output> ... # After removing the module, the corresponding files will go away $ modprobe -r kunit-example-test $ cat /sys/kernel/debug/kunit/example/results /sys/kernel/debug/kunit/example/results: No such file or directory
生成代码覆盖率报告
这里唯一模糊的特定于 KUnit 的建议是您可能希望将测试构建为模块。这样您就可以将覆盖率与启动期间执行的其他代码的测试隔离开来,例如
# Reset coverage counters before running the test. $ echo 0 > /sys/kernel/debug/gcov/reset $ modprobe kunit-example-test
测试属性和过滤
测试套件和案例可以标记测试属性,例如测试速度。这些属性稍后将打印在测试输出中,并可用于过滤测试执行。
标记测试属性
kunit_attributes通过在测试定义中包含一个对象来标记测试的属性。
可以使用宏来标记测试用例 来定义测试用例而不是.KUNIT_CASE_ATTR(test_name, attributes)KUNIT_CASE(test_name)
static const struct kunit_attributes example_attr = { .speed = KUNIT_VERY_SLOW, }; static struct kunit_case example_test_cases[] = { KUNIT_CASE_ATTR(example_test, example_attr), };
通过在套件定义中设置“attr”字段,可以用属性来标记测试套件。
static const struct kunit_attributes example_attr = { .speed = KUNIT_VERY_SLOW, }; static struct kunit_suite example_test_suite = { ..., .attr = example_attr, };
报告属性
当用户运行测试时,属性将出现在原始内核输出中(KTAP 格式)。请注意,对于所有通过的测试,默认情况下,属性将隐藏在 kunit.py 输出中,但可以使用该标志访问原始内核输出 --raw_output
。这是如何在内核输出中格式化测试用例的测试属性的示例:
# example_test.speed: slow ok 1 example_test
这是测试套件的测试属性如何在内核输出中格式化的示例:
KTAP version 2 # Subtest: example_suite # module: kunit_example_test 1..3 ... ok 1 example_suite
此外,用户可以使用命令行标志输出测试的完整属性报告及其属性--list_tests_attr
:
kunit.py run "example" --list_tests_attr
过滤
用户可以在运行测试时使用命令行标志过滤测试--filter
。举个例子:
kunit.py run --filter speed=slow
您还可以对过滤器使用以下操作:“<”、“">”、“<=”、“">=”、“!=”和“=”。例子:
kunit.py run --filter "speed>slow"
此示例将以快于慢的速度运行所有测试。请注意,字符 < 和 > 通常由 shell 解释,因此可能需要将它们加引号或转义,如上所述。
此外,您可以一次使用多个过滤器。只需使用逗号分隔过滤器即可。例子:
kunit.py run --filter "speed>slow, module=kunit_example_test"
过滤的测试将不会运行或显示在测试输出中。您可以使用该--filter_action=skip
标志来跳过过滤测试。这些测试将显示在测试中的测试输出中,但不会运行。要在手动运行 KUnit 时使用此功能,请使用模块 paramkunit.filter_action=skip
。
过滤程序规则
由于套件和测试用例都可以具有属性,因此在过滤过程中属性之间可能会发生冲突。过滤过程遵循以下规则:
- 过滤始终在每个测试级别运行。
- 如果测试设置了属性,则测试的值将被过滤。
- 否则,价值将回落至套件的价值。
- 如果两者均未设置,则该属性具有全局“默认”值,并使用该值。
当前属性列表
speed
该属性指示测试执行的速度(测试的速度有多慢或多快)。
此属性保存为具有以下类别的枚举:“normal”、“slow”或“very_slow”。假设的测试默认速度是“正常”。这表明测试花费的时间相对较短(不到 1 秒),无论运行在哪台机器上。任何比这慢的测试都可以标记为“slow”或“very_slow”。
该宏KUNIT_CASE_SLOW(test_name)
可以轻松地用于将测试用例的速度设置为“慢”。
module
该属性指示与测试关联的模块的名称。
该属性会自动保存为字符串并为每个套件打印。还可以使用此属性过滤测试。
5.3单元测试
单元测试单独测试单个代码单元。单元测试是最精细的测试粒度,允许在被测代码中测试所有可能的代码路径。如果被测代码很小并且没有测试控制之外的任何外部依赖项(例如硬件),则这是可能的。
编写单元测试
要编写良好的单元测试,有一个简单但功能强大的模式:排列-执行-断言。这是构建测试用例和定义操作顺序的好方法。
- 安排输入和目标:在测试开始时,安排允许函数运行的数据。示例:初始化语句或对象。
- 根据目标行为采取行动:调用正在测试的函数/代码。
- 断言预期结果:验证结果(或结果状态)是否符合预期。
单元测试的优点
- 从长远来看,提高测试速度和开发。
- 在初始阶段检测错误,因此与验收测试相比可以降低错误修复成本。
- 提高代码质量。
- 鼓励编写可测试的代码。
- 精品文章推荐阅读:
- 深入理解C++内存管理:指针、引用和内存分配
- 打破常规,Linux内核新的数据结构上场maple tree
- 深入挖掘Linux内核源码:揭秘其惊人的架构和设计
- 从零开始学习 Linux 内核套接字:掌握网络编程的必备技能
- 解密Linux内核神器:内存屏障的秘密功效与应用方法
- Linux内核文件系统:比万物之神还要强大的存储魔法!
- 内存分配不再神秘:深入剖析malloc函数实现原理与机制
- 探索网络通信核心技术,手写TCP/IP用户态协议栈,让性能飙升起来!