From 122ff6d7db69c5a0b72dee4c8e7e36eb36468f52 Mon Sep 17 00:00:00 2001 From: John Paul Mattia Date: Wed, 1 Jul 2026 06:33:36 -0700 Subject: [PATCH] Add a multithreaded test for IPC over sockets Add a test exercising the IPC-over-sockets implementation from a single thread and from multiple threads concurrently (Execute, Request, Poke, Advise, combined Advise+Request, and concurrent main-thread and worker-thread Request()s). Each test starts its own server by re-executing the test program with WX_IPC_TEST_SERVER set and shuts it down again in the fixture teardown, so no server process outlives a test (or disturbs the unrelated GUI tests in test_gui). The client runs in the main Catch2 process and queries the server for state to verify it (Catch2 macros cannot run in the server process). The wait loops are wall-clock bounded so they behave under a GUI event loop, and a per-fixture watchdog aborts with a diagnostic if a test ever hangs rather than letting CI time out. The test runs in both the console "test" and the GUI "test_gui" programs. It is excluded from one configuration: wxQt, whose event loop does not reliably process a cross-thread CallAfter() (a wxQt bug fixed separately). --- build/cmake/tests/base/CMakeLists.txt | 2 + tests/Makefile.in | 19 +- tests/descrip.mms | 8 +- tests/makefile.gcc | 18 +- tests/makefile.vc | 17 +- tests/net/ipc.cpp | 1078 +++++++++++++++++++++---- tests/net/ipc_setup_test.h | 33 + tests/net/ipc_test_server.cpp | 717 ++++++++++++++++ tests/net/ipc_test_server.h | 62 ++ tests/test.bkl | 13 +- tests/test.cpp | 36 + tests/test.vcxproj | 5 +- tests/test.vcxproj.filters | 3 + tests/test_gui.vcxproj | 6 +- tests/test_gui.vcxproj.filters | 6 + 15 files changed, 1852 insertions(+), 171 deletions(-) create mode 100644 tests/net/ipc_setup_test.h create mode 100644 tests/net/ipc_test_server.cpp create mode 100644 tests/net/ipc_test_server.h diff --git a/build/cmake/tests/base/CMakeLists.txt b/build/cmake/tests/base/CMakeLists.txt index 65f050553f..e5980479ed 100644 --- a/build/cmake/tests/base/CMakeLists.txt +++ b/build/cmake/tests/base/CMakeLists.txt @@ -52,6 +52,7 @@ set(TEST_SRC misc/pathlist.cpp misc/typeinfotest.cpp net/ipc.cpp + net/ipc_test_server.cpp net/socket.cpp net/webrequest.cpp regex/regextest.cpp @@ -124,6 +125,7 @@ set(TEST_DATA wx_add_test(test_base CONSOLE ${TEST_SRC} DATA ${TEST_DATA} ) +target_compile_definitions(test_base PRIVATE TEST_HAS_IPC_SERVER) if(wxUSE_SOCKETS) wx_exe_link_libraries(test_base wxnet) endif() diff --git a/tests/Makefile.in b/tests/Makefile.in index 84c0d02a24..ae4b8952b8 100644 --- a/tests/Makefile.in +++ b/tests/Makefile.in @@ -102,6 +102,7 @@ TEST_OBJECTS = \ test_pathlist.o \ test_typeinfotest.o \ test_ipc.o \ + test_ipc_test_server.o \ test_socket.o \ test_webrequest.o \ test_regextest.o \ @@ -162,7 +163,7 @@ TEST_GUI_CXXFLAGS = $(__test_gui_PCH_INC) $(WX_CPPFLAGS) -D__WX$(TOOLKIT)__ \ $(__WXUNIV_DEFINE_p) $(__DEBUG_DEFINE_p) $(__EXCEPTIONS_DEFINE_p) \ $(__RTTI_DEFINE_p) $(__THREAD_DEFINE_p) -I$(srcdir) $(__DLLFLAG_p) \ -I$(srcdir)/../samples $(WX_CXXFLAGS) $(SAMPLES_CXXFLAGS) $(CATCH2_CFLAGS) \ - $(CPPFLAGS) $(CXXFLAGS) + -DTEST_HAS_IPC_SERVER $(CPPFLAGS) $(CXXFLAGS) TEST_GUI_OBJECTS = \ $(__test_gui___win32rc) \ test_gui_asserthelper.o \ @@ -265,6 +266,8 @@ TEST_GUI_OBJECTS = \ test_gui_settings.o \ test_gui_textwrap.o \ test_gui_socket.o \ + test_gui_ipc.o \ + test_gui_ipc_test_server.o \ test_gui_tlw.o \ test_gui_dataview.o \ test_gui_rowheightcachetest.o \ @@ -292,6 +295,7 @@ TEST_ALLHEADERS_OBJECTS = \ TEST_ALLHEADERS_ODEP = \ $(_____pch_testprec_test_allheaders_testprec_h_gch___depname) + ### Conditionally set variables: ### @COND_DEPS_TRACKING_0@CXXC = $(CXX) @@ -562,7 +566,7 @@ test$(EXEEXT): $(TEST_OBJECTS) $(__test___win32rc) @COND_USE_PCH_1@./.pch/testprec_test_allheaders/testprec.h.gch: @COND_USE_PCH_1@ $(BK_MAKE_PCH) ./.pch/testprec_test_allheaders/testprec.h.gch testprec.h $(CXX) $(TEST_ALLHEADERS_CXXFLAGS) -data: +data: @mkdir -p . @for f in testdata.conf horse.svg; do \ if test ! -f ./$$f -a ! -d ./$$f ; \ @@ -650,7 +654,7 @@ test_test_rc.o: $(srcdir)/../tests/test.rc $(TEST_ODEP) $(WINDRES) -i$< -o$@ --define __WX$(TOOLKIT)__ $(__WXUNIV_DEFINE_p_9) $(__DEBUG_DEFINE_p_9) $(__EXCEPTIONS_DEFINE_p_9) $(__RTTI_DEFINE_p_9) $(__THREAD_DEFINE_p_9) --include-dir $(srcdir) $(__DLLFLAG_p_9) --define wxUSE_GUI=0 --include-dir $(top_srcdir)/samples $(__RCDEFDIR_p) --include-dir $(top_srcdir)/include test_test.o: $(srcdir)/test.cpp $(TEST_ODEP) - $(CXXC) -c -o $@ $(TEST_CXXFLAGS) $(srcdir)/test.cpp + $(CXXC) -c -o $@ $(TEST_CXXFLAGS) -DTEST_HAS_IPC_SERVER $(srcdir)/test.cpp test_anytest.o: $(srcdir)/any/anytest.cpp $(TEST_ODEP) $(CXXC) -c -o $@ $(TEST_CXXFLAGS) $(srcdir)/any/anytest.cpp @@ -781,6 +785,9 @@ test_typeinfotest.o: $(srcdir)/misc/typeinfotest.cpp $(TEST_ODEP) test_ipc.o: $(srcdir)/net/ipc.cpp $(TEST_ODEP) $(CXXC) -c -o $@ $(TEST_CXXFLAGS) $(srcdir)/net/ipc.cpp +test_ipc_test_server.o: $(srcdir)/net/ipc_test_server.cpp $(TEST_ODEP) + $(CXXC) -c -o $@ $(TEST_CXXFLAGS) $(srcdir)/net/ipc_test_server.cpp + test_socket.o: $(srcdir)/net/socket.cpp $(TEST_ODEP) $(CXXC) -c -o $@ $(TEST_CXXFLAGS) $(srcdir)/net/socket.cpp @@ -1225,6 +1232,12 @@ test_gui_textwrap.o: $(srcdir)/misc/textwrap.cpp $(TEST_GUI_ODEP) test_gui_socket.o: $(srcdir)/net/socket.cpp $(TEST_GUI_ODEP) $(CXXC) -c -o $@ $(TEST_GUI_CXXFLAGS) $(srcdir)/net/socket.cpp +test_gui_ipc.o: $(srcdir)/net/ipc.cpp $(TEST_GUI_ODEP) + $(CXXC) -c -o $@ $(TEST_GUI_CXXFLAGS) $(srcdir)/net/ipc.cpp + +test_gui_ipc_test_server.o: $(srcdir)/net/ipc_test_server.cpp $(TEST_GUI_ODEP) + $(CXXC) -c -o $@ $(TEST_GUI_CXXFLAGS) $(srcdir)/net/ipc_test_server.cpp + test_gui_tlw.o: $(srcdir)/persistence/tlw.cpp $(TEST_GUI_ODEP) $(CXXC) -c -o $@ $(TEST_GUI_CXXFLAGS) $(srcdir)/persistence/tlw.cpp diff --git a/tests/descrip.mms b/tests/descrip.mms index ddbc5bd3a0..a5c5de8652 100644 --- a/tests/descrip.mms +++ b/tests/descrip.mms @@ -97,6 +97,7 @@ TEST_OBJECTS = \ test_typeinfotest.obj TEST_OBJECTS1=test_ipc.obj,\ + test_ipc_test_server.obj,\ test_socket.obj,\ test_regextest.obj,\ test_wxregextest.obj,\ @@ -374,10 +375,13 @@ test_pathlist.obj : [.misc]pathlist.cpp test_typeinfotest.obj : [.misc]typeinfotest.cpp $(CXXC) /object=[]$@ $(TEST_CXXFLAGS) [.misc]typeinfotest.cpp -test_ipc.obj : [.net]ipc.cpp +test_ipc.obj : [.net]ipc.cpp $(CXXC) /object=[]$@ $(TEST_CXXFLAGS) [.net]ipc.cpp -test_socket.obj : [.net]socket.cpp +test_ipc_test_server.obj : [.net]ipc_test_server.cpp + $(CXXC) /object=[]$@ $(TEST_CXXFLAGS) [.net]ipc_test_server.cpp + +test_socket.obj : [.net]socket.cpp $(CXXC) /object=[]$@ $(TEST_CXXFLAGS)/warn=(disable=REFTEMPORARY)\ [.net]socket.cpp diff --git a/tests/makefile.gcc b/tests/makefile.gcc index 49b4ddc6a9..a62f15ea41 100644 --- a/tests/makefile.gcc +++ b/tests/makefile.gcc @@ -74,6 +74,7 @@ TEST_OBJECTS = \ $(OBJS)\test_pathlist.o \ $(OBJS)\test_typeinfotest.o \ $(OBJS)\test_ipc.o \ + $(OBJS)\test_ipc_test_server.o \ $(OBJS)\test_socket.o \ $(OBJS)\test_webrequest.o \ $(OBJS)\test_regextest.o \ @@ -135,8 +136,8 @@ TEST_GUI_CXXFLAGS = $(__DEBUGINFO) $(__OPTIMIZEFLAG) $(__THREADSFLAG) \ $(__EXCEPTIONS_DEFINE_p) $(__RTTI_DEFINE_p) $(__THREAD_DEFINE_p) \ -I$(SETUPHDIR) -I.\..\include $(____CAIRO_INCLUDEDIR_FILENAMES) -W -Wall -I. \ $(__DLLFLAG_p) -I.\..\samples -DNOPCH -I.\..\3rdparty\catch\single_include \ - $(__RTTIFLAG) $(__EXCEPTIONSFLAG) -Wno-ctor-dtor-privacy $(CPPFLAGS) \ - $(CXXFLAGS) + $(__RTTIFLAG) $(__EXCEPTIONSFLAG) -Wno-ctor-dtor-privacy \ + -DTEST_HAS_IPC_SERVER $(CPPFLAGS) $(CXXFLAGS) TEST_GUI_OBJECTS = \ $(OBJS)\test_gui_test_rc.o \ $(OBJS)\test_gui_dummy.o \ @@ -240,6 +241,8 @@ TEST_GUI_OBJECTS = \ $(OBJS)\test_gui_settings.o \ $(OBJS)\test_gui_textwrap.o \ $(OBJS)\test_gui_socket.o \ + $(OBJS)\test_gui_ipc.o \ + $(OBJS)\test_gui_ipc_test_server.o \ $(OBJS)\test_gui_tlw.o \ $(OBJS)\test_gui_dataview.o \ $(OBJS)\test_gui_rowheightcachetest.o \ @@ -585,7 +588,7 @@ $(OBJS)\test_dummy.o: ./dummy.cpp $(CXX) -c -o $@ $(TEST_CXXFLAGS) $(CPPDEPS) $< $(OBJS)\test_test.o: ./test.cpp - $(CXX) -c -o $@ $(TEST_CXXFLAGS) $(CPPDEPS) $< + $(CXX) -c -o $@ $(TEST_CXXFLAGS) -DTEST_HAS_IPC_SERVER $(CPPDEPS) $< $(OBJS)\test_anytest.o: ./any/anytest.cpp $(CXX) -c -o $@ $(TEST_CXXFLAGS) $(CPPDEPS) $< @@ -716,6 +719,9 @@ $(OBJS)\test_typeinfotest.o: ./misc/typeinfotest.cpp $(OBJS)\test_ipc.o: ./net/ipc.cpp $(CXX) -c -o $@ $(TEST_CXXFLAGS) $(CPPDEPS) $< +$(OBJS)\test_ipc_test_server.o: ./net/ipc_test_server.cpp + $(CXX) -c -o $@ $(TEST_CXXFLAGS) $(CPPDEPS) $< + $(OBJS)\test_socket.o: ./net/socket.cpp $(CXX) -c -o $@ $(TEST_CXXFLAGS) $(CPPDEPS) $< @@ -1166,6 +1172,12 @@ $(OBJS)\test_gui_textwrap.o: ./misc/textwrap.cpp $(OBJS)\test_gui_socket.o: ./net/socket.cpp $(CXX) -c -o $@ $(TEST_GUI_CXXFLAGS) $(CPPDEPS) $< +$(OBJS)\test_gui_ipc.o: ./net/ipc.cpp + $(CXX) -c -o $@ $(TEST_GUI_CXXFLAGS) $(CPPDEPS) $< + +$(OBJS)\test_gui_ipc_test_server.o: ./net/ipc_test_server.cpp + $(CXX) -c -o $@ $(TEST_GUI_CXXFLAGS) $(CPPDEPS) $< + $(OBJS)\test_gui_tlw.o: ./persistence/tlw.cpp $(CXX) -c -o $@ $(TEST_GUI_CXXFLAGS) $(CPPDEPS) $< diff --git a/tests/makefile.vc b/tests/makefile.vc index 155aec7fd2..874807bc7d 100644 --- a/tests/makefile.vc +++ b/tests/makefile.vc @@ -75,6 +75,7 @@ TEST_OBJECTS = \ $(OBJS)\test_pathlist.obj \ $(OBJS)\test_typeinfotest.obj \ $(OBJS)\test_ipc.obj \ + $(OBJS)\test_ipc_test_server.obj \ $(OBJS)\test_socket.obj \ $(OBJS)\test_webrequest.obj \ $(OBJS)\test_regextest.obj \ @@ -149,7 +150,8 @@ TEST_GUI_CXXFLAGS = /M$(__RUNTIME_LIBS_44)$(__DEBUGRUNTIME) /DWIN32 \ /I$(SETUPHDIR) /I.\..\include $(____CAIRO_INCLUDEDIR_FILENAMES) /W4 /I. \ $(__DLLFLAG_p) /I.\..\samples /DNOPCH /I.\..\3rdparty\catch\single_include \ /D_CONSOLE $(__RTTIFLAG) $(__EXCEPTIONSFLAG) /Yu"testprec.h" \ - /Fp"$(OBJS)\testprec_test_gui.pch" $(CPPFLAGS) $(CXXFLAGS) + /Fp"$(OBJS)\testprec_test_gui.pch" -DTEST_HAS_IPC_SERVER $(CPPFLAGS) \ + $(CXXFLAGS) TEST_GUI_OBJECTS = \ $(OBJS)\test_gui_dummy.obj \ $(OBJS)\test_gui_asserthelper.obj \ @@ -252,6 +254,8 @@ TEST_GUI_OBJECTS = \ $(OBJS)\test_gui_settings.obj \ $(OBJS)\test_gui_textwrap.obj \ $(OBJS)\test_gui_socket.obj \ + $(OBJS)\test_gui_ipc.obj \ + $(OBJS)\test_gui_ipc_test_server.obj \ $(OBJS)\test_gui_tlw.obj \ $(OBJS)\test_gui_dataview.obj \ $(OBJS)\test_gui_rowheightcachetest.obj \ @@ -873,7 +877,7 @@ $(OBJS)\test_dummy.obj: .\dummy.cpp $(CXX) /c /nologo /TP /Fo$@ $(TEST_CXXFLAGS) /Yctestprec.h .\dummy.cpp $(OBJS)\test_test.obj: .\test.cpp - $(CXX) /c /nologo /TP /Fo$@ $(TEST_CXXFLAGS) .\test.cpp + $(CXX) /c /nologo /TP /Fo$@ $(TEST_CXXFLAGS) /DTEST_HAS_IPC_SERVER .\test.cpp $(OBJS)\test_anytest.obj: .\any\anytest.cpp $(CXX) /c /nologo /TP /Fo$@ $(TEST_CXXFLAGS) .\any\anytest.cpp @@ -1004,6 +1008,9 @@ $(OBJS)\test_typeinfotest.obj: .\misc\typeinfotest.cpp $(OBJS)\test_ipc.obj: .\net\ipc.cpp $(CXX) /c /nologo /TP /Fo$@ $(TEST_CXXFLAGS) .\net\ipc.cpp +$(OBJS)\test_ipc_test_server.obj: .\net\ipc_test_server.cpp + $(CXX) /c /nologo /TP /Fo$@ $(TEST_CXXFLAGS) .\net\ipc_test_server.cpp + $(OBJS)\test_socket.obj: .\net\socket.cpp $(CXX) /c /nologo /TP /Fo$@ $(TEST_CXXFLAGS) .\net\socket.cpp @@ -1454,6 +1461,12 @@ $(OBJS)\test_gui_textwrap.obj: .\misc\textwrap.cpp $(OBJS)\test_gui_socket.obj: .\net\socket.cpp $(CXX) /c /nologo /TP /Fo$@ $(TEST_GUI_CXXFLAGS) .\net\socket.cpp +$(OBJS)\test_gui_ipc.obj: .\net\ipc.cpp + $(CXX) /c /nologo /TP /Fo$@ $(TEST_GUI_CXXFLAGS) .\net\ipc.cpp + +$(OBJS)\test_gui_ipc_test_server.obj: .\net\ipc_test_server.cpp + $(CXX) /c /nologo /TP /Fo$@ $(TEST_GUI_CXXFLAGS) .\net\ipc_test_server.cpp + $(OBJS)\test_gui_tlw.obj: .\persistence\tlw.cpp $(CXX) /c /nologo /TP /Fo$@ $(TEST_GUI_CXXFLAGS) .\persistence\tlw.cpp diff --git a/tests/net/ipc.cpp b/tests/net/ipc.cpp index c5494a2ad4..58bd1ed8bc 100644 --- a/tests/net/ipc.cpp +++ b/tests/net/ipc.cpp @@ -3,50 +3,72 @@ // Purpose: IPC classes unit tests // Author: Vadim Zeitlin // Copyright: (c) 2008 Vadim Zeitlin +// Modified by: JP Mattia, 2024 // Licence: wxWindows licence /////////////////////////////////////////////////////////////////////////////// -// For compilers that support precompilation, includes "wx/wx.h". -// and "wx/cppunit.h" #include "testprec.h" -// FIXME: this tests currently sometimes hangs in Connect() for unknown reason -// and this prevents buildbot builds from working so disabling it, but -// the real problem needs to be fixed, of course -#if 0 +// The IPC tests use a single test binary: the server is started by re-executing +// the same test program with WX_IPC_TEST_SERVER set (see ipc_test_server.cpp). +// The client runs in the main Catch2 process. Catch2 cannot run checks in the +// server process, so the client queries the server for state and verifies it +// here. +// +// This test requires wxUSE_THREADS==1 since it runs the test server concurrently +// with the client. One build configuration is excluded: wxQt. +// +// wxQt is excluded because of a bug in wxQt found during our testing: +// wxIPC worker threads marshal their socket I/O to the main thread via +// CallAfter(), but a cross-thread CallAfter() is not reliably processed by the +// wxQt event loop. wxQtEventLoopBase::WakeUp() wakes the loop without posting a +// Qt event, so the idle handler that runs pending events is never scheduled, and +// server-pushed Advise() notifications stall. That is a wxQt event-loop bug, not +// a wxIPC bug; it is fixed separately on branch +// jpmattia/wxQT-CallAfter-wxWakeUpIdle, which will be a separate PR. +// +#if wxUSE_THREADS && !defined(__WXQT__) -// this test needs threads as it runs the test server in a secondary thread -#if wxUSE_THREADS - -// for all others, include the necessary headers #ifndef WX_PRECOMP #include "wx/app.h" -#endif +#endif // WX_PRECOMP + +#include "ipc_setup_test.h" +#include "ipc_test_server.h" #include "wx/ipc.h" #include "wx/thread.h" +#include "wx/utils.h" +#include "wx/evtloop.h" +#include "wx/stopwatch.h" -#define wxUSE_SOCKETS_FOR_IPC (!wxUSE_DDE_FOR_IPC) +#include +#include -namespace -{ +// forward decl +class IPCTestClient; -const char *IPC_TEST_PORT = "4242"; -const char *IPC_TEST_TOPIC = "IPC TEST"; +// When g_showMessageTiming is set to true, Advise() and RequestReply() +// messages will be printed when they arrive. This shows how the IPC messages +// arrive and whether they interleave. +bool g_showMessageTiming = false; -} // anonymous namespace - -// ---------------------------------------------------------------------------- -// test connection class used by IPCTestServer -// ---------------------------------------------------------------------------- +// Output for g_showMessageTiming uses std::cout, so we can get a sense of the +// raw arrival times. +#include +// Test connection class used by the client. class IPCTestConnection : public wxConnection { public: - IPCTestConnection() { } + explicit IPCTestConnection(IPCTestClient* client) + { + m_client = client; + ResetThreadTrackers(); + } - virtual bool OnExec(const wxString& topic, const wxString& data) + virtual bool OnExec(const wxString& topic, const wxString& data) override { if ( topic != IPC_TEST_TOPIC ) return false; @@ -54,93 +76,141 @@ public: return data == "Date"; } + virtual bool OnAdvise(const wxString& topic, + const wxString& item, + const void* data, + size_t size, + wxIPCFormat format) override + { + if ( topic != IPC_TEST_TOPIC ) + return false; + + CHECK( format == wxIPC_TEXT ); + + wxString s(static_cast(data), size); + + if ( item == "SimpleAdvise test" ) + { + if ( s == "OK SimpleAdvise" ) + m_adviseComplete = true; + else + m_generalError << "SimpleAdvise: unexpected payload: " << s << '\n'; + } + + else if ( item == "MultiAdvise test" || + item == "MultiAdvise MultiThread test" || + item == "MultiAdvise MultiThread test with simultaneous Requests" ) + { + HandleThreadAdviseCounting(s); + + if ( m_threadAdviseLastVal[0] == MESSAGE_ITERATIONS && + m_threadAdviseLastVal[1] == MESSAGE_ITERATIONS && + m_threadAdviseLastVal[2] == MESSAGE_ITERATIONS ) + { + m_adviseComplete = true; + } + } + + else + { + m_generalError << "Unknown Advise item: " << item << wxString('\n'); + } + + return true; + } + + + virtual bool OnDisconnect() override; + private: + + void ResetThreadTrackers() + { + m_generalError.clear(); + + m_adviseComplete = false; + + for ( auto& val : m_threadAdviseLastVal ) + val = 0; + } + + void HandleThreadAdviseCounting(const wxString& adviseString); + + wxCRIT_SECT_DECLARE_MEMBER(m_csAssignBuffer); + + IPCTestClient* m_client; + +public: + bool m_adviseComplete; + + int m_threadAdviseLastVal[3]; + + wxString m_generalError; + wxDECLARE_NO_COPY_CLASS(IPCTestConnection); }; -// ---------------------------------------------------------------------------- -// event dispatching thread class -// ---------------------------------------------------------------------------- - -class EventThread : public wxThread +// Helper for the MultiAdvise thread tests. Repeated Advise's of the form +// "MultiAdvise thread " are received during +// the test. Track the serial number in the appropriate +// m_threadAdviseLastVal[N] element for CHECKing at the end of the test. +void IPCTestConnection::HandleThreadAdviseCounting(const wxString& adviseString) { -public: - EventThread() - : wxThread(wxTHREAD_JOINABLE) + wxCRIT_SECT_LOCKER(lock, m_csAssignBuffer); + + wxString info; + adviseString.StartsWith("MultiAdvise thread", &info); + + int threadNumber = wxAtoi(info.Left(2)); + int counterValue = wxAtoi(info.Mid(3)); + int lastval = INT_MIN; // default to causing an error below + + bool err = false; + wxString errString; + + if ( g_showMessageTiming ) + std::cout << adviseString << '\n' << std::flush; + + + switch (threadNumber) { - Create(); - Run(); + case 0: + errString = + "Error: MultiAdvise thread number could not be converted.\n"; + err = true; + break; + + case 1: + case 2: + case 3: + lastval = m_threadAdviseLastVal[threadNumber - 1]; + m_threadAdviseLastVal[threadNumber - 1] = counterValue; + break; + + default: + errString = + "Error: MultiAdvise thread number must be 1, 2, or 3.\n"; + err = true; } -protected: - virtual void *Entry() + if ( lastval != counterValue -1 ) { - wxTheApp->MainLoop(); - - return nullptr; + // Concatenate to any other error: + errString += + "Error: Misordered count in thread " + + wxString::Format("%d - expected %d, received %d\n", + threadNumber, lastval + 1, counterValue); + err = true; } - wxDECLARE_NO_COPY_CLASS(EventThread); -}; - -// ---------------------------------------------------------------------------- -// test server class -// ---------------------------------------------------------------------------- - -class IPCTestServer : public wxServer -{ -public: - IPCTestServer() + if ( err ) { - m_conn = nullptr; - -#if wxUSE_SOCKETS_FOR_IPC - // we must call this from the main thread - wxSocketBase::Initialize(); -#endif // wxUSE_SOCKETS_FOR_IPC - - // we need event dispatching to work for IPC server to work - m_thread = new EventThread; - - Create(IPC_TEST_PORT); + m_generalError += errString; } +} - virtual ~IPCTestServer() - { - wxTheApp->ExitMainLoop(); - - m_thread->Wait(); - delete m_thread; - - delete m_conn; - -#if wxUSE_SOCKETS_FOR_IPC - wxSocketBase::Shutdown(); -#endif // wxUSE_SOCKETS_FOR_IPC - } - - virtual wxConnectionBase *OnAcceptConnection(const wxString& topic) - { - if ( topic != IPC_TEST_TOPIC ) - return nullptr; - - m_conn = new IPCTestConnection; - return m_conn; - } - -private: - EventThread *m_thread; - IPCTestConnection *m_conn; - - wxDECLARE_NO_COPY_CLASS(IPCTestServer); -}; - -static IPCTestServer *gs_server = nullptr; - -// ---------------------------------------------------------------------------- -// test client class -// ---------------------------------------------------------------------------- - +// The actual client is pretty thin, most of the work is done in the +// connection class. class IPCTestClient : public wxClient { public: @@ -157,7 +227,7 @@ public: bool Connect(const wxString& host, const wxString& service, const wxString& topic) { - m_conn = MakeConnection(host, service, topic); + m_conn = (IPCTestConnection*) MakeConnection(host, service, topic); return m_conn != nullptr; } @@ -166,96 +236,792 @@ public: { if ( m_conn ) { + m_conn->Disconnect(); delete m_conn; m_conn = nullptr; } } - wxConnectionBase& GetConn() const + wxConnectionBase* OnMakeConnection() override { - CPPUNIT_ASSERT( m_conn ); + return new IPCTestConnection(this); + } + + IPCTestConnection& GetConn() const + { + REQUIRE( m_conn ); return *m_conn; } -private: - wxConnectionBase *m_conn; + IPCTestConnection* m_conn; wxDECLARE_NO_COPY_CLASS(IPCTestClient); }; -static IPCTestClient *gs_client = nullptr; +static IPCTestClient* gs_client = nullptr; +static wxEventLoop* gs_clientLoop = nullptr; -// ---------------------------------------------------------------------------- -// the test code itself -// ---------------------------------------------------------------------------- +static bool PumpConnect(const wxString& host, + const wxString& service, + const wxString& topic) +{ + return gs_client->Connect(host, service, topic); +} -class IPCTestCase : public CppUnit::TestCase +void IPCClientDispatch(unsigned long timeoutMs) +{ + if ( !gs_clientLoop ) + return; + + // The client loop is already active for the lifetime of IPCFixture, so do + // NOT re-activate it per call: that writes ms_activeLoop and races the + // worker threads reading it via CallAfter() -> WakeUpIdle(). + + // Run any queued CallAfter() work first: worker threads marshal their IPC + // socket I/O to the main thread via wxTCPEventHandler::RunOnMainThread(), + // which posts async method-call events. DispatchTimeout() only services FD + // (socket) events, so without this the marshaled jobs would never run. + if ( wxTheApp ) + wxTheApp->ProcessPendingEvents(); + + gs_clientLoop->DispatchTimeout(timeoutMs); +} + +static void PumpDispatch() +{ + IPCClientDispatch(10); +} + +static void DrainPendingIPCEvents() +{ + if ( gs_clientLoop ) + { + // No per-call activation: the client loop is already active via IPCFixture + // (and DispatchTimeout()/Pending() act on the loop object directly). + for ( int i = 0; i < 100; ++i ) + { + if ( !gs_clientLoop->Pending() ) + break; + + gs_clientLoop->DispatchTimeout(10); + } + } + + if ( wxTheApp ) + { + for ( int i = 0; i < 100; ++i ) + { + if ( !wxTheApp->Pending() ) + break; + + wxTheApp->ProcessPendingEvents(); + } + } +} + +bool IPCTestConnection::OnDisconnect() +{ + m_client->m_conn = nullptr; + return wxConnection::OnDisconnect(); +} + +// MultiRequestThread sends repeated Request()'s, each with a serial number, +// so that we can verify the repeated messages are sent and received correctly +// and in order. +class MultiRequestThread : public wxThread { public: - IPCTestCase() { } -private: - CPPUNIT_TEST_SUITE( IPCTestCase ); - CPPUNIT_TEST( Connect ); - CPPUNIT_TEST( Execute ); - CPPUNIT_TEST( Disconnect ); - CPPUNIT_TEST_SUITE_END(); + // label: A header to be put on the string sent to the server. + // It should be of the form "MultiRequest thread N", where N + // is "1", "2", or "3". + MultiRequestThread(const wxString& label ) + : wxThread(wxTHREAD_JOINABLE) + { + m_label = label; - void Connect(); - void Execute(); - void Disconnect(); + // Resolve the connection on the main thread. GetConn() uses + // REQUIRE(), a Catch2 macro that is not thread-safe, so it + // must not run on the worker thread in Entry(). The + // connection is stable for our lifetime, so caching the + // pointer is safe. + m_conn = &gs_client->GetConn(); - wxDECLARE_NO_COPY_CLASS(IPCTestCase); + Create(); + } + +protected: + virtual void* Entry() override + { + IPCTestConnection& conn = *m_conn; + + for (size_t n=1; n < MESSAGE_ITERATIONS + 1; n++) + { + wxString s = m_label + wxString::Format(" %zu", n); + size_t size = 0; + const char* data = (const char*) conn.Request(s, &size, wxIPC_PRIVATE); + + // Catch2 macros are not thread safe, so we check explicitly and + // store any deviation from the expected result. + if ( wxString(data) != "OK: " + s ) + { + m_error += "MultiRequestThread error: expected \"OK: " + s; + m_error += ", received " + wxString(data); + m_error += '\n'; + } + + if ( g_showMessageTiming ) + std::cout << wxString(data) << '\n' << std::flush; + + // Space out the requests, to test any race conditions with + // incoming messages, like Advise() + wxMilliSleep(50); + } + + return nullptr; + } + +public: + wxString m_label; + wxString m_error; + IPCTestConnection* m_conn = nullptr; + + wxDECLARE_NO_COPY_CLASS(MultiRequestThread); }; -CPPUNIT_TEST_SUITE_REGISTRATION( IPCTestCase ); -CPPUNIT_TEST_SUITE_NAMED_REGISTRATION( IPCTestCase, "IPCTestCase" ); - -void IPCTestCase::Connect() +// A deadlock cannot be detected from the main thread, because the main thread +// is precisely what gets stuck. This watchdog runs on its own thread and aborts +// the process with a diagnostic if the test does not signal completion in time, +// turning an otherwise indefinite hang into a clear, bounded failure. +class DeadlockWatchdog : public wxThread { - gs_server = new IPCTestServer; - gs_client = new IPCTestClient; +public: + explicit DeadlockWatchdog(int timeoutMs, const wxString& what = "the IPC test") + : wxThread(wxTHREAD_JOINABLE), m_timeoutMs(timeoutMs), m_what(what) {} - // connecting to the wrong port should fail - CPPUNIT_ASSERT( !gs_client->Connect("localhost", "2424", IPC_TEST_TOPIC) ); + // Called by the test once it has completed normally. + void Done() { m_done.store(true); } - // connecting using an unsupported topic should fail (unless the server - // expects a ROT-13'd topic name...) - CPPUNIT_ASSERT( !gs_client->Connect("localhost", IPC_TEST_PORT, "VCP GRFG") ); - - // connecting to the right port on the right topic should succeed - CPPUNIT_ASSERT( gs_client->Connect("localhost", IPC_TEST_PORT, IPC_TEST_TOPIC) ); -} - -void IPCTestCase::Execute() -{ - wxConnectionBase& conn = gs_client->GetConn(); - - const wxString s("Date"); - CPPUNIT_ASSERT( conn.Execute(s) ); - CPPUNIT_ASSERT( conn.Execute(s.mb_str(), s.length() + 1) ); - - char bytes[] = { 1, 2, 3 }; - CPPUNIT_ASSERT( conn.Execute(bytes, WXSIZEOF(bytes)) ); -} - -void IPCTestCase::Disconnect() -{ - if ( gs_client ) +protected: + void* Entry() override { - gs_client->Disconnect(); + const int step = 50; + for ( int waited = 0; waited < m_timeoutMs; waited += step ) + { + if ( m_done.load() ) + return nullptr; + wxMilliSleep(step); + } + + std::cerr << "\nIPC WATCHDOG: " << m_what + << " did not complete within " << m_timeoutMs + << " ms; aborting to avoid a CI hang.\n" << std::flush; + abort(); + } + + const int m_timeoutMs; + const wxString m_what; + std::atomic m_done{false}; + + wxDECLARE_NO_COPY_CLASS(DeadlockWatchdog); +}; + +// RAII wrapper that runs a DeadlockWatchdog for its whole lifetime. Used as the +// first member of IPCFixture so a watchdog covers the entire fixture (setup, +// test body, and teardown): if any of them blocks, e.g. a socket Connect() +// that never returns under an environment where the test server can't run, as +// on the Wine-based wxMSW cross-builds. The watchdog aborts with a diagnostic +// after the timeout instead of letting CI hang until its multi-hour job cap. +class FixtureWatchdog +{ +public: + FixtureWatchdog(int timeoutMs, const wxString& what) + : m_watchdog(timeoutMs, what) { m_watchdog.Run(); } + ~FixtureWatchdog() { m_watchdog.Done(); m_watchdog.Wait(); } + +private: + DeadlockWatchdog m_watchdog; + wxDECLARE_NO_COPY_CLASS(FixtureWatchdog); +}; + +// IPCFixture starts a fresh IPC server for each test, connects a client to it, +// and shuts the server down again in the destructor, so no IPC server process +// is ever alive between tests, or during the unrelated GUI control tests that +// run in the same test_gui binary. +// +// (A previous version shared one long-lived server across all IPC tests to dodge +// an intermittent Wine Connect() hang caused by rapidly restarting the localhost +// listener. But that left the server (a second GUI process on the one Xvfb +// display) alive for the rest of the run, where it stole window-from-point and +// focus from the client and failed ~30 non-IPC GUI tests, guifuncs/treectrl/ +// listbase/etc. The hang is now prevented at its source instead: the client's +// connect attempt is time-bounded (wxIPCTimeout, see wxTCPClient::MakeConnection) +// so a not-yet-ready server fails the attempt promptly and the readiness poll +// below retries, rather than blocking on the socket's long default timeout.) +class IPCFixture +{ + // Declared first so it is constructed first and destroyed last: the watchdog + // then covers the whole fixture lifetime (setup, test body, teardown). + FixtureWatchdog m_watchdog{180000, "an IPC test (setup, body, or teardown)"}; + std::unique_ptr m_clientLoop{new wxEventLoop}; + std::unique_ptr m_loopActivator; + IPCServerThread m_server; + +public: + IPCFixture() + { +#if wxUSE_SOCKETS_FOR_IPC + wxSocketBase::Initialize(); +#endif // wxUSE_SOCKETS_FOR_IPC + + DrainPendingIPCEvents(); + + gs_clientLoop = m_clientLoop.get(); + + // Activate the client loop once for the lifetime of this fixture so its + // worker threads see a stable wxEventLoopBase::ms_activeLoop. Activating + // per IPCClientDispatch() call would write ms_activeLoop and race the + // workers' CallAfter() -> WakeUpIdle() -> GetActive() reads. + m_loopActivator.reset(new wxEventLoopActivator(m_clientLoop.get())); + + gs_client = new IPCTestClient; + + REQUIRE( m_server.Start() ); + + // Wait until the freshly-launched server is ready to accept a + // connection: the re-exec'd server process can take a while to come up + // (well over a second under sanitizers, or as a GUI process doing full + // toolkit init). Poll with a throwaway connection until one succeeds, + // then drop it so the test starts from a clean state. The bound is + // wall-clock based, not iteration based: in a GUI event loop + // IPCClientDispatch() returns at once (idle events), so a fixed + // iteration count would expire before a GUI server is listening. Each + // Connect() attempt is itself time-bounded (see the class comment), so a + // not-yet-ready server fails promptly and we retry rather than blocking. + bool serverReady = false; + wxStopWatch sw; + while ( !serverReady && sw.Time() < 30000 ) // up to 30s + { + if ( gs_client->Connect("localhost", IPC_TEST_PORT, IPC_TEST_TOPIC) ) + { + gs_client->Disconnect(); + serverReady = true; + } + else + { + // Pace the retries: a refused Connect() returns at once, and + // IPCClientDispatch() also returns immediately when idle, so + // without a sleep this loop spins hundreds of connect attempts + // (each creating and destroying a socket) while the server comes + // up, enough churn to exhaust resources over a whole run of + // per-test servers. Sleep briefly so we poll ~20x/second instead. + IPCClientDispatch(50); + wxMilliSleep(50); + } + } + REQUIRE( serverReady ); + } + + ~IPCFixture() + { + // Ask the server to shut down cleanly, then wait for the child to exit. + // This runs while the client loop is still active, so the server's + // wxProcess::OnTerminate() is dispatched before its handler is destroyed + // (see PumpForProcessExit() in the server launcher). + if ( gs_client ) + { + if ( !gs_client->m_conn ) + PumpConnect("localhost", IPC_TEST_PORT, IPC_TEST_TOPIC); + + if ( gs_client->m_conn ) + { + const wxString s("shutdown"); + gs_client->GetConn().Execute(s); + wxMilliSleep(100); + } + } + + m_server.WaitForExit(); + + DrainPendingIPCEvents(); + + if ( gs_client ) + gs_client->Disconnect(); + + DrainPendingIPCEvents(); + + m_loopActivator.reset(); // restore the previously-active loop, once + gs_clientLoop = nullptr; + m_clientLoop.reset(); + delete gs_client; gs_client = nullptr; - } - if ( gs_server ) - { - delete gs_server; - gs_server = nullptr; +#if wxUSE_SOCKETS_FOR_IPC + wxSocketBase::Shutdown(); +#endif // wxUSE_SOCKETS_FOR_IPC + + if ( g_showMessageTiming ) + std::cout << "teardown complete\n" << std::flush; } +}; + +// Test the basics of Connect() +TEST_CASE_METHOD(IPCFixture, + "IPC::Connect", "[net][ipc][single_command]") +{ + if ( g_showMessageTiming ) + std::cout << "Running test Connect\n" << std::flush; + + // connecting to the wrong port should fail + CHECK( !PumpConnect("localhost", "2424", IPC_TEST_TOPIC) ); + + // connecting with the wrong topic should fail + CHECK( !PumpConnect("localhost", IPC_TEST_PORT, "VCP GRFG") ); + + // Connecting to the right port on the right topic should succeed. + REQUIRE( PumpConnect("localhost", IPC_TEST_PORT, IPC_TEST_TOPIC) ); } -#endif // wxUSE_THREADS +// Test the basics of Request(): A Request() goes out and it should result in +// a reply from the server. +TEST_CASE_METHOD(IPCFixture, + "IPC::SingleRequest", "[net][ipc][single_command]") +{ + if ( g_showMessageTiming ) + std::cout << "Running test SingleRequest\n" << std::flush; -#endif // !__WINDOWS__ + // If the connection itself failed there is no point in probing + // the server, and it distinguishes a connect failure from a + // Request() failure below. + REQUIRE( PumpConnect("localhost", IPC_TEST_PORT, IPC_TEST_TOPIC) ); + + IPCTestConnection& conn = gs_client->GetConn(); + + const wxString s("ping"); + size_t size = 0; + const char* data = (char*) conn.Request( s, &size, wxIPC_PRIVATE); + + // Guard against a null return before constructing a wxString from it: + // a failed Request() must report cleanly instead of dereferencing null + // (this was an information-free SIGSEGV on wxMSW). size is logged to help + // diagnose why the very first post-connect Request would fail. + INFO( "Request() returned size=" << size ); + REQUIRE( data != nullptr ); + + // Make sure that Request() works, because we use it to probe the + // state of the server for the remaining tests. + REQUIRE( wxString(data) == "pong" ); +} + +// Test the basics of Execute(). The Execute() goes out: Note that a return +// value of "true" means simply that the message was transmitted. We follow +// the Execute with a Request() to verify that the server received the Execute +// correctly. +TEST_CASE_METHOD(IPCFixture, + "IPC::SingleExecute", "[net][ipc][single_command]") +{ + if ( g_showMessageTiming ) + std::cout << "Running test Execute\n" << std::flush; + + CHECK( PumpConnect("localhost", IPC_TEST_PORT, IPC_TEST_TOPIC) ); + + IPCTestConnection& conn = gs_client->GetConn(); + + wxString s("Date"); + CHECK( conn.Execute(s) ); + + // Get the last execute from the server side. + size_t size = 0; + const wxString lastExecuteQuery("last_execute"); + + char* data = (char*) conn.Request(lastExecuteQuery, &size, wxIPC_PRIVATE); + CHECK( wxString(data) == s ); + + + s = "another execution command!"; + CHECK( conn.Execute(s.mb_str(), s.length() + 1) ); + + data = (char*) conn.Request(lastExecuteQuery, &size, wxIPC_PRIVATE); + CHECK( wxString(data) == s ); +} + +// Send multiple requests to the server. Each request has a serial number, and +// this test verifies that the replies have the correct serial in the reply +// message. After the serial requests are done, the client queries the server +// and verifies the server received the requests error-free. +TEST_CASE_METHOD(IPCFixture, + "IPC::RequestThread", "[net][ipc][multi_command]") +{ + if ( g_showMessageTiming ) + std::cout << "Running test Single Thread Of Requests\n" << std::flush; + + CHECK( PumpConnect("localhost", IPC_TEST_PORT, IPC_TEST_TOPIC) ); + + MultiRequestThread thread1("MultiRequest thread 1"); + thread1.Run(); + WaitForThreadWithDispatch(thread1); + + INFO( thread1.m_error ); + CHECK( thread1.m_error.empty() ); + + // Make sure the server got all the requests in the correct order. + IPCTestConnection& conn = gs_client->GetConn(); + + size_t size = 0; + wxString query("get_thread1_request_counter"); + + char* data = (char*) conn.Request(query, &size, wxIPC_PRIVATE); + CHECK( wxString(data) == MESSAGE_ITERATIONS_STRING ); + + size = 0; + query = "get_error_string"; + data = (char*) conn.Request(query, &size, wxIPC_PRIVATE); + + INFO( wxString(data) ); + CHECK( wxString(data).empty() ); +} + +// Send multiple requests to the server via three concurrent threads. Each +// reply is verified to make sure that the request corresponds to the correct +// thread and has the correctly ordered serial number. After the request +// threads are finished, the client queries the server and verifies the server +// received the requests error-free. +TEST_CASE_METHOD(IPCFixture, + "IPC::RequestMultiThread", "[net][ipc][multi_thread]") +{ + if ( g_showMessageTiming ) + std::cout << "Running test Requests with Multiple Threads\n" + << std::flush; + + CHECK( PumpConnect("localhost", IPC_TEST_PORT, IPC_TEST_TOPIC) ); + + MultiRequestThread thread1("MultiRequest thread 1"); + MultiRequestThread thread2("MultiRequest thread 2"); + MultiRequestThread thread3("MultiRequest thread 3"); + + thread1.Run(); + thread2.Run(); + thread3.Run(); + + WaitForThreadWithDispatch(thread1); + WaitForThreadWithDispatch(thread2); + WaitForThreadWithDispatch(thread3); + + INFO( thread1.m_error ); + CHECK( thread1.m_error.empty() ); + + INFO( thread2.m_error ); + CHECK( thread2.m_error.empty() ); + + INFO( thread3.m_error ); + CHECK( thread3.m_error.empty() ); + + // Make sure the server got all the requests in the correct order. + IPCTestConnection& conn = gs_client->GetConn(); + + size_t size = 0; + wxString query = "get_thread1_request_counter"; + + char* data = (char*) conn.Request(query, &size, wxIPC_PRIVATE); + CHECK( wxString(data) == MESSAGE_ITERATIONS_STRING ); + + size = 0; + query = "get_thread2_request_counter"; + + data = (char*) conn.Request(query, &size, wxIPC_PRIVATE); + CHECK( wxString(data) == MESSAGE_ITERATIONS_STRING ); + + size = 0; + query = "get_thread3_request_counter"; + + data = (char*) conn.Request(query, &size, wxIPC_PRIVATE); + CHECK( wxString(data) == MESSAGE_ITERATIONS_STRING ); + + size = 0; + query = "get_error_string"; + data = (char*) conn.Request(query, &size, wxIPC_PRIVATE); + + INFO( wxString(data) ); + CHECK( wxString(data).empty() ); +} + +// Test the basics of Advise(). First, send a StartAdvise(), then wait for the +// server to send a single Advise(). When that is received, StopAdvise() is +// sent. +TEST_CASE_METHOD(IPCFixture, + "IPC::SingleAdvise", "[net][ipc][single_command]") +{ + if ( g_showMessageTiming ) + std::cout << "Running test Advise as single command\n" << std::flush; + + CHECK( PumpConnect("localhost", IPC_TEST_PORT, IPC_TEST_TOPIC) ); + + IPCTestConnection& conn = gs_client->GetConn(); + wxString item = "SimpleAdvise test"; + + CHECK( conn.StartAdvise(item) ); + + // Wait a maximum of 2 seconds for completion. The bound is wall-clock based, + // not iteration based: under a GUI event loop PumpDispatch() returns at once + // (idle events), so a fixed iteration count would expire almost immediately, + // before the server's advise arrives. + wxStopWatch sw; + while ( sw.Time() < 2000 && !conn.m_adviseComplete ) + { + PumpDispatch(); + } + + CHECK( conn.StopAdvise(item) ); + CHECK( conn.m_adviseComplete ); + + // Make sure the server didn't record an error + wxString query = "get_error_string"; + size_t size = 0; + + char* data = (char*) conn.Request(query, &size, wxIPC_PRIVATE); + + INFO( wxString(data) ); + CHECK( wxString(data).empty() ); +} + +// Instruct the server to send a series of Advise() items to the client. Each +// Advise() is verified to make sure that the Advise() arrives in order by +// checking its serial number. Also verifies that the server encountered no +// errors during the Advise() calls. +TEST_CASE_METHOD(IPCFixture, + "IPC::AdviseThread", "[net][ipc][multi_command]") +{ + if ( g_showMessageTiming ) + std::cout << "Running test Single Thread Of Advise()'s\n" << std::flush; + + CHECK( PumpConnect("localhost", IPC_TEST_PORT, IPC_TEST_TOPIC) ); + + IPCTestConnection& conn = gs_client->GetConn(); + wxString item = "MultiAdvise test"; + + CHECK( conn.StartAdvise(item) ); + + // Wait a maximum of 20 seconds for completion (wall-clock bounded; see the + // note in IPC::SingleAdvise about GUI event loops and PumpDispatch()). + wxStopWatch sw; + while ( sw.Time() < 20000 && + conn.m_threadAdviseLastVal[0] != MESSAGE_ITERATIONS ) + { + PumpDispatch(); + } + + CHECK( conn.StopAdvise(item) ); + + // Verify the results of the test. + CHECK( conn.m_threadAdviseLastVal[0] == MESSAGE_ITERATIONS ); + + INFO( conn.m_generalError ); + CHECK( conn.m_generalError.empty() ); + + // Make sure the server didn't record an error + wxString query = "get_error_string"; + size_t size = 0; + + char* data = (char*) conn.Request(query, &size, wxIPC_PRIVATE); + + INFO( wxString(data) ); + CHECK( wxString(data).empty() ); +} + +// Instruct the server to send a series of Advise() items to the client via +// three concurrent threads. Each Advise() is verified to make sure that the +// Advise() arrives in order within its thread number. Also verifies that +// the server encountered no errors during the Advise() calls. +TEST_CASE_METHOD(IPCFixture, + "IPC::AdviseMultiThread", "[net][ipc][multi_thread]") +{ + if ( g_showMessageTiming ) + std::cout << "Running test MultipleThreadsOfMultiAdvise\n" << std::flush; + + CHECK( PumpConnect("localhost", IPC_TEST_PORT, IPC_TEST_TOPIC) ); + + IPCTestConnection& conn = gs_client->GetConn(); + wxString item = "MultiAdvise MultiThread test"; + + CHECK( conn.StartAdvise(item) ); + + // Wait a maximum of 20 seconds for completion (wall-clock bounded; see the + // note in IPC::SingleAdvise about GUI event loops and PumpDispatch()). + wxStopWatch sw; + while ( sw.Time() < 20000 ) + { + PumpDispatch(); + + if ( conn.m_threadAdviseLastVal[0] == MESSAGE_ITERATIONS && + conn.m_threadAdviseLastVal[1] == MESSAGE_ITERATIONS && + conn.m_threadAdviseLastVal[2] == MESSAGE_ITERATIONS) + { + break; + } + } + + CHECK( conn.StopAdvise(item) ); + + CHECK( conn.m_threadAdviseLastVal[0] == MESSAGE_ITERATIONS ); + CHECK( conn.m_threadAdviseLastVal[1] == MESSAGE_ITERATIONS ); + CHECK( conn.m_threadAdviseLastVal[2] == MESSAGE_ITERATIONS ); + + INFO( conn.m_generalError ); + CHECK( conn.m_generalError.empty() ); + + // Make sure the server didn't record an error + wxString query = "get_error_string"; + size_t size = 0; + + char* data = (char*) conn.Request(query, &size, wxIPC_PRIVATE); + + INFO( wxString(data) ); + CHECK( wxString(data).empty() ); +} + +// Run three concurrent threads in the client sending Requests() to the +// server, and simultaneously run three concurrent threads in the server +// sending Advise() information to the client. Verify that all messages are +// serial and correspond to the correct thread. Lastly, verify that the server +// encountered no errors during this test. +// +// By setting g_showMessageTiming to "true", the ordering of the Requests +// and Advise's can be seen. Different systems may need to change the delay +// wxMilliSleep in the client and server threads to make the interleave happen +// properly, which is a stringent test of race conditions that might be present +// in wxIPC. +TEST_CASE_METHOD(IPCFixture, + "IPC::AdviseAndRequestMultiThread", "[net][ipc][multi_thread]") +{ + if ( g_showMessageTiming ) + std::cout << "Running test MultiAdvise MultiThreads test with simultaneous MultiRequests MultiThreads\n" << std::flush; + + CHECK( PumpConnect("localhost", IPC_TEST_PORT, IPC_TEST_TOPIC) ); + IPCTestConnection& conn = gs_client->GetConn(); + + MultiRequestThread thread1("MultiRequest thread 1"); + MultiRequestThread thread2("MultiRequest thread 2"); + MultiRequestThread thread3("MultiRequest thread 3"); + + // start local and remote threads as close to simultaneously as possible + wxString item = "MultiAdvise MultiThread test with simultaneous Requests"; + + CHECK( conn.StartAdvise(item) ); // starts 3 advise threads on the server + + thread1.Run(); + thread2.Run(); + thread3.Run(); + + // Phase 1: Request() threads process interleaved Advise() via FindMessage(). + // Do not PumpDispatch() here: main-thread dispatch races worker Request(). + WaitForThreadWithDispatch(thread1); + WaitForThreadWithDispatch(thread2); + WaitForThreadWithDispatch(thread3); + + // Phase 2: dispatch any remaining Advise() notifications on the main thread. + // Wall-clock bounded; see the note in IPC::SingleAdvise about GUI event loops. + wxStopWatch sw; + while ( sw.Time() < 20000 ) + { + PumpDispatch(); + + if ( conn.m_threadAdviseLastVal[0] == MESSAGE_ITERATIONS && + conn.m_threadAdviseLastVal[1] == MESSAGE_ITERATIONS && + conn.m_threadAdviseLastVal[2] == MESSAGE_ITERATIONS ) + { + break; + } + } + + CHECK( conn.StopAdvise(item) ); + + // Everything is done, check that all the advise messages were + // correctly received. + + CHECK( conn.m_threadAdviseLastVal[0] == MESSAGE_ITERATIONS ); + CHECK( conn.m_threadAdviseLastVal[1] == MESSAGE_ITERATIONS ); + CHECK( conn.m_threadAdviseLastVal[2] == MESSAGE_ITERATIONS ); + + INFO( conn.m_generalError ); + CHECK( conn.m_generalError.empty() ); + + + // Also make sure all the request messages were correctly received on + // the server side. The client side was already validated in the + // MultiRequestThread. + size_t size = 0; + wxString query = "get_thread1_request_counter"; + + char* data = (char*) conn.Request(query, &size, wxIPC_PRIVATE); + CHECK( wxString(data) == MESSAGE_ITERATIONS_STRING ); + + size = 0; + query = "get_thread2_request_counter"; + + data = (char*) conn.Request(query, &size, wxIPC_PRIVATE); + CHECK( wxString(data) == MESSAGE_ITERATIONS_STRING ); + + size = 0; + query = "get_thread3_request_counter"; + + data = (char*) conn.Request(query, &size, wxIPC_PRIVATE); + CHECK( wxString(data) == MESSAGE_ITERATIONS_STRING ); + + size = 0; + query = "get_error_string"; + data = (char*) conn.Request(query, &size, wxIPC_PRIVATE); + + INFO( wxString(data) ); + CHECK( wxString(data).empty() ); +} + +// Exercises the case where a Request() is issued on the main thread while a +// worker thread is also issuing Request()s on the same connection, which was +// a source of several problems. +// +// When this test fails, the watchdog bounds the time-to-failure; the test should +// complete near-instantly if main-thread and worker-thread Request()s are +// properly serialized. +TEST_CASE_METHOD(IPCFixture, + "IPC::ConcurrentMainAndWorkerRequest", "[net][ipc][multi_command]") +{ + CHECK( PumpConnect("localhost", IPC_TEST_PORT, IPC_TEST_TOPIC) ); + IPCTestConnection& conn = gs_client->GetConn(); + + // Generous timeout: the test completes in ~1-2s when healthy, so the + // watchdog only ever fires on a *permanent* deadlock (which never recovers). + // A large value avoids spurious aborts on slow/loaded CI runners or under + // sanitizers, at no cost to the passing case. + DeadlockWatchdog watchdog(30000, + "concurrent main- and worker-thread Request() on the same connection"); + watchdog.Run(); + + MultiRequestThread worker("MultiRequest thread 1"); + worker.Run(); + + // Hammer the connection from the main thread while the worker does the same + // from its thread. Pump between requests so that, absent the deadlock, the + // worker's marshalled socket I/O can run on the main thread. + while ( worker.IsRunning() ) + { + size_t size = 0; + const char* pong = (const char*) conn.Request("ping", &size, wxIPC_PRIVATE); + + CHECK( pong != nullptr ); + if ( pong ) + CHECK( wxString(pong) == "pong" ); + + IPCClientDispatch(5); + } + + worker.Wait(); + watchdog.Done(); + watchdog.Wait(); + + INFO( worker.m_error ); + CHECK( worker.m_error.empty() ); +} + +#endif // wxUSE_THREADS && !__WXQT__ diff --git a/tests/net/ipc_setup_test.h b/tests/net/ipc_setup_test.h new file mode 100644 index 0000000000..3679bb9df1 --- /dev/null +++ b/tests/net/ipc_setup_test.h @@ -0,0 +1,33 @@ +/////////////////////////////////////////////////////////////////////////////// +// Name: tests/net/ipc_setup_test.h +// Purpose: IPC classes unit tests +// Author: Vadim Zeitlin +// Copyright: (c) 2008 +// Modified by: JP Mattia, 2024 +// Licence: wxWindows licence +/////////////////////////////////////////////////////////////////////////////// + +#ifndef _WX_TESTS_NET_IPC_H_ +#define _WX_TESTS_NET_IPC_H_ + +// This test was written for sockets +#define wxUSE_SOCKETS_FOR_IPC 1 +#define wxUSE_DDE_FOR_IPC 0 + +#define IPC_TEST_PORT "4242" +#define IPC_TEST_TOPIC "IPC TEST" + +// Many IPC issues show up only after several iterations, and +// MESSAGE_ITERATIONS sets the number of iterations. The value of 20 is chosen +// as a compromise between exposing race conditions vs slowing down the +// overall automated test process. +// +// For stress-testing: if the number of MESSAGE_ITERATIONS is set much beyond +// 200, then the client-side wait bounds should likely also be increased. +// Search for "Wait a maximum" in ipc.cpp to find those values. (The server +// side needs no adjustment: it paces each Advise() and otherwise waits for +// completion, so it scales with the iteration count on its own.) +#define MESSAGE_ITERATIONS 20 +#define MESSAGE_ITERATIONS_STRING wxString::Format("%d",MESSAGE_ITERATIONS) + +#endif // _WX_TESTS_NET_IPC_H_ diff --git a/tests/net/ipc_test_server.cpp b/tests/net/ipc_test_server.cpp new file mode 100644 index 0000000000..275114524a --- /dev/null +++ b/tests/net/ipc_test_server.cpp @@ -0,0 +1,717 @@ +/////////////////////////////////////////////////////////////////////////////// +// Name: tests/net/ipc_test_server.cpp +// Purpose: IPC test server for unit tests +// Authors: Vadim Zeitlin, JP Mattia +// Copyright: (c) 2024 +// Licence: wxWindows licence +/////////////////////////////////////////////////////////////////////////////// + +#include "testprec.h" + +// Match the guard in tests/net/ipc.cpp + +#if wxUSE_THREADS && !defined(__WXQT__) + +#ifndef WX_PRECOMP + #include "wx/app.h" +#endif + +#include "ipc_test_server.h" +#include "ipc_setup_test.h" + +#include "wx/ipc.h" +#include "wx/thread.h" +#include "wx/evtloop.h" +#include "wx/vector.h" +#include "wx/process.h" +#include "wx/timer.h" +#include "wx/filename.h" +#include "wx/stdpaths.h" + +#include "wx/private/make_unique.h" + +#ifdef __UNIX__ + #include +#endif + +#define MAX_MSG_BUFFERS 2048 + +class IPCServerTestServer; + +class IPCServerConnection : public wxConnection +{ +public: + explicit IPCServerConnection(IPCServerTestServer* server) + { + m_server = server; + ResetThreadTrackers(); + + for ( auto& buf : m_bufferList ) + buf = nullptr; + + m_nextAvailable = 0; + } + + ~IPCServerConnection() + { + m_adviseActive = false; + WaitForAdviseWorkers(); + + for ( auto& buf : m_bufferList ) + delete[] buf; + } + + virtual bool OnExec(const wxString& topic, const wxString& data) override; + virtual bool OnDisconnect() override; + + virtual const void* OnRequest(const wxString& topic, + const wxString& item, + size_t* size, + wxIPCFormat format) override; + + virtual bool OnStartAdvise(const wxString& topic, + const wxString& item) override; + + virtual bool OnStopAdvise(const wxString& topic, + const wxString& item) override; + +private: + wxString HandleThreadRequestCounting(const wxString& item); + void ResetThreadTrackers(); + void StartAdviseWorker(wxThread* thread); + void WaitForAdviseWorkers(); + + char* GetBufPtr(size_t size) + { + wxCRIT_SECT_LOCKER(lock, m_csAssignBuffer); + + delete[] m_bufferList[m_nextAvailable]; + + char* ptr = new char[size]; + + m_bufferList[m_nextAvailable] = ptr; + m_nextAvailable = (m_nextAvailable + 1) % MAX_MSG_BUFFERS; + + return ptr; + } + + wxCRIT_SECT_DECLARE_MEMBER(m_csAssignBuffer); + + IPCServerTestServer* m_server; + + char* m_bufferList[MAX_MSG_BUFFERS]; + int m_nextAvailable; + + wxString m_lastExecute; + + int m_threadRequestLastVal[3]; + +public: + bool m_adviseActive; + wxString m_generalError; + + bool m_waitForFirstRequest; + bool m_firstRequestReceived; + + wxVector m_adviseThreads; + + wxDECLARE_NO_COPY_CLASS(IPCServerConnection); +}; + +class SingleAdviseThread : public wxThread +{ +public: + SingleAdviseThread(IPCServerTestServer* server, const wxString& item) + : wxThread(wxTHREAD_JOINABLE), + m_server(server), + m_item(item) + { + Create(); + } + +protected: + virtual void* Entry() override; + + IPCServerTestServer* m_server; + wxString m_item; + + wxDECLARE_NO_COPY_CLASS(SingleAdviseThread); +}; + +class MultiAdviseThread : public wxThread +{ +public: + MultiAdviseThread(IPCServerTestServer* server, + const wxString& item, + const wxString& label) + : wxThread(wxTHREAD_JOINABLE), + m_server(server), + m_item(item), + m_label(label) + { + Create(); + } + +protected: + virtual void* Entry() override; + + IPCServerTestServer* m_server; + wxString m_item, m_label; + + wxDECLARE_NO_COPY_CLASS(MultiAdviseThread); +}; + +class IPCServerTestServer : public wxServer +{ +public: + IPCServerTestServer() = default; + + virtual ~IPCServerTestServer() + { + delete m_conn; + } + + virtual wxConnectionBase* OnAcceptConnection(const wxString& topic) override + { + if ( topic != IPC_TEST_TOPIC ) + return nullptr; + + m_conn = new IPCServerConnection(this); + return m_conn; + } + + void Shutdown() + { + if ( m_conn ) + m_conn->Disconnect(); + } + + IPCServerConnection& GetConn() + { + if ( !m_conn ) + { + m_conn = new IPCServerConnection(this); + m_conn->m_generalError = "Invalid connection in GetConn()"; + } + + return *m_conn; + } + + IPCServerConnection* m_conn = nullptr; + + wxDECLARE_NO_COPY_CLASS(IPCServerTestServer); +}; + +bool IPCServerConnection::OnExec(const wxString& topic, const wxString& data) +{ + if ( topic != IPC_TEST_TOPIC ) + return false; + + if ( data == "shutdown" ) + { + m_adviseActive = false; + WaitForAdviseWorkers(); + m_server->Shutdown(); + if ( wxEventLoopBase::GetActive() ) + wxEventLoopBase::GetActive()->ScheduleExit(0); + else + _exit(0); + return true; + } + + m_lastExecute = data; + return true; +} + +bool IPCServerConnection::OnDisconnect() +{ + // Clear the server's current-connection pointer only if *this* connection is + // still the current one. Connections can briefly overlap, e.g. the fixture + // probes server readiness with a throwaway connect/disconnect, and that + // probe's disconnect can be processed after the real connection has already + // been accepted and stored. Nulling unconditionally there would discard the + // live connection, after which GetConn() fabricates an unconnected one and + // server-side Advise() fails. (Surfaced under the GUI event loop, where the + // overlap is more likely.) + if ( m_server->m_conn == this ) + m_server->m_conn = nullptr; + return wxConnection::OnDisconnect(); +} + +const void* IPCServerConnection::OnRequest(const wxString& topic, + const wxString& item, + size_t* size, + wxIPCFormat format) +{ + *size = 0; + + if ( topic != IPC_TEST_TOPIC ) + return nullptr; + + m_firstRequestReceived = true; + + wxString s; + + if ( item == "ping" ) + { + if ( format != wxIPC_PRIVATE ) + return nullptr; + + s = "pong"; + } + else if ( item == "last_execute" ) + { + s = m_lastExecute; + } + else if ( item.StartsWith("MultiRequest thread") ) + { + s = HandleThreadRequestCounting(item); + } + else if ( item == "get_thread1_request_counter") + { + s = wxString::Format("%d", m_threadRequestLastVal[0]); + } + else if ( item == "get_thread2_request_counter") + { + s = wxString::Format("%d", m_threadRequestLastVal[1]); + } + else if ( item == "get_thread3_request_counter") + { + s = wxString::Format("%d", m_threadRequestLastVal[2]); + } + else if ( item == "get_error_string") + { + s = m_generalError; + } + else + { + s = "Error: Unknown request - " + item; + m_generalError += s; + } + + *size = strlen(s.mb_str()) + 1; + char* ret = GetBufPtr(*size); + strncpy(ret, s.mb_str(), *size); + return ret; +} + +wxString IPCServerConnection::HandleThreadRequestCounting(const wxString& item) +{ + wxString info; + item.StartsWith("MultiRequest thread", &info); + + int threadNumber = wxAtoi(info.Left(2)); + int counterValue = wxAtoi(info.Mid(3)); + int lastval = -2; + + bool err = false; + wxString errString; + + switch (threadNumber) + { + case 0: + errString = + "Error: MultiRequest thread number could not be converted.\n"; + err = true; + break; + + case 1: + case 2: + case 3: + lastval = m_threadRequestLastVal[threadNumber - 1]; + m_threadRequestLastVal[threadNumber - 1] = counterValue; + break; + + default: + errString = + "Error: MultiRequest thread number must be 1, 2, or 3.\n"; + err = true; + } + + if ( lastval != counterValue -1 ) + { + errString += + "Error: Misordered count in thread " + + wxString::Format("%d - expected %d, received %d\n", + threadNumber, lastval + 1, counterValue); + err = true; + } + + if ( err ) + { + m_generalError += errString; + return errString; + } + + return "OK: " + item; +} + +void IPCServerConnection::ResetThreadTrackers() +{ + m_generalError.clear(); + + for ( auto& val : m_threadRequestLastVal ) + val = 0; + + m_adviseActive = false; + + m_waitForFirstRequest = false; + m_firstRequestReceived = false; +} + +void IPCServerConnection::StartAdviseWorker(wxThread* thread) +{ + thread->Run(); + m_adviseThreads.push_back(thread); +} + +void IPCServerConnection::WaitForAdviseWorkers() +{ + for ( wxThread* thread : m_adviseThreads ) + { + if ( thread->IsRunning() ) + thread->Wait(); + + delete thread; + } + + m_adviseThreads.clear(); +} + +bool IPCServerConnection::OnStartAdvise(const wxString& topic, + const wxString& item) +{ + if ( topic != IPC_TEST_TOPIC ) + return false; + + WaitForAdviseWorkers(); + + m_adviseActive = true; + + if ( item == "SimpleAdvise test" ) + { + StartAdviseWorker(new SingleAdviseThread(m_server, item)); + } + else if ( item.StartsWith("MultiAdvise test") ) + { + StartAdviseWorker( + new MultiAdviseThread(m_server, item, "MultiAdvise thread 1")); + } + else if ( item.StartsWith("MultiAdvise MultiThread test") ) + { + StartAdviseWorker( + new MultiAdviseThread(m_server, item, "MultiAdvise thread 1")); + StartAdviseWorker( + new MultiAdviseThread(m_server, item, "MultiAdvise thread 2")); + StartAdviseWorker( + new MultiAdviseThread(m_server, item, "MultiAdvise thread 3")); + } + else if ( item.StartsWith("MultiAdvise MultiThread test with simultaneous Requests") ) + { + m_waitForFirstRequest = true; + + StartAdviseWorker( + new MultiAdviseThread(m_server, item, "MultiAdvise thread 1")); + StartAdviseWorker( + new MultiAdviseThread(m_server, item, "MultiAdvise thread 2")); + StartAdviseWorker( + new MultiAdviseThread(m_server, item, "MultiAdvise thread 3")); + } + else + { + m_generalError += "Unknown StartAdvise item\n"; + m_adviseActive = false; + } + + return m_adviseActive; +} + +bool IPCServerConnection::OnStopAdvise(const wxString& topic, + const wxString& WXUNUSED(item)) +{ + if ( topic != IPC_TEST_TOPIC ) + return false; + + m_adviseActive = false; + + return true; +} + +void* SingleAdviseThread::Entry() +{ + wxMilliSleep(50); + + IPCServerConnection& conn = m_server->GetConn(); + if ( !conn.m_adviseActive ) + conn.m_generalError = "Advise() call without StartAdvise()\n"; + + wxString s = "OK SimpleAdvise"; + size_t size = strlen(s.mb_str()); + + if ( !conn.Advise(m_item, s.mb_str(), size, wxIPC_TEXT) ) + conn.m_generalError = "Advise() call returned false"; + + return nullptr; +} + +void* MultiAdviseThread::Entry() +{ + IPCServerConnection& conn = m_server->GetConn(); + + if ( !conn.m_adviseActive ) + conn.m_generalError = "Advise() call without StartAdvise()\n"; + + if ( conn.m_waitForFirstRequest ) + { + while ( !conn.m_firstRequestReceived ) + wxMilliSleep(50); + } + + for ( size_t n = 1; n < MESSAGE_ITERATIONS + 1; n++ ) + { + if ( !conn.m_adviseActive ) + break; + + wxMilliSleep(50); + + wxString s = m_label + wxString::Format(" %zu", n); + size_t size = strlen(s.mb_str()); + + if ( !conn.Advise(m_item, s.mb_str(), size, wxIPC_TEXT) ) + { + conn.m_generalError + += wxString::Format(m_label + + "Advise() call returned false: %zu", n); + } + } + + return nullptr; +} + +// ============================================================================ +// In-process server context (server child process entry point) +// ============================================================================ + +class IPCServerContext +{ +public: + IPCServerContext() + : m_server(std::make_unique()) + { + } + + ~IPCServerContext() + { + Stop(); + } + + bool Start() + { + wxEventLoopActivator activate(&m_loop); + + if ( !m_server->Create(IPC_TEST_PORT) ) + return false; + + m_started = true; + return true; + } + + void RunUntilStopped() + { + wxEventLoopActivator activate(&m_loop); + m_loop.Run(); + } + + void Stop() + { + if ( !m_started ) + return; + + m_server->Shutdown(); + m_started = false; + } + +private: + std::unique_ptr m_server; + wxEventLoop m_loop; + bool m_started = false; + + wxDECLARE_NO_COPY_CLASS(IPCServerContext); +}; + +void RunIPCServerUntilStopped() +{ +#if wxUSE_SOCKETS + wxSocketBase::Initialize(); +#endif + + IPCServerContext ctx; + + if ( !ctx.Start() ) + _exit(1); + + ctx.RunUntilStopped(); +} + +// ============================================================================ +// IPCServerThread: launch server via same test binary (no separate executable) +// ============================================================================ + +class IPCServerProcess : public wxProcess +{ +public: + IPCServerProcess() { m_finished = false; } + + virtual void OnTerminate(int pid, int status) override + { + wxUnusedVar(status); + wxUnusedVar(pid); + m_finished = true; + } + + bool IsFinished() const { return m_finished; } + + bool m_finished; +}; + +struct IPCServerLaunchState +{ + std::unique_ptr process; + wxString command; + long pid = 0; + + IPCServerLaunchState() : process(std::make_unique()) {} +}; + +// Pump the real event loop so async wxExecute() can deliver its process-exit +// notification (on MSW, a wxWM_PROC_TERMINATED window message handled by +// wxExecuteWindowCbk, which calls wxProcess::OnTerminate()). The IPC client's +// IPCClientDispatch() can't be used here: during end-of-run teardown the client +// loop is already gone (gs_clientLoop == nullptr) so it no-ops, and the exit +// notification would otherwise be delivered only later, after we've destroyed +// the IPCServerProcess handler, making wxExecuteWindowCbk dereference a freed +// wxProcess. Dispatching here lets OnTerminate() run (and wxExecute free its +// per-process data) while the handler is still alive. +static void PumpForProcessExit(unsigned long timeoutMs) +{ + if ( wxEventLoopBase* const active = wxEventLoopBase::GetActive() ) + { + active->DispatchTimeout(timeoutMs); + } + else + { + wxEventLoop loop; + wxEventLoopActivator activate(&loop); + loop.DispatchTimeout(timeoutMs); + } +} + +class IPCServerLauncher : public wxTimer +{ +public: + IPCServerLauncher(IPCServerLaunchState* state) + : m_state(state) + { + wxFileName fn(wxStandardPaths::Get().GetExecutablePath()); + m_state->command = fn.GetFullPath(); + } + + bool DoStart() + { + if ( m_state->pid ) + DoStop(); + + wxEventLoop loop; + Bind(wxEVT_TIMER, &IPCServerLauncher::OnTimer, this); + StartOnce(10); + loop.Run(); + + return m_state->pid != 0; + } + + void OnTimer(wxTimerEvent& WXUNUSED(event)) + { + wxSetEnv("WX_IPC_TEST_SERVER", "1"); + + // Pass the environment to the child explicitly. Relying on wxExecute() + // to inherit a variable set via wxSetEnv() works for the console "test" + // but not for the wxGTK GUI "test_gui": there the re-executed child did + // not see WX_IPC_TEST_SERVER and ran the whole test suite instead of the + // IPC server. Building the env map and handing it to wxExecute() makes + // the hand-off reliable in both programs. + wxExecuteEnv execEnv; + wxGetEnvMap(&execEnv.env); + execEnv.env["WX_IPC_TEST_SERVER"] = "1"; + + m_state->pid = wxExecute(m_state->command, + wxEXEC_ASYNC, + m_state->process.get(), + &execEnv); + + if ( wxEventLoop::GetActive() ) + wxEventLoop::GetActive()->Exit(); + } + + void DoStop() + { + if ( m_state->pid && !m_state->process->IsFinished() ) + { + wxKill(m_state->pid, wxSIGTERM); + // Pump the real loop (not IPCClientDispatch) so the child's exit + // notification reaches wxProcess::OnTerminate() before the handler + // is destroyed; see PumpForProcessExit(). + for ( int i = 0; i < 250 && !m_state->process->IsFinished(); ++i ) + { + PumpForProcessExit(20); + wxMilliSleep(20); + } + } + + m_state->pid = 0; + wxUnsetEnv("WX_IPC_TEST_SERVER"); + } + + void WaitUntilFinished() + { + for ( int i = 0; i < 100 && !m_state->process->IsFinished(); ++i ) + { + PumpForProcessExit(20); + wxMilliSleep(20); + } + } + + IPCServerLaunchState* m_state; +}; + +struct IPCServerThread::Private +{ + IPCServerLaunchState state; +}; + +IPCServerThread::IPCServerThread() + : m_priv(std::make_unique()) +{ +} + +IPCServerThread::~IPCServerThread() +{ + WaitForExit(); +} + +bool IPCServerThread::Start() +{ + IPCServerLauncher launcher(&m_priv->state); + return launcher.DoStart(); +} + +void IPCServerThread::WaitForExit() +{ + if ( !m_priv->state.pid ) + return; + + IPCServerLauncher launcher(&m_priv->state); + launcher.WaitUntilFinished(); + launcher.DoStop(); +} + +#endif // wxUSE_THREADS && !__WXQT__ diff --git a/tests/net/ipc_test_server.h b/tests/net/ipc_test_server.h new file mode 100644 index 0000000000..52df36e6a9 --- /dev/null +++ b/tests/net/ipc_test_server.h @@ -0,0 +1,62 @@ +/////////////////////////////////////////////////////////////////////////////// +// Name: tests/net/ipc_test_server.h +// Purpose: IPC test server (in-process on Windows, same-binary subprocess +// on Unix where wxSocket IPC requires main-thread event dispatch) +// Author: JP Mattia +// Copyright: (c) 2024 +// Licence: wxWindows licence +/////////////////////////////////////////////////////////////////////////////// + +#ifndef _WX_TESTS_NET_IPC_TEST_SERVER_H_ +#define _WX_TESTS_NET_IPC_TEST_SERVER_H_ + +#include "wx/evtloop.h" +#include "wx/thread.h" + +// Match the guard in tests/net/ipc.cpp and ipc_test_server.cpp: the IPC test is +// excluded from wxQt, so its declarations must be too. +#if wxUSE_THREADS && !defined(__WXQT__) + +#include + +// Starts the IPC test server and blocks until it is listening (or failed). +// Stops the server in WaitForExit(). +class IPCServerThread +{ +public: + IPCServerThread(); + ~IPCServerThread(); + + bool Start(); + void WaitForExit(); + +private: + struct Private; + std::unique_ptr m_priv; + + wxDECLARE_NO_COPY_CLASS(IPCServerThread); +}; + +// Called from tests/test.cpp when WX_IPC_TEST_SERVER is set in the environment. +void RunIPCServerUntilStopped(); + +// Dispatch client-side socket events (implemented in ipc.cpp). +void IPCClientDispatch(unsigned long timeoutMs = 10); + +// Wait for a joinable thread to finish while pumping the client event loop. +// Worker threads marshal their IPC socket I/O to the main thread (see +// wxTCPEventHandler::RunOnMainThread), so we must keep dispatching here for those +// marshaled jobs to run; otherwise the worker blocks forever. This is safe now +// that workers no longer touch the socket themselves (which is what previously +// made re-entrant dispatch corrupt the connection). +inline void WaitForThreadWithDispatch(wxThread& thread) +{ + while ( thread.IsRunning() ) + IPCClientDispatch(10); + + thread.Wait(); +} + +#endif // wxUSE_THREADS && !__WXQT__ + +#endif // _WX_TESTS_NET_IPC_TEST_SERVER_H_ diff --git a/tests/test.bkl b/tests/test.bkl index 9b5d4e9a11..2e755fa89c 100644 --- a/tests/test.bkl +++ b/tests/test.bkl @@ -34,6 +34,7 @@ + -DTEST_HAS_IPC_SERVER test.cpp any/anytest.cpp @@ -79,6 +80,7 @@ misc/pathlist.cpp misc/typeinfotest.cpp net/ipc.cpp + net/ipc_test_server.cpp net/socket.cpp net/webrequest.cpp regex/regextest.cpp @@ -165,6 +167,9 @@ console + + -DTEST_HAS_IPC_SERVER + asserthelper.cpp test.cpp @@ -276,11 +281,13 @@ misc/settings.cpp misc/textwrap.cpp net/socket.cpp + net/ipc.cpp + net/ipc_test_server.cpp persistence/tlw.cpp persistence/dataview.cpp rowheightcache/rowheightcachetest.cpp diff --git a/tests/test.cpp b/tests/test.cpp index 3c00bc184a..f131fe4c8f 100644 --- a/tests/test.cpp +++ b/tests/test.cpp @@ -55,6 +55,14 @@ std::string wxTheCurrentTestClass, wxTheCurrentTestMethod; #include "wx/socket.h" #include "wx/evtloop.h" +// __WXQT__ guard: see the longer note in tests/net/ipc.cpp. The IPC test (and +// its server) is excluded from wxQt (cross-thread CallAfter() not processed by +// the wxQt event loop, fixed separately on branch +// jpmattia/wxQT-CallAfter-wxWakeUpIdle). +#if wxUSE_THREADS && defined(TEST_HAS_IPC_SERVER) && !defined(__WXQT__) + #include "net/ipc_test_server.h" +#endif + using namespace std; // ---------------------------------------------------------------------------- @@ -340,6 +348,22 @@ public: virtual int OnRun() override { +#if wxUSE_THREADS && defined(TEST_HAS_IPC_SERVER) && !defined(__WXQT__) + // The IPC test re-executes this same binary as its server (with + // WX_IPC_TEST_SERVER set), so test_gui must run the server here too, + // exactly as the console test does in the non-GUI OnRun() below. See the + // note there and tests/net/ipc.cpp for the wxQt exclusion. + if ( wxGetEnv("WX_IPC_TEST_SERVER", nullptr) ) + { + // Suppress the idle-driven test runner: RunIPCServerUntilStopped() + // spins its own event loop, and our OnIdle() would otherwise fire + // there and run the whole test suite inside the server process. + m_runTests = false; + RunIPCServerUntilStopped(); + return 0; + } +#endif // wxUSE_THREADS && TEST_HAS_IPC_SERVER && !__WXQT__ + if ( !IsGUIEnabled() ) return 0; @@ -351,6 +375,18 @@ public: #else // !wxUSE_GUI virtual int OnRun() override { +#if wxUSE_THREADS && defined(TEST_HAS_IPC_SERVER) && !defined(__WXQT__) + // The IPC test starts its server by re-executing this same binary with + // WX_IPC_TEST_SERVER set and then running a bare event loop here. The IPC + // sources and TEST_HAS_IPC_SERVER are built into both the console "test" + // and "test_gui" programs, so the GUI OnRun() above has the same hook. + if ( wxGetEnv("WX_IPC_TEST_SERVER", nullptr) ) + { + RunIPCServerUntilStopped(); + return 0; + } +#endif // wxUSE_THREADS && TEST_HAS_IPC_SERVER && !__WXQT__ + return RunTests(); } #endif // wxUSE_GUI/!wxUSE_GUI diff --git a/tests/test.vcxproj b/tests/test.vcxproj index 7bb62ebf67..bc74679634 100644 --- a/tests/test.vcxproj +++ b/tests/test.vcxproj @@ -972,6 +972,7 @@ + @@ -1002,7 +1003,9 @@ - + + TEST_HAS_IPC_SERVER;%(PreprocessorDefinitions) + diff --git a/tests/test.vcxproj.filters b/tests/test.vcxproj.filters index faf26a4b41..f28564e318 100644 --- a/tests/test.vcxproj.filters +++ b/tests/test.vcxproj.filters @@ -124,6 +124,9 @@ Source Files + + Source Files + Source Files diff --git a/tests/test_gui.vcxproj b/tests/test_gui.vcxproj index fa3051722a..f5d6bee41d 100644 --- a/tests/test_gui.vcxproj +++ b/tests/test_gui.vcxproj @@ -1024,13 +1024,17 @@ + + - + + TEST_HAS_IPC_SERVER;%(PreprocessorDefinitions) + diff --git a/tests/test_gui.vcxproj.filters b/tests/test_gui.vcxproj.filters index 022e374a9b..9fa923390c 100644 --- a/tests/test_gui.vcxproj.filters +++ b/tests/test_gui.vcxproj.filters @@ -233,6 +233,12 @@ Source Files + + Source Files + + + Source Files + Source Files