diff --git a/src/core/view.cc b/src/core/view.cc index a67d4c6c..725d4148 100644 --- a/src/core/view.cc +++ b/src/core/view.cc @@ -301,7 +301,7 @@ View::filter() { std::for_each(changed.begin(), splitChanged, [this](const auto& d) { rpc::call_object_d_nothrow(m_event_removed, d.get()); }); if (!m_event_added.is_empty()) - std::for_each(changed.begin(), splitChanged, [this](const auto& d) { rpc::call_object_d_nothrow(m_event_added, d.get()); }); + std::for_each(splitChanged, changed.end(), [this](const auto& d) { rpc::call_object_d_nothrow(m_event_added, d.get()); }); emit_changed(); } diff --git a/test/Makefile.am b/test/Makefile.am index 5432dcb5..4c0879b9 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -1,4 +1,5 @@ TESTS = \ + rtorrent_Test_Core \ rtorrent_Test_Rpc \ rtorrent_Test_Src @@ -7,6 +8,7 @@ check_PROGRAMS = $(TESTS) rtorrent_Test_LDADD = \ ../src/libsub_root.a +rtorrent_Test_Core_LDADD = $(rtorrent_Test_LDADD) rtorrent_Test_Rpc_LDADD = $(rtorrent_Test_LDADD) rtorrent_Test_Src_LDADD = $(rtorrent_Test_LDADD) @@ -30,6 +32,10 @@ rtorrent_Test_Common = \ helpers/test_utils.h \ helpers/utils.h +rtorrent_Test_Core_SOURCES = $(rtorrent_Test_Common) \ + core/test_view.cc \ + core/test_view.h + rtorrent_Test_Rpc_SOURCES = $(rtorrent_Test_Common) \ rpc/test_command.cc \ rpc/test_command.h \ @@ -62,6 +68,8 @@ rtorrent_Test_Src_SOURCES = $(rtorrent_Test_Common) \ src/test_watch_ready_queue.cc \ src/test_watch_ready_queue.h +rtorrent_Test_Core_CXXFLAGS = $(CPPUNIT_CFLAGS) +rtorrent_Test_Core_LDFLAGS = $(CPPUNIT_LIBS) -ldl rtorrent_Test_Rpc_CXXFLAGS = $(CPPUNIT_CFLAGS) rtorrent_Test_Rpc_LDFLAGS = $(CPPUNIT_LIBS) -ldl rtorrent_Test_Src_CXXFLAGS = $(CPPUNIT_CFLAGS) diff --git a/test/core/test_view.cc b/test/core/test_view.cc new file mode 100644 index 00000000..432f7aed --- /dev/null +++ b/test/core/test_view.cc @@ -0,0 +1,158 @@ +#include "config.h" + +#include "test/core/test_view.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "command_helpers.h" +#include "control.h" +#include "core/download.h" +#include "core/view.h" +#include "globals.h" +#include "rpc/parse_commands.h" + +CPPUNIT_TEST_SUITE_REGISTRATION(TestView); + +namespace { + +const char* filter_command = "test.view.filter"; +const char* added_command = "test.view.event_added"; +const char* removed_command = "test.view.event_removed"; + +// View only stores these and hands them to the command layer as RPC targets, +// which casts the pointer without reading through it. +struct download_stub { + alignas(core::Download) std::byte storage[sizeof(core::Download)]; +}; + +std::deque stub_storage; +std::vector stub_downloads; + +std::set matches_filter; +std::vector added_events; +std::vector removed_events; + +std::shared_ptr +make_stub_download() { + auto download = reinterpret_cast(&stub_storage.emplace_back()); + + stub_downloads.push_back(download); + + return std::shared_ptr(download, [](core::Download*) {}); +} + +// Renders a dispatch list as "d0,d2" so a failure names the downloads the +// handler saw, not just how many there were. +std::string +describe(const std::vector& downloads) { + std::string result; + + for (const auto& download : downloads) { + auto itr = std::find(stub_downloads.begin(), stub_downloads.end(), download); + + if (!result.empty()) + result += ','; + + result += 'd' + std::to_string(std::distance(stub_downloads.begin(), itr)); + } + + return result; +} + +torrent::Object +cmd_filter(core::Download* download, const torrent::Object&) { + return torrent::Object(static_cast(matches_filter.count(download))); +} + +torrent::Object +cmd_event_added(core::Download* download, const torrent::Object&) { + added_events.push_back(download); + return torrent::Object(); +} + +torrent::Object +cmd_event_removed(core::Download* download, const torrent::Object&) { + removed_events.push_back(download); + return torrent::Object(); +} + +torrent::Object +command_object(const char* command) { + return torrent::Object(std::string(command) + "="); +} + +} // namespace + +void +TestView::setUp() { + TestFixtureWithMainThread::setUp(); + + if (control == nullptr) + control = new Control; + + if (!rpc::commands.has(filter_command)) { + CMD2_DL(filter_command, &cmd_filter); + CMD2_DL(added_command, &cmd_event_added); + CMD2_DL(removed_command, &cmd_event_removed); + } + + stub_storage.clear(); + stub_downloads.clear(); + matches_filter.clear(); + added_events.clear(); + removed_events.clear(); +} + +void +TestView::tearDown() { + TestFixtureWithMainThread::tearDown(); +} + +// d0 and d1 start visible and stop matching, d2 and d3 start filtered out and +// start matching. The two halves are the same size on purpose: a test that +// only counted the dispatches would pass even if both events were sent over +// the same half of the changed range. +void +TestView::test_filter_dispatches_events_to_the_right_downloads() { + core::View view; + view.initialize("test_view"); + + auto d0 = make_stub_download(); + auto d1 = make_stub_download(); + auto d2 = make_stub_download(); + auto d3 = make_stub_download(); + + view.insert(d0); + view.insert(d1); + view.insert(d2); + view.insert(d3); + + view.set_visible(d0.get()); + view.set_visible(d1.get()); + + CPPUNIT_ASSERT_EQUAL(core::View::size_type(2), view.size_visible()); + + matches_filter = {d2.get(), d3.get()}; + + view.set_filter(command_object(filter_command)); + view.set_event_added(command_object(added_command)); + view.set_event_removed(command_object(removed_command)); + + added_events.clear(); + removed_events.clear(); + + view.filter(); + + CPPUNIT_ASSERT_EQUAL(std::string("d2,d3"), describe(added_events)); + CPPUNIT_ASSERT_EQUAL(std::string("d0,d1"), describe(removed_events)); + + CPPUNIT_ASSERT_EQUAL(core::View::size_type(2), view.size_visible()); + CPPUNIT_ASSERT(*view.begin_visible() == d2); + CPPUNIT_ASSERT(*(view.begin_visible() + 1) == d3); +} diff --git a/test/core/test_view.h b/test/core/test_view.h new file mode 100644 index 00000000..8e84b19e --- /dev/null +++ b/test/core/test_view.h @@ -0,0 +1,15 @@ +#include "test/helpers/test_main_thread.h" + +class TestView : public TestFixtureWithMainThread { + CPPUNIT_TEST_SUITE(TestView); + + CPPUNIT_TEST(test_filter_dispatches_events_to_the_right_downloads); + + CPPUNIT_TEST_SUITE_END(); + +public: + void setUp(); + void tearDown(); + + void test_filter_dispatches_events_to_the_right_downloads(); +};