From 6fd18236eb3733385d22497cac50acbb32bd3c13 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 11 Oct 2023 17:47:19 -0400 Subject: [PATCH] + unit test --- CMakeLists.txt | 1 + utest/CMakeLists.txt | 35 +++++ utest/PollingReactor.test.cpp | 233 ++++++++++++++++++++++++++++++++++ utest/Sink.test.cpp | 100 +++++++++++++++ utest/reactor_utest_main.cpp | 6 + 5 files changed, 375 insertions(+) create mode 100644 utest/CMakeLists.txt create mode 100644 utest/PollingReactor.test.cpp create mode 100644 utest/Sink.test.cpp create mode 100644 utest/reactor_utest_main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9bbe8050..5ddd7483 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ xo_toplevel_compile_options() # ---------------------------------------------------------------- add_subdirectory(src/reactor) +add_subdirectory(utest) # ---------------------------------------------------------------- # provide find_pacakge() support for reactor customers diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt new file mode 100644 index 00000000..1ab4b440 --- /dev/null +++ b/utest/CMakeLists.txt @@ -0,0 +1,35 @@ +# build unittest reactor/unittest' + +set(SELF_EXE utest.reactor) +set(SELF_SRCS Sink.test.cpp PollingReactor.test.cpp reactor_utest_main.cpp) + +add_executable(${SELF_EXE} ${SELF_SRCS}) +xo_include_options2(${SELF_EXE}) + +add_test(NAME ${SELF_EXE} COMMAND ${SELF_EXE}) +target_code_coverage(${SELF_EXE} AUTO ALL) + +# ---------------------------------------------------------------- +# internal dependency (on this codebase) + +xo_self_dependency(${SELF_EXE} reactor) + +# ---------------------------------------------------------------- +# external dependencies + +xo_external_target_dependency(${SELF_EXE} Catch2 Catch2::Catch2) + +# should be getting this via xo_include_options2() + +## ---------------------------------------------------------------- +## make standard directories for std:: includes explicit +## so that +## (1) they appear in compile_commands.json. +## (2) clangd (run from emacs lsp-mode) can find them +## +#if(CMAKE_EXPORT_COMPILE_COMMANDS) +# set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES +# ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES}) +#endif() + +# end CMakeLists.txt diff --git a/utest/PollingReactor.test.cpp b/utest/PollingReactor.test.cpp new file mode 100644 index 00000000..39845264 --- /dev/null +++ b/utest/PollingReactor.test.cpp @@ -0,0 +1,233 @@ +/* @file PollingReactor.test.cpp */ + +#include "xo/reactor/PollingReactor.hpp" +#include "xo/reactor/FifoQueue.hpp" +#include "xo/reactor/Sink.hpp" +#include "xo/randomgen/xoshiro256.hpp" +#include "xo/indentlog/print/pair.hpp" +#include "catch2/catch.hpp" + +namespace xo { + //using xo::reactor::Reactor; + using xo::reactor::PollingReactor; + using xo::reactor::FifoQueue; + using xo::reactor::SinkToFunction; + using xo::ref::rp; + using xo::time::timeutil; + using xo::time::seconds; + using xo::time::utc_nanos; + +/* note: trivial REQUIRE() call in else branch bc we still want + * catch2 to count assertions when verification succeeds + */ +# define REQUIRE_ORCAPTURE(ok_flag, catch_flag, expr) \ + if (catch_flag) { \ + REQUIRE((expr)); \ + } else { \ + REQUIRE(true); \ + ok_flag &= (expr); \ + } + +# define REQUIRE_ORFAIL(ok_flag, catch_flag, expr) \ + REQUIRE_ORCAPTURE(ok_flag, catch_flag, expr); \ + if (!ok_flag) \ + return ok_flag + + namespace { + using TestEvent = std::pair; + using TestQueue = FifoQueue; + + struct RandomTestData { + RandomTestData(std::size_t n, + xo::rng::xoshiro256ss * p_rgen); + + std::uint32_t size() const { return u1v_.size(); } + std::vector const & u1v() const { return u1v_; } + + private: + /* a set of n randomly chosen elements drawn from [0 .. 2n-1] */ + std::vector u1v_; + }; + + RandomTestData::RandomTestData(std::size_t n, + xo::rng::xoshiro256ss * p_rgen) + : u1v_(n) + { + std::shuffle(u1v_.begin(), u1v_.end(), *p_rgen); + } + } /*namespace*/ + + namespace ut { + TEST_CASE("polling0", "[reactor]") { + rp reactor = PollingReactor::make(); + + REQUIRE(reactor.get()); + + for (std::uint32_t i=0; i<3; ++i) { + INFO(xtag("i", i)); + REQUIRE(reactor->run_one() == 0); + } + } /*TEST_CASE(polling0)*/ + + /* return true=success, false=fail */ + bool + run_polling1_test(std::size_t n, + bool catch_flag, + xo::rng::xoshiro256ss * p_rgen) + { + scope log(XO_DEBUG(catch_flag)); + log && log(xtag("n", n)); + + bool ok_flag = true; + + rp reactor = PollingReactor::make(); + REQUIRE_ORFAIL(ok_flag, catch_flag, reactor.get() != nullptr); + + if (ok_flag) + reactor->set_loglevel(catch_flag + ? log_level::always + : log_level::error); + + rp q = TestQueue::make(); + REQUIRE_ORFAIL(ok_flag, catch_flag, q.get() != nullptr); + + if (ok_flag) + q->set_name("fifo"); + + /* capture delivered events */ + std::vector out_ev_v; + + auto sink_fn + = ([&out_ev_v](TestEvent const & x) { out_ev_v.push_back(x); }); + + q->add_callback(new SinkToFunction + >(sink_fn)); + + + reactor->add_source(q); + + /* max #of consecutive inserts */ + std::size_t max_enq = std::max(1UL, n/3); + /* max #of consecutive removes */ + std::size_t max_deq = std::max(1UL, n/3); + + RandomTestData seq(n, p_rgen); + + q->set_debug_sim_flag(catch_flag); + + /* verify: + * 1. queue conservation -- everything inserted gets delivered + * 2. events consumed in the same order they where inserted + * 3. no problem with queue being sometimes empty + */ + + utc_nanos t0 = timeutil::ymd_hms(20231011 /*ymd*/, 131300 /*hms*/); + + /* count #of events delivered by reactor */ + std::size_t n_delivered = 0; + + std::size_t i = 0; + while ((i < seq.u1v().size()) || (n_delivered < n)) { + /* sum of (#of enq, #of deq) attempted for this iteration */ + std::size_t n_work_attempted = 0; + /* sum of (#of enq, #of deq) accomplished for this iteration */ + std::size_t n_work_done = 0; + std::size_t n_enq = p_rgen->generate() % (max_enq + 1); + std::size_t n_deq_attempted = 1 + (p_rgen->generate() % (max_deq + 1)); + std::size_t n_deq_done = 0; + + /* pick random #of elements to insert (to back of queue) */ + { + for (std::size_t j = 0; (j < n_enq) && (i < seq.u1v().size()); ++j) { + utc_nanos ti = t0 + seconds(i); + + q->notify_ev(std::make_pair(ti, seq.u1v()[i++])); + } + + n_work_attempted += n_enq; + n_work_done += n_enq; + } + + /* pick random #of elements to remove (from front of queue) */ + { + + + for (std::size_t j = 0; j < n_deq_attempted; ++j) + n_deq_done += reactor->run_one(); + + n_work_attempted += n_deq_attempted; + n_work_done += n_deq_done; + n_delivered += n_deq_done; + } + + log && log(xtag("i", i), + xtag("n", n), + xtag("n_work_attempted", n_work_attempted), + xtag("n_work_done", n_work_done), + xtag("n_enq", n_enq), + xtag("n_deq_attempted", n_deq_attempted), + xtag("n_deq_done", n_deq_done)); + + if ((i == seq.u1v().size()) /*no more enqueues planned*/ + && (n_work_attempted > 0) + && (n_work_done == 0)) + { + /* expect incremental progress every iteration; + * want unit test to always terminate + */ + break; + } + } + + REQUIRE_ORFAIL(ok_flag, catch_flag, i == n); + REQUIRE_ORFAIL(ok_flag, catch_flag, n_delivered == n); + + /* check events delivered 1:1 and in order */ + for (std::size_t i=0; i seed; + auto rgen = xo::rng::xoshiro256ss(seed); + + for (std::size_t n = 4; n <= 1024; n *= 2) { + bool ok_flag = false; + + for (std::uint32_t attention = 0; !ok_flag && (attention < 2); ++attention) { + ok_flag = true; + + /* attention=0: + * - no logging + * - detect assertion failures, but don't report them to catch + * attention=1: + * - only runs if failure detected with attention=0 + * - full logging + * - report to catch + */ + + bool debug_flag = (attention == 1); + + ok_flag &= run_polling1_test(n, debug_flag, &rgen); + } + } + + } /*TEST_CASE(polling1)*/ + } /*namespace ut*/ + +} /*namespace xo*/ + + +/* end PollingReactor.test.cpp */ diff --git a/utest/Sink.test.cpp b/utest/Sink.test.cpp new file mode 100644 index 00000000..160530a0 --- /dev/null +++ b/utest/Sink.test.cpp @@ -0,0 +1,100 @@ +/* @file Sink.test.cpp */ + +#include "xo/reactor/PollingReactor.hpp" +#include "xo/reactor/Sink.hpp" +#include "xo/indentlog/print/pair.hpp" +#include "catch2/catch.hpp" + +namespace xo { + using xo::reactor::Reactor; + using xo::reactor::PollingReactor; + using xo::reactor::AbstractSink; + using xo::reactor::Sink1; + using xo::reactor::SinkEndpoint; + using xo::reactor::SinkToConsole; + using xo::time::utc_nanos; + using xo::ref::rp; + + namespace { + class TestSink : public SinkEndpoint { + public: + TestSink() = default; + + virtual uint32_t n_in_ev() const override { return 0; } + virtual bool allow_volatile_source() const override { return true; } + virtual void notify_ev(int const & ev) override {} + virtual void display(std::ostream & os) const override { os << ""; } + }; /*TestSink*/ + + class TestSink2 : public SinkEndpoint { + public: + TestSink2() = default; + + virtual uint32_t n_in_ev() const override { return 0; } + virtual bool allow_volatile_source() const override { return true; } + virtual void notify_ev(utc_nanos const & ev) override {} + virtual void display(std::ostream & os) const override { os << ""; } + }; /*TestSink2*/ + + using TestSink3 = SinkToConsole>; + } /*namespace*/ + + namespace ut { + TEST_CASE("sink-cast", "[reactor][sink]") { + rp test_sink = new TestSink(); + rp sink = test_sink; + + TestSink * cast_sink = dynamic_cast(sink.get()); + + REQUIRE(test_sink.get() == cast_sink); + + Sink1 * int_sink = dynamic_cast *>(sink.get()); + + REQUIRE(test_sink.get() == int_sink); + + rp> int_sink2 + = Sink1::require_native("TEST_CASE(sink-cast)", sink.get()); + + REQUIRE(test_sink.get() == int_sink2.get()); + } /*TEST_CASE(sink-cast)*/ + + TEST_CASE("sink-cast2", "[reactor]") { + rp test_sink = new TestSink2(); + rp sink = test_sink; + + TestSink2 * cast_sink = dynamic_cast(sink.get()); + + REQUIRE(test_sink.get() == cast_sink); + + Sink1 * dt_sink = dynamic_cast *>(sink.get()); + + REQUIRE(test_sink.get() == dt_sink); + + rp> dt_sink2 + = Sink1::require_native("TEST_CASE(sink-cast2)", sink.get()); + + REQUIRE(test_sink.get() == dt_sink2.get()); + } /*TEST_CASE(sink-cast2)*/ + + TEST_CASE("sink-cast3", "[reactor]") { + rp test_sink = new TestSink3(); + rp sink = test_sink; + + TestSink3 * cast_sink = dynamic_cast(sink.get()); + + REQUIRE(test_sink.get() == cast_sink); + + Sink1> * ev_sink + = dynamic_cast> *>(sink.get()); + + REQUIRE(test_sink.get() == ev_sink); + + rp>> ev_sink2 + = Sink1>::require_native("TEST_CASE(sink-cast3)", sink.get()); + + REQUIRE(test_sink.get() == ev_sink2.get()); + } /*TEST_CASE(sink-cast3)*/ + } /*namespace ut*/ +} /*namespace xo*/ + +/* end Sink.test.cpp */ diff --git a/utest/reactor_utest_main.cpp b/utest/reactor_utest_main.cpp new file mode 100644 index 00000000..c3b80295 --- /dev/null +++ b/utest/reactor_utest_main.cpp @@ -0,0 +1,6 @@ +/* @file reactor_utest_main.cpp */ + +#define CATCH_CONFIG_MAIN +#include "catch2/catch.hpp" + +/* end reactor_utest_main.cpp */