From f85be67abbaeccca96be166b41cfa117e91d6023 Mon Sep 17 00:00:00 2001 From: Vlada Kanivets Date: Wed, 20 Aug 2025 17:10:22 +0200 Subject: [PATCH 1/4] add tests for Thread --- test/CMakeLists.txt | 1 + test/ThreadTest.cpp | 155 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 test/ThreadTest.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6dccd52..1a3d8e7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -59,6 +59,7 @@ wdk_add_driver(kf-test WINVER NTDDI_WIN10 STL AutoSpinLockTest.cpp EResourceSharedLockTest.cpp RecursiveAutoSpinLockTest.cpp + ThreadTest.cpp ) target_link_libraries(kf-test kf::kf kmtest::kmtest) diff --git a/test/ThreadTest.cpp b/test/ThreadTest.cpp new file mode 100644 index 0000000..dde43fb --- /dev/null +++ b/test/ThreadTest.cpp @@ -0,0 +1,155 @@ +#include "pch.h" +#include + +namespace +{ + struct TestObject + { + NTSTATUS run() + { + value = 123; + return STATUS_SUCCESS; + } + + int value = 0; + }; +} + +SCENARIO("kf::Thread") +{ + GIVEN("Default-constructed thread") + { + kf::Thread thread; + + WHEN("Starting a thread with lambda") + { + int value = 0; + NTSTATUS status = thread.start([](void* context) { + auto p = static_cast(context); + *p = 123; + }, &value); + + THEN("Status is successful") + { + REQUIRE(NT_SUCCESS(status)); + } + + thread.join(); + + THEN("Thread changed the given value") + { + REQUIRE(value == 123); + } + } + + WHEN("Thread is not started and joined") + { + thread.join(); + + THEN("Does nothing and no crash occurs") + { + } + } + + WHEN("Calling join twice after start") + { + REQUIRE_NT_SUCCESS(thread.start([](void*) { }, nullptr)); + + thread.join(); + thread.join(); + + THEN("Does nothing and no crash occurs") + { + } + } + } + + GIVEN("Thread with a member routine") + { + kf::Thread thread; + TestObject obj; + + WHEN("Starting a thread with member function") + { + NTSTATUS status = thread.start<&TestObject::run>(&obj); + + THEN("Status is successful") + { + REQUIRE(NT_SUCCESS(status)); + } + + thread.join(); + + THEN("Thread called the function") + { + REQUIRE(obj.value == 123); + } + } + } + + GIVEN("Thread with long working function") + { + kf::Thread thread; + int value = 0; + + WHEN("Starting a thread with long working function and joined") + { + REQUIRE_NT_SUCCESS(thread.start([](void* context) { + auto p = static_cast(context); + LARGE_INTEGER interval; + interval.QuadPart = -10'000; + KeDelayExecutionThread(KernelMode, FALSE, &interval); + *p = 246; + }, &value)); + + thread.join(); + + THEN("Main thread execution continued after given thread finished its work") + { + REQUIRE(value == 246); + } + } + } + + GIVEN("Thread that goes out of scope") + { + int value = 0; + { + kf::Thread thread; + REQUIRE_NT_SUCCESS(thread.start([](void* context) { + auto p = static_cast(context); + *p = 777; + }, &value)); + } + + THEN("Destructor joined and work was finished") + { + REQUIRE(value == 777); + } + } + + GIVEN("Working thread") + { + kf::Thread thread; + int value = 0; + + REQUIRE_NT_SUCCESS(thread.start([](void* context) { + auto p = static_cast(context); + LARGE_INTEGER interval; + interval.QuadPart = -10'000; + KeDelayExecutionThread(KernelMode, FALSE, &interval); + *p = 999; + }, &value)); + + WHEN("Thread is moved to another instance") + { + kf::Thread thread2 = std::move(thread); + thread2.join(); + + THEN("Function form thread is completed") + { + REQUIRE(value == 999); + } + } + } +} From d22eff4d3f313be2537cf7808edc1c55d40b9d9d Mon Sep 17 00:00:00 2001 From: Sergey Podobry Date: Fri, 16 Jan 2026 15:27:47 +0200 Subject: [PATCH 2/4] Refactor tests --- test/ThreadTest.cpp | 212 ++++++++++++++++++++++++++++---------------- 1 file changed, 137 insertions(+), 75 deletions(-) diff --git a/test/ThreadTest.cpp b/test/ThreadTest.cpp index dde43fb..61e1850 100644 --- a/test/ThreadTest.cpp +++ b/test/ThreadTest.cpp @@ -3,152 +3,214 @@ namespace { + struct ThreadContext + { + bool started = false; + }; + + void threadProc(void* context) + { + static_cast(context)->started = true; + PsTerminateSystemThread(STATUS_SUCCESS); + } + struct TestObject { + ThreadContext m_threadContext; + NTSTATUS run() { - value = 123; + m_threadContext.started = true; return STATUS_SUCCESS; } - - int value = 0; }; } -SCENARIO("kf::Thread") +SCENARIO("kf::Thread basic lifecycle") { - GIVEN("Default-constructed thread") + GIVEN("A default constructed thread") { kf::Thread thread; - WHEN("Starting a thread with lambda") + WHEN("join() is called without start()") + { + thread.join(); + + THEN("it should be a no-op") + { + REQUIRE(true); + } + } + + WHEN("start() is used with threadProc") { - int value = 0; - NTSTATUS status = thread.start([](void* context) { - auto p = static_cast(context); - *p = 123; - }, &value); + ThreadContext context; - THEN("Status is successful") + const NTSTATUS status = thread.start(&threadProc, &context); + + THEN("start() should succeed") + { + REQUIRE_NT_SUCCESS(status); + } + + THEN("join() waits for completion") + { + thread.join(); + REQUIRE(context.started); + } + } + + WHEN("start() is used with lambda") + { + ThreadContext context; + + NTSTATUS status = thread.start([](void* context) + { + static_cast(context)->started = true; + PsTerminateSystemThread(STATUS_SUCCESS); + }, + &context); + + THEN("start() should succeed") { REQUIRE(NT_SUCCESS(status)); } thread.join(); - THEN("Thread changed the given value") + THEN("lambda should run") { - REQUIRE(value == 123); + REQUIRE(context.started); } } - WHEN("Thread is not started and joined") + WHEN("start() is used with object member routine") { + TestObject obj; + + const NTSTATUS status = thread.start<&TestObject::run>(&obj); + + THEN("start() should succeed") + { + REQUIRE_NT_SUCCESS(status); + } + thread.join(); - THEN("Does nothing and no crash occurs") + THEN("member routine should run") { + REQUIRE(obj.m_threadContext.started); } } - WHEN("Calling join twice after start") + WHEN("calling join twice after start") { - REQUIRE_NT_SUCCESS(thread.start([](void*) { }, nullptr)); + REQUIRE_NT_SUCCESS(thread.start([](void*) { PsTerminateSystemThread(STATUS_SUCCESS); }, nullptr)); thread.join(); thread.join(); - THEN("Does nothing and no crash occurs") + THEN("it's safe to call join() multiple times") { } } - } - GIVEN("Thread with a member routine") - { - kf::Thread thread; - TestObject obj; - - WHEN("Starting a thread with member function") + WHEN("thread object is destroyed while running") { - NTSTATUS status = thread.start<&TestObject::run>(&obj); + ThreadContext context; - THEN("Status is successful") { - REQUIRE(NT_SUCCESS(status)); + kf::Thread local; + REQUIRE_NT_SUCCESS(local.start(&threadProc, &context)); } - thread.join(); - - THEN("Thread called the function") + THEN("destructor should join and thread should have run") { - REQUIRE(obj.value == 123); + REQUIRE(context.started); } } - } - GIVEN("Thread with long working function") - { - kf::Thread thread; - int value = 0; - - WHEN("Starting a thread with long working function and joined") + WHEN("starting a thread with a long working function") { - REQUIRE_NT_SUCCESS(thread.start([](void* context) { - auto p = static_cast(context); - LARGE_INTEGER interval; - interval.QuadPart = -10'000; - KeDelayExecutionThread(KernelMode, FALSE, &interval); - *p = 246; - }, &value)); + ThreadContext context; + + REQUIRE_NT_SUCCESS(thread.start([](void* context) + { + LARGE_INTEGER oneMillisecond{ .QuadPart = -10'000LL }; // 1ms + KeDelayExecutionThread(KernelMode, false, &oneMillisecond); + + static_cast(context)->started = true; + PsTerminateSystemThread(STATUS_SUCCESS); + }, + &context)); thread.join(); - THEN("Main thread execution continued after given thread finished its work") + THEN("join() waits for completion") { - REQUIRE(value == 246); + thread.join(); + REQUIRE(context.started); } } } +} - GIVEN("Thread that goes out of scope") +SCENARIO("kf::Thread move constructor and move assignment") +{ + GIVEN("A started thread") { - int value = 0; - { - kf::Thread thread; - REQUIRE_NT_SUCCESS(thread.start([](void* context) { - auto p = static_cast(context); - *p = 777; - }, &value)); - } + ThreadContext context; + kf::Thread thread1; + REQUIRE_NT_SUCCESS(thread1.start(&threadProc, &context)); - THEN("Destructor joined and work was finished") + WHEN("Thread is move-constructed") { - REQUIRE(value == 777); + kf::Thread thread2(std::move(thread1)); + + THEN("moved-to object should be able to join") + { + thread2.join(); + REQUIRE(context.started); + } + + THEN("moved-from object join() should be safe") + { + thread1.join(); + REQUIRE(context.started); + } } } - GIVEN("Working thread") + GIVEN("Two threads where left-hand side already owns a running thread") { - kf::Thread thread; - int value = 0; + ThreadContext context1; + ThreadContext context2; - REQUIRE_NT_SUCCESS(thread.start([](void* context) { - auto p = static_cast(context); - LARGE_INTEGER interval; - interval.QuadPart = -10'000; - KeDelayExecutionThread(KernelMode, FALSE, &interval); - *p = 999; - }, &value)); + kf::Thread thread1; + kf::Thread thread2; - WHEN("Thread is moved to another instance") + REQUIRE_NT_SUCCESS(thread1.start(&threadProc, &context1)); + REQUIRE_NT_SUCCESS(thread2.start(&threadProc, &context2)); + + WHEN("Move-assigning thread1 = std::move(thread2)") { - kf::Thread thread2 = std::move(thread); - thread2.join(); + thread1 = std::move(thread2); + + THEN("thread1 should join the thread it now owns") + { + thread1.join(); + REQUIRE(context2.started); + } + + THEN("thread2 should be in a valid empty state") + { + thread2.join(); + REQUIRE(true); + } - THEN("Function form thread is completed") + THEN("the original thread from thread1 should have executed") { - REQUIRE(value == 999); + REQUIRE(context1.started); } } } From b254ce3fa4b9ad83177edcdbb2f676deb1c8e491 Mon Sep 17 00:00:00 2001 From: Sergey Podobry Date: Fri, 16 Jan 2026 15:33:00 +0200 Subject: [PATCH 3/4] Update test/ThreadTest.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/ThreadTest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ThreadTest.cpp b/test/ThreadTest.cpp index 61e1850..35ef2ad 100644 --- a/test/ThreadTest.cpp +++ b/test/ThreadTest.cpp @@ -73,7 +73,7 @@ SCENARIO("kf::Thread basic lifecycle") THEN("start() should succeed") { - REQUIRE(NT_SUCCESS(status)); + REQUIRE_NT_SUCCESS(status); } thread.join(); From e6dfbd79daeab28317aaac168ab909c3e9524682 Mon Sep 17 00:00:00 2001 From: Sergey Podobry Date: Fri, 16 Jan 2026 15:35:42 +0200 Subject: [PATCH 4/4] Fix review comments --- test/ThreadTest.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/ThreadTest.cpp b/test/ThreadTest.cpp index 35ef2ad..02f8122 100644 --- a/test/ThreadTest.cpp +++ b/test/ThreadTest.cpp @@ -112,6 +112,7 @@ SCENARIO("kf::Thread basic lifecycle") THEN("it's safe to call join() multiple times") { + REQUIRE(true); } } @@ -144,8 +145,6 @@ SCENARIO("kf::Thread basic lifecycle") }, &context)); - thread.join(); - THEN("join() waits for completion") { thread.join();