Source
stdlib/tests/thread_test.cpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
// Unit tests for the `thread` module — spawn/Thread only (shared state is the memory module's4
// job and is tested there). Everything here is DETERMINISTIC: results are observed through5
// join-ordering or non-copyable accumulators (a std::atomic passed by reference), never through6
// timing. Iteration counts stay small — this suite also runs under Valgrind, ASan, and TSan.8
#include <atomic>9
#include <memory>10
#include <stdexcept>11
#include <string>12
#include <utility>14
#include <gtest/gtest.h>16
#include "thread.hpp"18
namespace thr = cheatah::thread;20
// ── spawn: runs the worker, copies copyables, references non-copyables, moves rvalues ──────22
TEST(CheatahThread, SpawnRunsTheWorker) {23
std::atomic<long long> out{0}; // non-copyable -> passed by reference (the one sharing path)24
auto t = thr::spawn([](std::atomic<long long>& o, long long n) { o = n * 2; }, out, 21);25
t.join();26
EXPECT_EQ(out.load(), 42);27
}29
TEST(CheatahThread, SpawnRunsTheGenericLambdaLowering) {30
// The shape purrc emits for an UNTYPED cheatah fn passed by name: a capture-less generic31
// forwarding lambda. Must deduce into spawn and run.32
std::atomic<long long> out{0};33
auto generic = [](auto&&... a) { ((void)0, ..., void(a)); };34
auto t = thr::spawn(generic, 1, 2.5, std::string("x"));35
t.join();36
auto u = thr::spawn([](auto&& o, auto&& n) { o += n; }, out, 5);37
u.join();38
EXPECT_EQ(out.load(), 5);39
}41
TEST(CheatahThread, CopyableArgumentsAreCopied) {42
// A copyable lvalue is decay-copied into the thread: the worker's writes stay its own.43
std::string mine = "caller";44
auto t = thr::spawn([](std::string& s) { s += "-worker"; }, mine);45
t.join();46
EXPECT_EQ(mine, "caller"); // untouched — the worker mutated its own copy47
}49
TEST(CheatahThread, SpawnPassesANonCopyableByReference) {50
// Two workers share ONE non-copyable, non-movable object by reference — exact final state.51
std::atomic<long long> sum{0};52
{53
auto a = thr::spawn([](std::atomic<long long>& s) { for (int i = 0; i < 1000; ++i) ++s; }, sum);54
auto b = thr::spawn([](std::atomic<long long>& s) { for (int i = 0; i < 1000; ++i) ++s; }, sum);55
} // both guards join here56
EXPECT_EQ(sum.load(), 2000);57
}59
TEST(CheatahThread, SpawnMovesANonCopyableRvalue) {60
// A move-only RVALUE (a factory result — e.g. a socket/io guard) is moved INTO the thread.61
std::atomic<long long> out{0};62
auto t = thr::spawn(63
[](std::atomic<long long>& o, std::unique_ptr<long long>& p) { o = *p; },64
out, std::make_unique<long long>(7));65
t.join();66
EXPECT_EQ(out.load(), 7);67
}69
// ── join: rethrow, one-shot, joinable lifecycle ─────────────────────────────────────────────71
TEST(CheatahThread, JoinRethrowsTheWorkersException) {72
auto t = thr::spawn([] { throw std::runtime_error("kaboom"); });73
try {74
t.join();75
FAIL() << "join() must rethrow the worker's exception";76
} catch (const std::runtime_error& e) {77
EXPECT_STREQ(e.what(), "kaboom");78
}79
EXPECT_FALSE(t.joinable());80
}82
TEST(CheatahThread, JoinOnNothingRaises) {83
auto t = thr::spawn([] {});84
t.join();85
EXPECT_THROW(t.join(), std::runtime_error); // one-shot: a second join raises86
}88
TEST(CheatahThread, JoinableLifecycle) {89
std::atomic<bool> release{false};90
auto t = thr::spawn([](std::atomic<bool>& r) { while (!r) {} }, release);91
EXPECT_TRUE(t.joinable());92
release = true;93
t.join();94
EXPECT_FALSE(t.joinable());95
}97
// ── the guard: move-only ownership, join-on-destroy, honest error reporting ────────────────99
TEST(CheatahThread, MoveTransfersOwnership) {100
std::atomic<long long> out{0};101
auto a = thr::spawn([](std::atomic<long long>& o) { o = 1; }, out);102
thr::Thread b = std::move(a);103
EXPECT_FALSE(a.joinable()); // NOLINT(bugprone-use-after-move) — moved-from state is the test104
EXPECT_TRUE(b.joinable());105
b.join();106
EXPECT_EQ(out.load(), 1);107
}109
TEST(CheatahThread, MoveAssignSettlesTheOldThread) {110
// Assigning over a guard whose worker threw must JOIN it and REPORT the unobserved error.111
testing::internal::CaptureStderr();112
auto loser = thr::spawn([] { throw std::runtime_error("lost update"); });113
std::atomic<long long> out{0};114
loser = thr::spawn([](std::atomic<long long>& o) { o = 9; }, out);115
const std::string err = testing::internal::GetCapturedStderr();116
EXPECT_NE(err.find("cheatah thread: unhandled exception in thread: lost update"),117
std::string::npos);118
loser.join();119
EXPECT_EQ(out.load(), 9);120
}122
TEST(CheatahThread, DestructorJoinsARunningThread) {123
std::atomic<long long> done{0};124
std::atomic<bool> release{false};125
{126
auto t = thr::spawn(127
[](std::atomic<long long>& d, std::atomic<bool>& r) {128
while (!r) {}129
d = 1;130
},131
done, release);132
release = true;133
} // guard drops while the worker may still be running -> destructor joins134
EXPECT_EQ(done.load(), 1);135
}137
TEST(CheatahThread, DestructorReportsAnUnobservedException) {138
testing::internal::CaptureStderr();139
{ auto t = thr::spawn([] { throw std::runtime_error("nobody joined me"); }); }140
const std::string err = testing::internal::GetCapturedStderr();141
EXPECT_NE(err.find("cheatah thread: unhandled exception in thread: nobody joined me"),142
std::string::npos);143
}145
TEST(CheatahThread, DestructorReportsANonStdException) {146
// A worker can escape with anything; without a `what()` the report says so honestly.147
testing::internal::CaptureStderr();148
{ auto t = thr::spawn([] { throw 42; }); }149
const std::string err = testing::internal::GetCapturedStderr();150
EXPECT_NE(err.find("cheatah thread: unhandled exception in thread: unknown error"),151
std::string::npos);152
}154
// ── compile-time contract: the concepts reject what must not compile ────────────────────────156
TEST(CheatahThread, ConceptsRejectUnholdableArguments) {157
struct Pinned { // non-copyable, non-movable — the memory::Owner shape158
Pinned() = default;159
Pinned(const Pinned&) = delete;160
Pinned& operator=(const Pinned&) = delete;161
};162
static_assert(thr::SpawnArg<Pinned&>, "a pinned lvalue travels by reference");163
static_assert(!thr::SpawnArg<Pinned>, "a pinned TEMPORARY would dangle — must not compile");164
static_assert(thr::SpawnArg<int>, "values are copied in");165
static_assert(thr::SpawnArg<std::unique_ptr<int>>, "a movable rvalue is moved in");166
static_assert(thr::SpawnCallable<void (*)(long long), int>,167
"the fully-typed fn-pointer lowering is spawnable");168
static_assert(!thr::SpawnCallable<void (*)(long long)>,169
"arity mismatches are rejected at compile time");170
SUCCEED();171
}