GCC Code Coverage Report


Directory: ../
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 0 / 94 / 94
Functions: -% 0 / 22 / 22
Branches: -% 0 / 257 / 257

test/unittest/UnitThreadPool.cpp
Line Branch Exec Source
1 // Copyright (c) 2021-2026 ChilliBits. All rights reserved.
2
3 #include <gtest/gtest.h>
4
5 #include <atomic>
6 #include <mutex>
7 #include <ranges>
8 #include <stdexcept>
9 #include <string>
10 #include <thread>
11 #include <unordered_map>
12
13 #include <util/Concurrency.h>
14 #include <util/ThreadPool.h>
15
16 // LCOV_EXCL_START
17
18 namespace spice::testing {
19
20 using namespace spice::compiler;
21
22 TEST(ThreadPoolTest, RunsAllSubmittedTasks) {
23 ThreadPool pool(4);
24 std::atomic<size_t> executedTasks = 0;
25 for (size_t i = 0; i < 1000; i++)
26 pool.submit([&executedTasks] { ++executedTasks; });
27 pool.waitForAll();
28
29 EXPECT_EQ(1000u, executedTasks.load());
30 EXPECT_EQ(4u, pool.getThreadCount());
31 }
32
33 TEST(ThreadPoolTest, RethrowsExceptionOfEarliestFailingTask) {
34 ThreadPool pool(4);
35 // Two tasks fail. Independent of which worker gets there first, the failure of the task that was submitted first has
36 // to win, so that the reported compiler error does not depend on the scheduling.
37 for (size_t i = 0; i < 200; i++)
38 pool.submit([i] {
39 if (i == 7 || i == 90)
40 throw std::runtime_error("task " + std::to_string(i));
41 });
42
43 try {
44 pool.waitForAll();
45 FAIL() << "Expected the failure of the earliest task to be re-thrown"; // GCOV_EXCL_LINE
46 } catch (const std::runtime_error &e) {
47 EXPECT_EQ("task 7", std::string(e.what()));
48 }
49 }
50
51 TEST(ThreadPoolTest, IsReusableAfterFailure) {
52 ThreadPool pool(2);
53 pool.submit([] { throw std::runtime_error("boom"); });
54 EXPECT_THROW(pool.waitForAll(), std::runtime_error);
55
56 std::atomic<size_t> executedTasks = 0;
57 for (size_t i = 0; i < 100; i++)
58 pool.submit([&executedTasks] { ++executedTasks; });
59 pool.waitForAll();
60
61 EXPECT_EQ(100u, executedTasks.load());
62 }
63
64 TEST(ConcurrencyTest, ConditionalLockOnlyLocksInParallelSection) {
65 std::mutex mutex;
66 // try_lock on a mutex that is already held by the calling thread is undefined behavior, so the probe has to run on
67 // another thread
68 const auto isLockedByUs = [&mutex] {
69 bool couldLock = false;
70 std::thread probe([&] {
71 couldLock = mutex.try_lock();
72 if (couldLock)
73 mutex.unlock();
74 });
75 probe.join();
76 return !couldLock;
77 };
78
79 ASSERT_FALSE(concurrentPassesRunning.load());
80
81 { // Outside of a parallel section the lock is a no-op
82 ConditionalLock lock(mutex);
83 EXPECT_FALSE(isLockedByUs());
84 }
85
86 { // Inside a parallel section the mutex is held for the lifetime of the lock
87 const ParallelSection parallelSection;
88 ASSERT_TRUE(concurrentPassesRunning.load());
89 ConditionalLock lock(mutex);
90 EXPECT_TRUE(isLockedByUs());
91 }
92
93 EXPECT_FALSE(concurrentPassesRunning.load());
94 }
95
96 TEST(ConcurrencyTest, ConditionalLockGuardsSharedMapUnderContention) {
97 ThreadPool pool(4);
98 std::unordered_map<size_t, size_t> sharedMap; // Stands in for the type registry / lookup caches
99
100 {
101 std::mutex mutex;
102 const ParallelSection parallelSection;
103 for (size_t i = 0; i < 4; i++)
104 pool.submit([&] {
105 for (size_t key = 0; key < 10000; key++) {
106 ConditionalLock lock(mutex);
107 sharedMap[key % 500]++;
108 }
109 });
110 pool.waitForAll();
111 }
112
113 ASSERT_EQ(500u, sharedMap.size());
114 for (const auto &count : sharedMap | std::views::values)
115 EXPECT_EQ(4u * 20u, count);
116 }
117
118 } // namespace spice::testing
119
120 // LCOV_EXCL_STOP
121