btcsignals_tests.cpp
1 // Copyright (c) The Bitcoin Core developers 2 // Distributed under the MIT software license, see the accompanying 3 // file COPYING or http://www.opensource.org/licenses/mit-license.php. 4 5 #include <btcsignals.h> 6 #include <test/util/setup_common.h> 7 8 #include <boost/test/unit_test.hpp> 9 10 #include <semaphore> 11 12 namespace { 13 14 15 struct MoveOnlyData { 16 MoveOnlyData(int data) : m_data(data) {} 17 MoveOnlyData(MoveOnlyData&&) = default; 18 19 MoveOnlyData& operator=(MoveOnlyData&&) = delete; 20 MoveOnlyData(const MoveOnlyData&) = delete; 21 MoveOnlyData& operator=(const MoveOnlyData&) = delete; 22 23 int m_data; 24 }; 25 26 MoveOnlyData MoveOnlyReturnCallback(int val) 27 { 28 return {val}; 29 } 30 31 void IncrementCallback(int& val) 32 { 33 val++; 34 } 35 void SquareCallback(int& val) 36 { 37 val *= val; 38 } 39 40 bool ReturnTrue() 41 { 42 return true; 43 } 44 bool ReturnFalse() 45 { 46 return false; 47 } 48 49 } // anonymous namespace 50 51 BOOST_FIXTURE_TEST_SUITE(btcsignals_tests, BasicTestingSetup) 52 53 /* Callbacks should always be executed in the order in which they were added 54 */ 55 BOOST_AUTO_TEST_CASE(callback_order) 56 { 57 btcsignals::signal<void(int&)> sig0; 58 sig0.connect(IncrementCallback); 59 sig0.connect(SquareCallback); 60 int val{3}; 61 sig0(val); 62 BOOST_CHECK_EQUAL(val, 16); 63 BOOST_CHECK(!sig0.empty()); 64 } 65 66 BOOST_AUTO_TEST_CASE(disconnects) 67 { 68 btcsignals::signal<void(int&)> sig0; 69 auto conn0 = sig0.connect(IncrementCallback); 70 auto conn1 = sig0.connect(SquareCallback); 71 conn1.disconnect(); 72 BOOST_CHECK(!sig0.empty()); 73 int val{3}; 74 sig0(val); 75 BOOST_CHECK_EQUAL(val, 4); 76 77 BOOST_CHECK(!sig0.empty()); 78 conn0.disconnect(); 79 BOOST_CHECK(sig0.empty()); 80 sig0(val); 81 BOOST_CHECK_EQUAL(val, 4); 82 83 conn0 = sig0.connect(IncrementCallback); 84 conn1 = sig0.connect(IncrementCallback); 85 BOOST_CHECK(!sig0.empty()); 86 sig0(val); 87 BOOST_CHECK_EQUAL(val, 6); 88 conn1.disconnect(); 89 90 BOOST_CHECK(conn0.connected()); 91 { 92 btcsignals::scoped_connection scope(conn0); 93 } 94 BOOST_CHECK(!conn0.connected()); 95 BOOST_CHECK(sig0.empty()); 96 sig0(val); 97 BOOST_CHECK_EQUAL(val, 6); 98 } 99 100 /* Check that move-only return types work correctly 101 */ 102 BOOST_AUTO_TEST_CASE(moveonly_return) 103 { 104 btcsignals::signal<MoveOnlyData(int)> sig0; 105 sig0.connect(MoveOnlyReturnCallback); 106 int data{3}; 107 auto ret = sig0(data); 108 BOOST_CHECK_EQUAL(ret->m_data, 3); 109 } 110 111 /* The result of the signal invocation should always be the result of the last 112 * enabled callback. 113 */ 114 BOOST_AUTO_TEST_CASE(return_value) 115 { 116 btcsignals::signal<bool()> sig0; 117 decltype(sig0)::result_type ret; 118 ret = sig0(); 119 BOOST_CHECK(!ret); 120 { 121 btcsignals::scoped_connection conn0 = sig0.connect(ReturnTrue); 122 ret = sig0(); 123 BOOST_CHECK(ret && *ret == true); 124 } 125 ret = sig0(); 126 BOOST_CHECK(!ret); 127 { 128 btcsignals::scoped_connection conn1 = sig0.connect(ReturnTrue); 129 btcsignals::scoped_connection conn0 = sig0.connect(ReturnFalse); 130 ret = sig0(); 131 BOOST_CHECK(ret && *ret == false); 132 conn0.disconnect(); 133 ret = sig0(); 134 BOOST_CHECK(ret && *ret == true); 135 } 136 ret = sig0(); 137 BOOST_CHECK(!ret); 138 } 139 140 /* Test the thread-safety of connect/disconnect/empty/connected/callbacks. 141 * Connect sig0 to an incrementor function and loop in a thread. 142 * Meanwhile, in another thread, inject and call new increment callbacks. 143 * Both threads are constantly calling empty/connected. 144 * Though the end-result is undefined due to a non-deterministic number of 145 * total callbacks executed, this should all be completely threadsafe. 146 * Sanitizers should pick up any buggy data race behavior (if present). 147 */ 148 BOOST_AUTO_TEST_CASE(thread_safety) 149 { 150 btcsignals::signal<void()> sig0; 151 std::atomic<uint32_t> val{0}; 152 auto conn0 = sig0.connect([&val] { 153 val++; 154 }); 155 156 std::thread incrementor([&conn0, &sig0] { 157 for (int i = 0; i < 1000; i++) { 158 sig0(); 159 } 160 // Because these calls are purposely happening on both threads at the 161 // same time, these must be asserts rather than BOOST_CHECKs to prevent 162 // a race inside of BOOST_CHECK itself (writing to the log). 163 assert(!sig0.empty()); 164 assert(conn0.connected()); 165 }); 166 167 std::thread extra_increment_injector([&conn0, &sig0, &val] { 168 static constexpr size_t num_extra_conns{1000}; 169 std::vector<btcsignals::scoped_connection> extra_conns; 170 extra_conns.reserve(num_extra_conns); 171 for (size_t i = 0; i < num_extra_conns; i++) { 172 BOOST_CHECK(!sig0.empty()); 173 BOOST_CHECK(conn0.connected()); 174 extra_conns.emplace_back(sig0.connect([&val] { 175 val++; 176 })); 177 sig0(); 178 } 179 }); 180 incrementor.join(); 181 extra_increment_injector.join(); 182 conn0.disconnect(); 183 BOOST_CHECK(sig0.empty()); 184 185 // sig will have been called 2000 times, and at least 1000 of those will 186 // have been executing multiple incrementing callbacks. So while val is 187 // probably MUCH bigger, it's guaranteed to be at least 3000. 188 BOOST_CHECK_GE(val.load(), 3000); 189 } 190 191 /* Test that connection and disconnection works from within signal 192 * callbacks. 193 */ 194 BOOST_AUTO_TEST_CASE(recursion_safety) 195 { 196 btcsignals::connection conn0, conn1, conn2; 197 btcsignals::signal<void()> sig0; 198 bool nonrecursive_callback_ran{false}; 199 bool recursive_callback_ran{false}; 200 201 conn0 = sig0.connect([&] { 202 BOOST_CHECK(!sig0.empty()); 203 nonrecursive_callback_ran = true; 204 }); 205 BOOST_CHECK(!nonrecursive_callback_ran); 206 sig0(); 207 BOOST_CHECK(nonrecursive_callback_ran); 208 BOOST_CHECK(conn0.connected()); 209 210 nonrecursive_callback_ran = false; 211 conn1 = sig0.connect([&] { 212 nonrecursive_callback_ran = true; 213 conn1.disconnect(); 214 }); 215 BOOST_CHECK(!nonrecursive_callback_ran); 216 BOOST_CHECK(conn0.connected()); 217 BOOST_CHECK(conn1.connected()); 218 sig0(); 219 BOOST_CHECK(nonrecursive_callback_ran); 220 BOOST_CHECK(conn0.connected()); 221 BOOST_CHECK(!conn1.connected()); 222 223 nonrecursive_callback_ran = false; 224 conn1 = sig0.connect([&] { 225 conn2 = sig0.connect([&] { 226 BOOST_CHECK(conn0.connected()); 227 recursive_callback_ran = true; 228 conn0.disconnect(); 229 conn2.disconnect(); 230 }); 231 nonrecursive_callback_ran = true; 232 conn1.disconnect(); 233 }); 234 BOOST_CHECK(!nonrecursive_callback_ran); 235 BOOST_CHECK(!recursive_callback_ran); 236 BOOST_CHECK(conn0.connected()); 237 BOOST_CHECK(conn1.connected()); 238 BOOST_CHECK(!conn2.connected()); 239 sig0(); 240 BOOST_CHECK(nonrecursive_callback_ran); 241 BOOST_CHECK(!recursive_callback_ran); 242 BOOST_CHECK(conn0.connected()); 243 BOOST_CHECK(!conn1.connected()); 244 BOOST_CHECK(conn2.connected()); 245 sig0(); 246 BOOST_CHECK(recursive_callback_ran); 247 BOOST_CHECK(!conn0.connected()); 248 BOOST_CHECK(!conn1.connected()); 249 BOOST_CHECK(!conn2.connected()); 250 } 251 252 /* Test that disconnection from another thread works in real time 253 */ 254 BOOST_AUTO_TEST_CASE(disconnect_thread_safety) 255 { 256 btcsignals::connection conn0, conn1, conn2; 257 btcsignals::signal<void(int&)> sig0; 258 std::binary_semaphore done1{0}; 259 std::binary_semaphore done2{0}; 260 int val{0}; 261 262 conn0 = sig0.connect([&](int&) { 263 conn1.disconnect(); 264 done1.release(); 265 done2.acquire(); 266 }); 267 conn1 = sig0.connect(IncrementCallback); 268 conn2 = sig0.connect(IncrementCallback); 269 std::thread thr([&] { 270 done1.acquire(); 271 conn2.disconnect(); 272 done2.release(); 273 }); 274 sig0(val); 275 thr.join(); 276 BOOST_CHECK_EQUAL(val, 0); 277 } 278 279 280 BOOST_AUTO_TEST_SUITE_END()