#include "catch2/catch.hpp" #include #include using namespace fastsignals; using namespace std::literals; namespace { template class any_of_combiner { public: static_assert(std::is_same_v); using result_type = bool; template void operator()(TRef&& value) { m_result = m_result || bool(value); } result_type get_value() const { return m_result; } private: result_type m_result = {}; }; } // namespace TEST_CASE("Can connect a few slots and emit", "[signal]") { signal valueChanged; int value1 = 0; int value2 = 0; valueChanged.connect([&value1](int value) { value1 = value; }); valueChanged.connect([&value2](int value) { value2 = value; }); REQUIRE(value1 == 0); REQUIRE(value2 == 0); valueChanged(10); REQUIRE(value1 == 10); REQUIRE(value2 == 10); } TEST_CASE("Can safely pass rvalues", "[signal]") { const std::string expected = "If the type T is a reference type, provides the member typedef type which is the type referred to by T. Otherwise type is T."; std::string passedValue = expected; signal valueChanged; std::string value1; std::string value2; valueChanged.connect([&value1](std::string value) { value1 = value; }); valueChanged.connect([&value2](std::string value) { value2 = value; }); valueChanged(std::move(passedValue)); REQUIRE(value1 == expected); REQUIRE(value2 == expected); } TEST_CASE("Can pass mutable ref", "[signal]") { const std::string expected = "If the type T is a reference type, provides the member typedef type which is the type referred to by T. Otherwise type is T."; signal valueChanged; std::string passedValue; valueChanged.connect([expected](std::string& value) { value = expected; }); valueChanged(passedValue); REQUIRE(passedValue == expected); } TEST_CASE("Can disconnect slot with explicit call", "[signal]") { signal valueChanged; int value1 = 0; int value2 = 0; int value3 = 0; auto conn1 = valueChanged.connect([&value1](int value) { value1 = value; }); auto conn2 = valueChanged.connect([&value2](int value) { value2 = value; }); valueChanged.connect([&value3](int value) { value3 = value; }); REQUIRE(value1 == 0); REQUIRE(value2 == 0); REQUIRE(value3 == 0); valueChanged(10); REQUIRE(value1 == 10); REQUIRE(value2 == 10); REQUIRE(value3 == 10); conn2.disconnect(); valueChanged(-99); REQUIRE(value1 == -99); REQUIRE(value2 == 10); REQUIRE(value3 == -99); conn1.disconnect(); valueChanged(17); REQUIRE(value1 == -99); REQUIRE(value2 == 10); REQUIRE(value3 == 17); } TEST_CASE("Can disconnect slot with scoped_connection", "[signal]") { signal valueChanged; int value1 = 0; int value2 = 0; int value3 = 0; { scoped_connection conn1 = valueChanged.connect([&value1](int value) { value1 = value; }); { scoped_connection conn2 = valueChanged.connect([&value2](int value) { value2 = value; }); valueChanged.connect([&value3](int value) { value3 = value; }); REQUIRE(value1 == 0); REQUIRE(value2 == 0); REQUIRE(value3 == 0); valueChanged(10); REQUIRE(value1 == 10); REQUIRE(value2 == 10); REQUIRE(value3 == 10); } // conn2 disconnected. valueChanged(-99); REQUIRE(value1 == -99); REQUIRE(value2 == 10); REQUIRE(value3 == -99); } // conn1 disconnected. valueChanged(17); REQUIRE(value1 == -99); REQUIRE(value2 == 10); REQUIRE(value3 == 17); } TEST_CASE("Can disconnect all", "[signal]") { signal valueChanged; int value1 = 0; int value2 = 0; int value3 = 0; valueChanged.connect([&value1](int value) { value1 = value; }); valueChanged.connect([&value2](int value) { value2 = value; }); valueChanged.connect([&value3](int value) { value3 = value; }); REQUIRE(value1 == 0); REQUIRE(value2 == 0); REQUIRE(value3 == 0); valueChanged(63); REQUIRE(value1 == 63); REQUIRE(value2 == 63); REQUIRE(value3 == 63); valueChanged.disconnect_all_slots(); valueChanged(101); REQUIRE(value1 == 63); REQUIRE(value2 == 63); REQUIRE(value3 == 63); } TEST_CASE("Can disconnect inside slot", "[signal]") { signal valueChanged; int value1 = 0; int value2 = 0; int value3 = 0; connection conn2; valueChanged.connect([&value1](int value) { value1 = value; }); conn2 = valueChanged.connect([&](int value) { value2 = value; conn2.disconnect(); }); valueChanged.connect([&value3](int value) { value3 = value; }); REQUIRE(value1 == 0); REQUIRE(value2 == 0); REQUIRE(value3 == 0); valueChanged(63); REQUIRE(value1 == 63); REQUIRE(value2 == 63); REQUIRE(value3 == 63); valueChanged(101); REQUIRE(value1 == 101); REQUIRE(value2 == 63); // disconnected in slot. REQUIRE(value3 == 101); } TEST_CASE("Disconnects OK if signal dead first", "[signal]") { connection conn2; { scoped_connection conn1; { signal valueChanged; conn2 = valueChanged.connect([](int) { }); // Just unused. valueChanged.connect([](int) { }); conn1 = valueChanged.connect([](int) { }); } REQUIRE(conn2.connected()); REQUIRE(conn1.connected()); conn2.disconnect(); REQUIRE(!conn2.connected()); REQUIRE(conn1.connected()); } conn2.disconnect(); } TEST_CASE("Returns last called slot result with default combiner", "[signal]") { connection conn2; { scoped_connection conn1; { signal absSignal; conn2 = absSignal.connect([](int value) { return value * value; }); conn1 = absSignal.connect([](int value) { return abs(value); }); absSignal(-1); REQUIRE(absSignal(45) == 45); REQUIRE(absSignal(-1) == 1); REQUIRE(absSignal(-177) == 177); REQUIRE(absSignal(0) == 0); } REQUIRE(conn2.connected()); conn2.disconnect(); REQUIRE(!conn2.connected()); } conn2.disconnect(); REQUIRE(!conn2.connected()); } TEST_CASE("Works with custom any_of combiner", "[signal]") { using cancellable_signal = signal; cancellable_signal startRequested; auto conn1 = startRequested.connect([](std::string op) { return op == "1"; }); auto conn2 = startRequested.connect([](std::string op) { return op == "1" || op == "2"; }); REQUIRE(startRequested("0") == false); REQUIRE(startRequested("1") == true); REQUIRE(startRequested("2") == true); REQUIRE(startRequested("3") == false); conn1.disconnect(); conn2.disconnect(); REQUIRE(startRequested("0") == false); REQUIRE(startRequested("1") == false); REQUIRE(startRequested("2") == false); REQUIRE(startRequested("3") == false); } TEST_CASE("Can release scoped connection", "[signal]") { int value2 = 0; int value3 = 0; signal valueChanged; connection conn1; { scoped_connection conn2; scoped_connection conn3; conn2 = valueChanged.connect([&value2](int x) { value2 = x; }); conn3 = valueChanged.connect([&value3](int x) { value3 = x; }); valueChanged(42); REQUIRE(value2 == 42); REQUIRE(value3 == 42); REQUIRE(conn2.connected()); REQUIRE(conn3.connected()); REQUIRE(!conn1.connected()); conn1 = conn3.release(); REQUIRE(conn2.connected()); REQUIRE(!conn3.connected()); REQUIRE(conn1.connected()); valueChanged(144); REQUIRE(value2 == 144); REQUIRE(value3 == 144); } // conn2 disconnected, conn1 connected. valueChanged(17); REQUIRE(value2 == 144); REQUIRE(value3 == 17); REQUIRE(conn1.connected()); conn1.disconnect(); valueChanged(90); REQUIRE(value2 == 144); REQUIRE(value3 == 17); } TEST_CASE("Can use signal with more than one argument", "[signal]") { signal)> event; int value1 = 0; std::string value2; std::vector value3; event.connect([&](int v1, const std::string& v2, const std::vector& v3) { value1 = v1; value2 = v2; value3 = v3; }); event(9815, "using namespace std::literals!"s, std::vector{ "std::vector"s, "using namespace std::literals"s }); REQUIRE(value1 == 9815); REQUIRE(value2 == "using namespace std::literals!"s); REQUIRE(value3 == std::vector{ "std::vector"s, "using namespace std::literals"s }); } TEST_CASE("Can blocks slots using shared_connection_block", "[signal]") { bool callbackShouldBeCalled = true; bool callbackCalled = false; const int value = 123; signal event; auto conn = event.connect([&](int gotValue) { CHECK(gotValue == value); callbackCalled = true; if (!callbackShouldBeCalled) { FAIL("callback is blocked and should not be called"); } }, advanced_tag{}); event(value); REQUIRE(callbackCalled); shared_connection_block block(conn); callbackShouldBeCalled = false; callbackCalled = false; event(value); REQUIRE(!callbackCalled); block.unblock(); callbackShouldBeCalled = true; event(value); REQUIRE(callbackCalled); } TEST_CASE("Other slots are unaffected by the block", "[signal]") { bool callback1Called = false; bool callback2Called = false; const int value = 123; signal event; auto conn1 = event.connect([&](int gotValue) { CHECK(gotValue == value); callback1Called = true; }, advanced_tag{}); auto conn2 = event.connect([&](int) { callback2Called = true; FAIL("callback is blocked and should not be called"); }, advanced_tag{}); shared_connection_block block(conn2); event(value); REQUIRE(callback1Called); REQUIRE(!callback2Called); } TEST_CASE("Multiple blocks block until last one is unblocked", "[signal]") { bool callbackShouldBeCalled = false; bool callbackCalled = false; const int value = 123; signal event; auto conn = event.connect([&](int gotValue) { CHECK(gotValue == value); callbackCalled = true; if (!callbackShouldBeCalled) { FAIL("callback is blocked and should not be called"); } }, advanced_tag{}); shared_connection_block block1(conn); shared_connection_block block2(conn); event(value); REQUIRE(!callbackCalled); block1.unblock(); event(value); REQUIRE(!callbackCalled); block1.block(); block2.unblock(); event(value); REQUIRE(!callbackCalled); block1.unblock(); callbackShouldBeCalled = true; event(value); REQUIRE(callbackCalled); } TEST_CASE("Can copy and move shared_connection_block objects", "[signal]") { bool callbackShouldBeCalled = false; bool callbackCalled = false; const int value = 123; signal event; auto conn = event.connect([&](int gotValue) { CHECK(gotValue == value); callbackCalled = true; if (!callbackShouldBeCalled) { FAIL("callback is blocked and should not be called"); } }, advanced_tag{}); shared_connection_block block1(conn); shared_connection_block block2(block1); event(value); REQUIRE(block1.blocking()); REQUIRE(block2.blocking()); REQUIRE(!callbackCalled); shared_connection_block block3(std::move(block2)); event(value); REQUIRE(block1.blocking()); REQUIRE(!block2.blocking()); REQUIRE(block3.blocking()); REQUIRE(!callbackCalled); block2 = block3; event(value); REQUIRE(block1.blocking()); REQUIRE(block2.blocking()); REQUIRE(block3.blocking()); REQUIRE(!callbackCalled); block3 = std::move(block2); event(value); REQUIRE(block1.blocking()); REQUIRE(!block2.blocking()); REQUIRE(block3.blocking()); REQUIRE(!callbackCalled); block3 = shared_connection_block(conn, false); event(value); REQUIRE(block1.blocking()); REQUIRE(!block2.blocking()); REQUIRE(!block3.blocking()); REQUIRE(!callbackCalled); block1.unblock(); callbackShouldBeCalled = true; event(value); REQUIRE(!block1.blocking()); REQUIRE(!block2.blocking()); REQUIRE(!block3.blocking()); REQUIRE(callbackCalled); } TEST_CASE("Unblocks when shared_connection_block goes out of scope") { bool callbackCalled = false; const int value = 123; signal event; auto conn = event.connect([&](int gotValue) { CHECK(gotValue == value); callbackCalled = true; }, advanced_tag{}); callbackCalled = false; event(value); CHECK(callbackCalled); { callbackCalled = false; shared_connection_block block(conn); event(value); CHECK(!callbackCalled); { callbackCalled = false; shared_connection_block block2(conn); event(value); CHECK(!callbackCalled); } } callbackCalled = false; event(value); CHECK(callbackCalled); } TEST_CASE("Can disconnect advanced slot using advanced_scoped_connection", "[signal]") { signal valueChanged; int value1 = 0; int value2 = 0; int value3 = 0; { advanced_scoped_connection conn1 = valueChanged.connect([&value1](int value) { value1 = value; }, advanced_tag{}); { advanced_scoped_connection conn2 = valueChanged.connect([&value2](int value) { value2 = value; }, advanced_tag{}); valueChanged.connect([&value3](int value) { value3 = value; }); REQUIRE(value1 == 0); REQUIRE(value2 == 0); REQUIRE(value3 == 0); valueChanged(10); REQUIRE(value1 == 10); REQUIRE(value2 == 10); REQUIRE(value3 == 10); } // conn2 disconnected. valueChanged(-99); REQUIRE(value1 == -99); REQUIRE(value2 == 10); REQUIRE(value3 == -99); } // conn1 disconnected. valueChanged(17); REQUIRE(value1 == -99); REQUIRE(value2 == 10); REQUIRE(value3 == 17); } TEST_CASE("Can move signal", "[signal]") { signal src; int srcFireCount = 0; auto srcConn = src.connect([&srcFireCount] { ++srcFireCount; }); src(); REQUIRE(srcFireCount == 1); auto dst = std::move(src); int dstFireCount = 0; auto dstConn = dst.connect([&dstFireCount] { ++dstFireCount; }); dst(); REQUIRE(srcFireCount == 2); REQUIRE(dstFireCount == 1); srcConn.disconnect(); dstConn.disconnect(); dst(); REQUIRE(srcFireCount == 2); REQUIRE(dstFireCount == 1); } TEST_CASE("Can swap signals", "[signal]") { signal s1; signal s2; int s1FireCount = 0; int s2FireCount = 0; s1.connect([&s1FireCount] { ++s1FireCount; }); s2.connect([&s2FireCount] { ++s2FireCount; }); std::swap(s1, s2); s1(); REQUIRE(s1FireCount == 0); REQUIRE(s2FireCount == 1); s2(); REQUIRE(s1FireCount == 1); REQUIRE(s2FireCount == 1); } TEST_CASE("Signal can be destroyed inside its slot and will call the rest of its slots", "[signal]") { std::optional> s; s.emplace(); s->connect([&] { s.reset(); }); bool called = false; s->connect([&] { called = true; }); (*s)(); CHECK(called); } TEST_CASE("Signal can be used as a slot for another signal", "[signal]") { signal s1; bool called = false; s1.connect([&] { called = true; }); signal s2; s2.connect(s1); s2(); CHECK(called); } // memory leak fix TEST_CASE("Releases lambda and its captured const data", "[signal]") { struct Captured { Captured(bool& released) : m_released(released) { } ~Captured() { m_released = true; } private: bool& m_released; }; bool released = false; { const auto captured = std::make_shared(released); signal changeSignal; changeSignal.connect([captured]{}); } CHECK(released); }