深入了解KUnit:Linux内核新一代单元测试工具(下)

简介: 深入了解KUnit:Linux内核新一代单元测试工具

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_kzallocKUnit,然后确保测试完成后释放内存。这很有用,因为它允许我们使用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 函数传递到测试的方式。一般来说是可用于任何用户数据的指针。这比静态变量更可取,因为它避免了并发问题。structkunitpriv

如果我们想要更灵活的东西,我们可以使用命名的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 无法工作,我该怎么办?

不幸的是,有很多事情可能会被破坏,但这里有一些可以尝试的事情。

  1. 使用参数运行 。这可能会显示 kunit_tool 解析器隐藏的详细信息或错误消息。./tools/testing/kunit/kunit.pyrun--raw_output
  2. 不要运行,而是尝试独立运行、 、 和。这可以帮助追踪问题发生的位置。 (如果您认为解析器有问题,您可以针对或带有 . 的文件手动运行它。)kunit.pyrunkunit.pyconfigkunit.pybuildkunit.pyexecstdinkunit.pyparse
  3. 直接运行 UML 内核通常会发现问题或错误消息,但 kunit_tool请忽略。这应该像./vmlinux 构建 UML 内核后运行一样简单(例如,通过使用)。请注意,UML 有一些不寻常的要求(例如安装了 tmpfs 文件系统的主机),并且过去在静态构建且主机启用了 KASLR 时遇到了问题。 (在较旧的主机内核上,您可能需要运行 来禁用 KASLR。)kunit.pybuildsetarch`uname-m`-R./vmlinux
  4. 确保内核 .config 具有CONFIG_KUNIT=y至少一项测试(例如CONFIG_KUNIT_EXAMPLE_TEST=y)。 kunit_tool 将保留其 .config,因此您可以在运行后看到使用了哪些配置。它还保留您可能进行的任何配置更改,因此您可以启用/禁用或类似的功能,然后重新运行 kunit_tool。kunit.pyrunmakeARCH=ummenuconfig
  5. 跑步前先尝试跑步。这可能有助于清理任何可能导致问题的残留配置项。makeARCH=umdefconfigkunit.pyrun
  6. 最后,尝试在 UML 之外运行 KUnit。 KUnit 和 KUnit 测试可以内置到任何内核中,也可以构建为模块并在运行时加载。这样做应该可以让您确定 UML 是否导致了您所看到的问题。当测试是内置的时,它们将在内核启动时执行,模块在加载时将自动执行相关测试。测试结果可以从 收集,并可以用 进行解析。/sys/kernel/debug/kunit/<testsuite>/resultskunit.pyparse

五、运行KUnit测试的技巧

使用(“kunit 工具”)kunit.pyrun

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.pyrun接受一个可选的 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.pyrun

由于在 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单元测试

单元测试单独测试单个代码单元。单元测试是最精细的测试粒度,允许在被测代码中测试所有可能的代码路径。如果被测代码很小并且没有测试控制之外的任何外部依赖项(例如硬件),则这是可能的。

编写单元测试

要编写良好的单元测试,有一个简单但功能强大的模式:排列-执行-断言。这是构建测试用例和定义操作顺序的好方法。

  • 安排输入和目标:在测试开始时,安排允许函数运行的数据。示例:初始化语句或对象。
  • 根据目标行为采取行动:调用正在测试的函数/代码。
  • 断言预期结果:验证结果(或结果状态)是否符合预期。

单元测试的优点

