編寫測試¶
測試用例¶
KUnit 中的基本單元是測試用例。測試用例是一個函式,其簽名為 void (*)(struct kunit *test)。它呼叫被測函式,然後設定預期結果。例如
void example_test_success(struct kunit *test)
{
}
void example_test_failure(struct kunit *test)
{
KUNIT_FAIL(test, "This test never passes.");
}
在上面的例子中,example_test_success 總是透過,因為它什麼也不做;沒有設定任何預期,因此所有預期都透過。另一方面,example_test_failure 總是失敗,因為它呼叫 KUNIT_FAIL,這是一個特殊的預期,它記錄一條訊息並導致測試用例失敗。
預期¶
預期指定我們期望一段程式碼在測試中做什麼。 預期像函式一樣被呼叫。測試是透過設定對被測程式碼的行為的預期來完成的。當一個或多個預期失敗時,測試用例失敗,並記錄有關失敗的資訊。例如
void add_test_basic(struct kunit *test)
{
KUNIT_EXPECT_EQ(test, 1, add(1, 0));
KUNIT_EXPECT_EQ(test, 2, add(1, 1));
}
在上面的例子中,add_test_basic 對一個名為 add 的函式的行為做出了許多斷言。第一個引數始終是 struct kunit * 型別,其中包含有關當前測試上下文的資訊。在本例中,第二個引數是期望的值。最後一個值是實際值。如果 add 通過了所有這些預期,則測試用例 add_test_basic 將透過;如果這些預期中的任何一個失敗,測試用例將失敗。
當任何預期被違反時,測試用例會失敗;但是,測試將繼續執行,並嘗試其他預期,直到測試用例結束或以其他方式終止。這與稍後討論的斷言相反。
要了解有關更多 KUnit 預期的資訊,請參閱 測試 API。
注意
單個測試用例應該簡短、易於理解,並且專注於單一行為。
例如,如果我們想嚴格測試上面的 add 函式,請建立額外的測試用例,這些用例將測試 add 函式應具有的每個屬性,如下所示
void add_test_basic(struct kunit *test)
{
KUNIT_EXPECT_EQ(test, 1, add(1, 0));
KUNIT_EXPECT_EQ(test, 2, add(1, 1));
}
void add_test_negative(struct kunit *test)
{
KUNIT_EXPECT_EQ(test, 0, add(-1, 1));
}
void add_test_max(struct kunit *test)
{
KUNIT_EXPECT_EQ(test, INT_MAX, add(0, INT_MAX));
KUNIT_EXPECT_EQ(test, -1, add(INT_MAX, INT_MIN));
}
void add_test_overflow(struct kunit *test)
{
KUNIT_EXPECT_EQ(test, INT_MIN, add(INT_MAX, 1));
}
斷言¶
斷言類似於預期,不同之處在於,如果條件不滿足,斷言會立即終止測試用例。例如
static void test_sort(struct kunit *test)
{
int *a, i, r = 1;
a = kunit_kmalloc_array(test, TEST_LEN, sizeof(*a), GFP_KERNEL);
KUNIT_ASSERT_NOT_ERR_OR_NULL(test, a);
for (i = 0; i < TEST_LEN; i++) {
r = (r * 725861) % 6599;
a[i] = r;
}
sort(a, TEST_LEN, sizeof(*a), cmpint, NULL);
for (i = 0; i < TEST_LEN-1; i++)
KUNIT_EXPECT_LE(test, a[i], a[i + 1]);
}
在這個例子中,我們需要能夠分配一個數組來測試 sort() 函式。因此,如果存在分配錯誤,我們使用 KUNIT_ASSERT_NOT_ERR_OR_NULL() 來中止測試。
注意
在其他測試框架中,ASSERT 宏通常透過呼叫 return 來實現,因此它們僅適用於測試函式。在 KUnit 中,我們在失敗時停止當前的 kthread,因此您可以從任何地方呼叫它們。
注意
警告:上述規則有一個例外。你不應該在套件的 exit() 函式中,或者在資源的 free 函式中使用斷言。這些函式在測試關閉時執行,並且此處的斷言會阻止進一步的清理程式碼執行,可能導致記憶體洩漏。
自定義錯誤訊息¶
每個 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)都提供了測試套件的概念。測試套件是一個程式碼單元的測試用例集合,帶有可選的 setup 和 teardown 函式,這些函式在整個套件和/或每個測試用例之前/之後執行。
注意
測試用例只有在與測試套件關聯時才會執行。
例如
static struct kunit_case example_test_cases[] = {
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 測試框架。
注意
即使 init 或 suite_init 失敗,exit 和 suite_exit 函式也會執行。確保它們可以處理因 init 或 suite_init 遇到錯誤或提前退出而導致的任何不一致狀態。
kunit_test_suite(...) 是一個宏,它告訴連結器將指定的測試套件放在一個特殊的連結器節中,以便 KUnit 可以在 late_init 之後執行它,或者在載入測試模組時執行它(如果測試是作為模組構建的)。
有關更多資訊,請參閱 測試 API。
為其他架構編寫測試¶
最好編寫在 UML 上執行的測試,而不是僅在特定架構下執行的測試。最好編寫在 QEMU 或另一個易於獲取(且經濟上免費)的軟體環境中執行的測試,而不是特定硬體的測試。
儘管如此,仍然有充分的理由編寫特定於架構或硬體的測試。例如,我們可能想測試實際上屬於 arch/some-arch/* 中的程式碼。即便如此,也要嘗試編寫測試,使其不依賴於物理硬體。我們的一些測試用例可能不需要硬體,實際上只有少數測試需要硬體才能進行測試。當硬體不可用時,我們可以跳過測試,而不是停用測試。
現在我們已經明確了哪些位是特定於硬體的,編寫和執行測試的實際過程與編寫普通的 KUnit 測試相同。
重要提示
我們可能必須重置硬體狀態。如果這不可能,我們每次呼叫可能只能執行一個測試用例。
常見模式¶
隔離行為¶
單元測試將測試的程式碼量限制為單個單元。它控制當被測單元呼叫函式時執行哪些程式碼。函式作為 API 的一部分公開,以便可以在不影響程式碼庫其餘部分的情況下更改該函式的定義。在核心中,這來自兩個構造:類,它是包含實現者提供的函式指標的結構;以及特定於架構的函式,它們在編譯時選擇定義。
類¶
類不是 C 程式語言中內建的構造;但是,這是一個容易匯出的概念。因此,在大多數情況下,每個不使用標準化面向物件庫(如 GNOME 的 GObject)的專案都有自己略有不同的面向物件程式設計方式;Linux 核心也不例外。
核心面向物件程式設計的核心概念是類。在核心中,類是一個包含函式指標的結構。這在實現者和使用者之間建立了一個契約,因為它強制他們使用相同的函式簽名,而無需直接呼叫該函式。要成為一個類,函式指標必須指定指向該類的指標(稱為類控制代碼)是其中一個引數。因此,成員函式(也稱為方法)可以訪問成員變數(也稱為欄位),從而允許相同的實現具有多個例項。
類可以透過將父類嵌入到子類中來被覆蓋為子類。然後,當呼叫子類方法時,子類實現知道傳遞給它的指標是包含在子類中的父類的指標。因此,子類可以計算指向自身的指標,因為指向父類的指標始終是距指向子類的指標的固定偏移量。此偏移量是子類結構中包含的父類的偏移量。例如
struct shape {
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);
}
我們現在可以使用它來測試 struct eeprom_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");
請注意使用 KUNIT_EXPECT_STREQ 的 _MSG 版本來列印更詳細的錯誤,並使輔助宏中的斷言更清晰。
當多次呼叫相同的預期(在迴圈或輔助函式中)時,_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",
},
};
// Creates `sha1_gen_params()` to iterate over `cases` while using
// the struct member `str` for the case description.
KUNIT_ARRAY_PARAM_DESC(sha1, cases, str);
// 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 or KUNIT_ARRAY_PARAM_DESC.
static struct kunit_case sha1_test_cases[] = {
KUNIT_CASE_PARAM(sha1_test, sha1_gen_params),
{}
};
分配記憶體¶
您可以改為使用 kunit_kzalloc,而不是使用 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 測試中承擔與“cleanup”函式相同的角色,在支援它們的語言中承擔“defer”語句的角色,以及(在某些情況下)在 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 這樣只接受單個指標大小引數的函式,可以使用 KUNIT_DEFINE_ACTION_WRAPPER() 宏自動生成包裝器,例如
KUNIT_DEFINE_ACTION_WRAPPER(device_unregister, device_unregister_wrapper, struct device *);
kunit_add_action(test, &device_unregister_wrapper, &dev);
您應該這樣做,而不是手動轉換為 kunit_action_t 型別,因為轉換函式指標會破壞控制流完整性 (CFI)。
例如,如果系統記憶體不足,kunit_add_action 可能會失敗。您可以改用 kunit_add_action_or_reset,如果無法延遲該操作,則會立即執行該操作。
如果您需要更多地控制清理函式的呼叫時間,可以使用 kunit_release_action 提前觸發它,或者使用 kunit_remove_action 完全取消它。
測試靜態函式¶
如果您想測試靜態函式,而不將這些函式暴露在測試之外,一種選擇是有條件地匯出符號。啟用 KUnit 後,該符號將被公開,但否則仍保持靜態。要使用此方法,請按照以下模板進行操作。
/* In the file containing functions to test "my_file.c" */
#include <kunit/visibility.h>
#include <my_file.h>
...
VISIBLE_IF_KUNIT int do_interesting_thing()
{
...
}
EXPORT_SYMBOL_IF_KUNIT(do_interesting_thing);
/* In the header file "my_file.h" */
#if IS_ENABLED(CONFIG_KUNIT)
int do_interesting_thing(void);
#endif
/* In the KUnit test file "my_file_test.c" */
#include <kunit/visibility.h>
#include <my_file.h>
...
MODULE_IMPORT_NS(EXPORTED_FOR_KUNIT_TESTING);
...
// Use do_interesting_thing() in tests
有關完整示例,請參閱此 補丁,其中修改了測試,以使用上面的宏有條件地公開靜態函式以進行測試。
作為上述方法的替代方法,您可以有條件地在 .c 檔案的末尾 #include 測試檔案。不建議這樣做,但如果需要,可以使用。例如
/* 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,可以使此僅用於測試的程式碼更有用:訪問當前測試。
訪問當前測試¶
在某些情況下,我們需要從測試檔案外部呼叫僅用於測試的程式碼。例如,在提供函式的偽造實現或從錯誤處理程式中使任何當前測試失敗時,這很有用。我們可以透過 task_struct 中的 kunit_test 欄位來執行此操作,我們可以使用 kunit/test-bug.h 中的 kunit_get_current_test() 函式來訪問該欄位。
即使未啟用 KUnit,呼叫 kunit_get_current_test() 也是安全的。如果未啟用 KUnit,或者當前任務中未執行任何測試,它將返回 NULL。這會編譯為 no-op 或靜態金鑰檢查,因此在未執行任何測試時,效能影響可以忽略不計。
下面的示例使用它來實現函式 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);
}
在此示例中,我們使用 struct kunit 的 priv 成員作為從 init 函式將資料傳遞到測試的一種方式。通常,priv 是一個指標,可以用於任何使用者資料。這優於靜態變數,因為它避免了併發問題。
如果我們想要更靈活的東西,我們可以使用命名的 kunit_resource。每個測試可以有多個資源,這些資源具有字串名稱,提供與 priv 成員相同的靈活性,而且,例如,允許輔助函式建立資源而不會相互衝突。也可以為每個資源定義一個清理函式,從而很容易避免資源洩漏。有關更多資訊,請參閱 資源 API。
使當前測試失敗¶
如果我們想使當前測試失敗,我們可以使用 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,呼叫 kunit_fail_current_test() 也是安全的。如果未啟用 KUnit,或者當前任務中未執行任何測試,它將不執行任何操作。這會編譯為 no-op 或靜態金鑰檢查,因此在未執行任何測試時,效能影響可以忽略不計。
管理偽造裝置和驅動程式¶
在測試驅動程式或與驅動程式互動的程式碼時,許多函式將需要 struct device 或 struct device_driver。在許多情況下,測試任何給定函式都不需要設定真實裝置,因此可以改用偽造裝置。
KUnit 提供了輔助函式來建立和管理這些偽造裝置,這些裝置在內部是 struct kunit_device 型別,並附加到特殊的 kunit_bus。這些裝置支援託管裝置資源 (devres),如 Devres - 託管裝置資源 中所述
要建立 KUnit 管理的 struct device_driver,請使用 kunit_driver_create(),它將在 kunit_bus 上建立具有給定名稱的驅動程式。此驅動程式將在相應的測試完成後自動銷燬,但也可以使用 driver_unregister() 手動銷燬。
要建立偽造裝置,請使用 kunit_device_register(),它將建立並註冊一個裝置,使用使用 kunit_driver_create() 建立的新的 KUnit 管理的驅動程式。要提供特定的非 KUnit 管理的驅動程式,請改用 kunit_device_register_with_driver()。與託管驅動程式一樣,KUnit 管理的偽造裝置會在測試完成後自動清理,但可以使用 kunit_device_unregister() 提前手動清理。
在裝置不是平臺裝置的情況下,應優先使用 KUnit 裝置,而不是 root_device_register(),而應使用 platform_device_register()。
例如
#include <kunit/device.h>
static void test_my_device(struct kunit *test)
{
struct device *fake_device;
const char *dev_managed_string;
// Create a fake device.
fake_device = kunit_device_register(test, "my_device");
KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fake_device)
// Pass it to functions which need a device.
dev_managed_string = devm_kstrdup(fake_device, "Hello, World!");
// Everything is cleaned up automatically when the test ends.
}