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