cheatah
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's
4// job and is tested there). Everything here is DETERMINISTIC: results are observed through
5// join-ordering or non-copyable accumulators (a std::atomic passed by reference), never through
6// 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"
18namespace thr = cheatah::thread;
20// ── spawn: runs the worker, copies copyables, references non-copyables, moves rvalues ──────
22TEST(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);
29TEST(CheatahThread, SpawnRunsTheGenericLambdaLowering) {
30 // The shape purrc emits for an UNTYPED cheatah fn passed by name: a capture-less generic
31 // 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);
41TEST(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 copy
49TEST(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 here
56 EXPECT_EQ(sum.load(), 2000);
59TEST(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);
69// ── join: rethrow, one-shot, joinable lifecycle ─────────────────────────────────────────────
71TEST(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());
82TEST(CheatahThread, JoinOnNothingRaises) {
83 auto t = thr::spawn([] {});
84 t.join();
85 EXPECT_THROW(t.join(), std::runtime_error); // one-shot: a second join raises
88TEST(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());
97// ── the guard: move-only ownership, join-on-destroy, honest error reporting ────────────────
99TEST(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 test
104 EXPECT_TRUE(b.joinable());
105 b.join();
106 EXPECT_EQ(out.load(), 1);
109TEST(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);
122TEST(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 joins
134 EXPECT_EQ(done.load(), 1);
137TEST(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);
145TEST(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);
154// ── compile-time contract: the concepts reject what must not compile ────────────────────────
156TEST(CheatahThread, ConceptsRejectUnholdableArguments) {
157 struct Pinned { // non-copyable, non-movable — the memory::Owner shape
158 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();