相关文章
|
5天前
|
缓存 算法 Linux
深入理解Linux内核调度器:公平性与性能的平衡####
真知灼见 本文将带你深入了解Linux操作系统的核心组件之一——完全公平调度器(CFS),通过剖析其设计原理、工作机制以及在实际系统中的应用效果,揭示它是如何在众多进程间实现资源分配的公平性与高效性的。不同于传统的摘要概述,本文旨在通过直观且富有洞察力的视角,让读者仿佛亲身体验到CFS在复杂系统环境中游刃有余地进行任务调度的过程。 ####
26 6
|
3天前
|
缓存 资源调度 安全
深入探索Linux操作系统的心脏——内核配置与优化####
本文作为一篇技术性深度解析文章,旨在引领读者踏上一场揭秘Linux内核配置与优化的奇妙之旅。不同于传统的摘要概述,本文将以实战为导向,直接跳入核心内容,探讨如何通过精细调整内核参数来提升系统性能、增强安全性及实现资源高效利用。从基础概念到高级技巧,逐步揭示那些隐藏在命令行背后的强大功能,为系统管理员和高级用户打开一扇通往极致性能与定制化体验的大门。 --- ###
19 9
|
2天前
|
缓存 负载均衡 Linux
深入理解Linux内核调度器
本文探讨了Linux操作系统核心组件之一——内核调度器的工作原理和设计哲学。不同于常规的技术文章,本摘要旨在提供一种全新的视角来审视Linux内核的调度机制,通过分析其对系统性能的影响以及在多核处理器环境下的表现,揭示调度器如何平衡公平性和效率。文章进一步讨论了完全公平调度器(CFS)的设计细节,包括它如何处理不同优先级的任务、如何进行负载均衡以及它是如何适应现代多核架构的挑战。此外,本文还简要概述了Linux调度器的未来发展方向,包括对实时任务支持的改进和对异构计算环境的适应性。
18 6
|
3天前
|
缓存 Linux 开发者
Linux内核中的并发控制机制:深入理解与应用####
【10月更文挑战第21天】 本文旨在为读者提供一个全面的指南,探讨Linux操作系统中用于实现多线程和进程间同步的关键技术——并发控制机制。通过剖析互斥锁、自旋锁、读写锁等核心概念及其在实际场景中的应用,本文将帮助开发者更好地理解和运用这些工具来构建高效且稳定的应用程序。 ####
18 5
|
1天前
|
算法 Linux 调度
深入理解Linux内核调度器:从基础到优化####
本文旨在通过剖析Linux操作系统的心脏——内核调度器,为读者揭开其高效管理CPU资源的神秘面纱。不同于传统的摘要概述,本文将直接以一段精简代码片段作为引子,展示一个简化版的任务调度逻辑,随后逐步深入,详细探讨Linux内核调度器的工作原理、关键数据结构、调度算法演变以及性能调优策略,旨在为开发者与系统管理员提供一份实用的技术指南。 ####
14 4
|
3天前
|
算法 Unix Linux
深入理解Linux内核调度器:原理与优化
本文探讨了Linux操作系统的心脏——内核调度器(Scheduler)的工作原理,以及如何通过参数调整和代码优化来提高系统性能。不同于常规摘要仅概述内容,本摘要旨在激发读者对Linux内核调度机制深层次运作的兴趣,并简要介绍文章将覆盖的关键话题,如调度算法、实时性增强及节能策略等。
|
3天前
|
安全 前端开发 测试技术
如何选择合适的自动化安全测试工具
选择合适的自动化安全测试工具需考虑多个因素,包括项目需求、测试目标、系统类型和技术栈,工具的功能特性、市场评价、成本和许可,以及集成性、误报率、社区支持、易用性和安全性。综合评估这些因素,可确保所选工具满足项目需求和团队能力。
|
4天前
|
存储 监控 安全
Linux内核调优的艺术:从基础到高级###
本文深入探讨了Linux操作系统的心脏——内核的调优方法。文章首先概述了Linux内核的基本结构与工作原理,随后详细阐述了内核调优的重要性及基本原则。通过具体的参数调整示例(如sysctl、/proc/sys目录中的设置),文章展示了如何根据实际应用场景优化系统性能,包括提升CPU利用率、内存管理效率以及I/O性能等关键方面。最后,介绍了一些高级工具和技术,如perf、eBPF和SystemTap,用于更深层次的性能分析和问题定位。本文旨在为系统管理员和高级用户提供实用的内核调优策略,以最大化Linux系统的效率和稳定性。 ###
|
2天前
|
监控 网络协议 Java
一些适合性能测试脚本编写和维护的工具
一些适合性能测试脚本编写和维护的工具
|
3天前
|
安全 网络协议 关系型数据库
最好用的17个渗透测试工具
渗透测试是安全人员为防止恶意黑客利用系统漏洞而进行的操作。本文介绍了17款业内常用的渗透测试工具,涵盖网络发现、无线评估、Web应用测试、SQL注入等多个领域,包括Nmap、Aircrack-ng、Burp Suite、OWASP ZAP等,既有免费开源工具,也有付费专业软件,适用于不同需求的安全专家。
8 2