Skip to content

Commit 17889ce

Browse files
committed
Added a test
1 parent a4aa192 commit 17889ce

1 file changed

Lines changed: 241 additions & 0 deletions

File tree

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
/*
2+
* Copyright 2026 Datadog, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include <gtest/gtest.h>
18+
#include "threadLocal.h"
19+
#include "gtest_crash_handler.h"
20+
#include <atomic>
21+
#include <cmath>
22+
#include <cstdint>
23+
#include <thread>
24+
#include <vector>
25+
26+
static constexpr char THREADLOCAL_TEST_NAME[] = "ThreadLocalTest";
27+
28+
// NOTE on the instances below being namespace-scope `static`:
29+
// ThreadLocal's destructor calls pthread_key_delete() but never resets
30+
// _key_once, so destroying an instance and then constructing another of the
31+
// same type would operate on a deleted key (UB). The production code only ever
32+
// uses `static ThreadLocal<...>` locals (destroyed at process exit), so the
33+
// tests mirror that: one long-lived instance per distinct type. Per-thread
34+
// behaviour is exercised in freshly spawned threads, which start with empty
35+
// thread-specific storage.
36+
37+
// ---- generic pointer specialization: plain set/get, no create/clean ----
38+
static ThreadLocal<intptr_t> g_int_tl;
39+
40+
// ---- lazy-create + cleanup instrumentation ----
41+
static std::atomic<int> g_created{0};
42+
static std::atomic<int> g_freed{0};
43+
44+
static void *create_tracked() {
45+
g_created.fetch_add(1, std::memory_order_relaxed);
46+
return new int(1234);
47+
}
48+
49+
static void free_tracked(void *p) {
50+
g_freed.fetch_add(1, std::memory_order_relaxed);
51+
delete static_cast<int *>(p);
52+
}
53+
54+
static ThreadLocal<int *, create_tracked, free_tracked> g_tracked_tl;
55+
56+
// ---- double specialization ----
57+
static ThreadLocal<double> g_double_tl;
58+
59+
class ThreadLocalTest : public ::testing::Test {
60+
protected:
61+
void SetUp() override {
62+
installGtestCrashHandler<THREADLOCAL_TEST_NAME>();
63+
}
64+
void TearDown() override {
65+
restoreDefaultSignalHandlers();
66+
}
67+
};
68+
69+
// set() then get() round-trips a value on the same thread.
70+
TEST_F(ThreadLocalTest, Generic_SetGetRoundTrip) {
71+
g_int_tl.set(42);
72+
EXPECT_EQ(42, g_int_tl.get());
73+
g_int_tl.set(-7);
74+
EXPECT_EQ(-7, g_int_tl.get());
75+
}
76+
77+
// Each thread sees only its own value: storage is per-thread, not shared.
78+
TEST_F(ThreadLocalTest, Generic_PerThreadIsolation) {
79+
constexpr int kThreads = 8;
80+
std::atomic<int> ready{0};
81+
std::atomic<bool> go{false};
82+
std::vector<std::thread> threads;
83+
std::atomic<int> mismatches{0};
84+
85+
for (int i = 0; i < kThreads; ++i) {
86+
threads.emplace_back([&, i] {
87+
// Fresh thread: storage must start empty.
88+
if (g_int_tl.get() != 0) {
89+
mismatches.fetch_add(1, std::memory_order_relaxed);
90+
}
91+
g_int_tl.set(i + 1);
92+
93+
// Barrier: every thread writes before any thread reads back, so a
94+
// shared (buggy) slot would be observably clobbered.
95+
ready.fetch_add(1, std::memory_order_relaxed);
96+
while (!go.load(std::memory_order_acquire)) {
97+
}
98+
99+
if (g_int_tl.get() != static_cast<intptr_t>(i + 1)) {
100+
mismatches.fetch_add(1, std::memory_order_relaxed);
101+
}
102+
});
103+
}
104+
105+
while (ready.load(std::memory_order_relaxed) != kThreads) {
106+
}
107+
go.store(true, std::memory_order_release);
108+
109+
for (auto &t : threads) {
110+
t.join();
111+
}
112+
EXPECT_EQ(0, mismatches.load());
113+
}
114+
115+
// A fresh thread that never called set() reads the zero-initialized default.
116+
TEST_F(ThreadLocalTest, Generic_UnsetIsZero) {
117+
intptr_t observed = -1;
118+
std::thread t([&] { observed = g_int_tl.get(); });
119+
t.join();
120+
EXPECT_EQ(0, observed);
121+
}
122+
123+
// The create function lazily initializes storage on first get() and is invoked
124+
// exactly once per thread; subsequent get()s return the same pointer.
125+
TEST_F(ThreadLocalTest, Lazy_CreateOncePerThread) {
126+
g_created.store(0, std::memory_order_relaxed);
127+
g_freed.store(0, std::memory_order_relaxed);
128+
129+
int *first = nullptr;
130+
int *second = nullptr;
131+
int value = 0;
132+
std::thread t([&] {
133+
first = g_tracked_tl.get();
134+
second = g_tracked_tl.get();
135+
// Read the payload here: free_tracked() deletes it on thread exit, so
136+
// dereferencing first/second after join() would be use-after-free.
137+
value = *first;
138+
});
139+
t.join();
140+
141+
ASSERT_NE(nullptr, first);
142+
EXPECT_EQ(first, second); // same instance reused (pointer compare only)
143+
EXPECT_EQ(1234, value); // created via create_tracked()
144+
EXPECT_EQ(1, g_created.load()); // created exactly once
145+
}
146+
147+
// The clean function runs when the owning thread exits, freeing per-thread state.
148+
TEST_F(ThreadLocalTest, Lazy_CleanupOnThreadExit) {
149+
g_created.store(0, std::memory_order_relaxed);
150+
g_freed.store(0, std::memory_order_relaxed);
151+
152+
std::thread t([&] {
153+
// Touch storage so a value exists to be cleaned up on exit.
154+
ASSERT_NE(nullptr, g_tracked_tl.get());
155+
});
156+
t.join();
157+
// After join the thread has fully terminated, so its TSD destructor
158+
// (free_tracked) must have run.
159+
EXPECT_EQ(1, g_created.load());
160+
EXPECT_EQ(1, g_freed.load());
161+
}
162+
163+
// Independent threads each create and free their own value.
164+
TEST_F(ThreadLocalTest, Lazy_CleanupAcrossManyThreads) {
165+
g_created.store(0, std::memory_order_relaxed);
166+
g_freed.store(0, std::memory_order_relaxed);
167+
168+
constexpr int kThreads = 16;
169+
std::vector<std::thread> threads;
170+
for (int i = 0; i < kThreads; ++i) {
171+
threads.emplace_back([] { (void)g_tracked_tl.get(); });
172+
}
173+
for (auto &t : threads) {
174+
t.join();
175+
}
176+
EXPECT_EQ(kThreads, g_created.load());
177+
EXPECT_EQ(kThreads, g_freed.load());
178+
}
179+
180+
// The double specialization preserves the exact bit pattern through the
181+
// u64<->void* round-trip (on 64-bit targets, where a double fits in a pointer).
182+
TEST_F(ThreadLocalTest, Double_RoundTripPreservesValue) {
183+
static_assert(sizeof(void *) >= sizeof(double),
184+
"ThreadLocal<double> requires pointer >= double width");
185+
186+
const double values[] = {
187+
0.0,
188+
1.0,
189+
-1.0,
190+
3.141592653589793,
191+
-2.718281828459045,
192+
1.7976931348623157e308, // near DBL_MAX
193+
2.2250738585072014e-308, // near DBL_MIN (smallest normal)
194+
4.9e-324, // smallest subnormal
195+
};
196+
197+
std::atomic<int> mismatches{0};
198+
std::thread t([&] {
199+
for (double v : values) {
200+
g_double_tl.set(v);
201+
if (g_double_tl.get() != v) {
202+
mismatches.fetch_add(1, std::memory_order_relaxed);
203+
}
204+
}
205+
});
206+
t.join();
207+
EXPECT_EQ(0, mismatches.load());
208+
}
209+
210+
// An unset double reads back as 0.0 (matches the original `thread_local double = 0`).
211+
TEST_F(ThreadLocalTest, Double_UnsetIsZero) {
212+
double observed = -1.0;
213+
std::thread t([&] { observed = g_double_tl.get(); });
214+
t.join();
215+
EXPECT_EQ(0.0, observed);
216+
}
217+
218+
// Per-thread accumulation mirrors LivenessTracker's `skipped` usage: each thread
219+
// keeps its own running sum, isolated from the others.
220+
TEST_F(ThreadLocalTest, Double_PerThreadAccumulation) {
221+
constexpr int kThreads = 8;
222+
constexpr int kIters = 1000;
223+
std::atomic<int> mismatches{0};
224+
std::vector<std::thread> threads;
225+
226+
for (int i = 0; i < kThreads; ++i) {
227+
threads.emplace_back([&, i] {
228+
const double step = static_cast<double>(i + 1);
229+
for (int k = 0; k < kIters; ++k) {
230+
g_double_tl.set(g_double_tl.get() + step);
231+
}
232+
if (g_double_tl.get() != step * kIters) {
233+
mismatches.fetch_add(1, std::memory_order_relaxed);
234+
}
235+
});
236+
}
237+
for (auto &t : threads) {
238+
t.join();
239+
}
240+
EXPECT_EQ(0, mismatches.load());
241+
}

0 commit comments

Comments
 (0)