Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions ApplicationLibCode/Application/RiaGuiApplication.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@
#include <unistd.h> // for usleep
#endif // WIN32

#ifdef ENABLE_SPIX
#include <Spix/AnyRpcServer.h>
#include <Spix/QtWidgetsBot.h>
#endif

//==================================================================================================
///
/// \class RiaGuiApplication
Expand Down Expand Up @@ -213,6 +218,13 @@ RiaGuiApplication::~RiaGuiApplication()
// This must be done before any window deletion that might trigger events
setNotifyInDestructorFlag( true );

#ifdef ENABLE_SPIX
// Tear down the Spix server before the windows it watches. The server destructor
// joins its worker thread, after which the bot can be released safely.
m_spixServer.reset();
m_spixBot.reset();
#endif

processEvents();

if ( m_mainWindow )
Expand Down Expand Up @@ -570,6 +582,17 @@ RiaApplication::ApplicationStatus RiaGuiApplication::handleArguments( gsl::not_n
}
}

#ifdef ENABLE_SPIX
if ( cvf::Option o = progOpt->option( "spix-port" ) )
{
if ( o.valueCount() == 1 )
{
int port = o.value( 0 ).toInt();
if ( port > 0 ) startSpixServer( port );
}
Comment on lines +585 to +592

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Spix port unvalidated 🐞 Bug ≡ Correctness

RiaGuiApplication::handleArguments parses --spix-port with toInt() and only checks >0, so
non-numeric input becomes 0 (silently ignored) and out-of-range ports can attempt startup without a
clear error/exit status when startup fails.
Agent Prompt
### Issue description
`--spix-port` is parsed via `toInt()` and only checked for `> 0`, which means invalid strings silently become `0` (server not started, no message), and invalid ranges can proceed to `startSpixServer()`.

### Issue Context
There is already precedent for strict CLI validation (`--threadcount`) returning `EXIT_WITH_ERROR` and logging a clear message. Spix startup failures are currently swallowed (log-only) and the application keeps running.

### Fix Focus Areas
- ApplicationLibCode/Application/RiaGuiApplication.cpp[568-594]
- ApplicationLibCode/Application/RiaGuiApplication.cpp[1867-1886]

### Suggested fix
- Validate that the value is a valid integer and within `[1, 65535]`.
- If invalid, log an error and return `EXIT_WITH_ERROR` (consistent with `--threadcount`).
- Consider changing `startSpixServer` to return `bool` (or throw) so `handleArguments()` can fail fast when startup fails, instead of continuing silently.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}
#endif

// Code generation
// -----------------
if ( cvf::Option o = progOpt->option( "generate" ) )
Expand Down Expand Up @@ -1840,3 +1863,25 @@ bool RiaGuiApplication::notify( QObject* receiver, QEvent* event )

return done;
}

#ifdef ENABLE_SPIX
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaGuiApplication::startSpixServer( int port )
{
try
{
m_spixBot = std::make_unique<spix::QtWidgetsBot>();
m_spixServer = std::make_unique<spix::AnyRpcServer>( port );
m_spixBot->runTestServer( *m_spixServer );
RiaLogging::info( QString( "Spix UI test automation server listening on port %1" ).arg( port ) );
}
catch ( const std::exception& e )
{
RiaLogging::error( QString( "Failed to start Spix server on port %1: %2" ).arg( port ).arg( e.what() ) );
m_spixServer.reset();
m_spixBot.reset();
}
}
#endif
17 changes: 17 additions & 0 deletions ApplicationLibCode/Application/RiaGuiApplication.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ namespace caf
class FontHolderInterface;
}

#ifdef ENABLE_SPIX
namespace spix
{
class AnyRpcServer;
class QtWidgetsBot;
} // namespace spix
#endif

//==================================================================================================
//
//
Expand Down Expand Up @@ -158,6 +166,10 @@ class RiaGuiApplication : public QApplication, public RiaApplication

void storeTreeViewState();

#ifdef ENABLE_SPIX
void startSpixServer( int port );
#endif

private slots:
void slotWorkerProcessFinished( int exitCode, QProcess::ExitStatus exitStatus );
void onLastWindowClosed();
Expand All @@ -169,4 +181,9 @@ private slots:
std::unique_ptr<RiuRecentFileActionProvider> m_recentFileActionProvider;

std::unique_ptr<RiuMdiMaximizeWindowGuard> m_maximizeWindowGuard;

#ifdef ENABLE_SPIX
std::unique_ptr<spix::QtWidgetsBot> m_spixBot;
std::unique_ptr<spix::AnyRpcServer> m_spixServer;
#endif
};
4 changes: 4 additions & 0 deletions ApplicationLibCode/Application/Tools/RiaArgumentParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ bool RiaArgumentParser::parseArguments( cvf::ProgramOptions* progOpt )
progOpt->registerOption( "console", "", "Launch as a console application without graphics" );
progOpt->registerOption( "server", "[<portnumber>]", "Launch as a GRPC server. Default port is 50051", cvf::ProgramOptions::SINGLE_VALUE );
progOpt->registerOption( "portnumberfile", "[<filename>]", "Write the port number to this file.", cvf::ProgramOptions::SINGLE_VALUE );
progOpt->registerOption( "spix-port",
"<portnumber>",
"Start Spix UI test automation server on the given HTTP port.",
cvf::ProgramOptions::SINGLE_VALUE );

progOpt->registerOption( "threadcount", "<threadcount>", "Set number of threads for parallel processing.\n", cvf::ProgramOptions::SINGLE_VALUE );

Expand Down
4 changes: 4 additions & 0 deletions ApplicationLibCode/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,10 @@ target_link_libraries(
)
target_link_libraries(ApplicationLibCode PRIVATE ResInsightCommonSettings)

if(RESINSIGHT_ENABLE_SPIX)
target_link_libraries(ApplicationLibCode PUBLIC Spix::QtWidgets)
endif()

target_include_directories(
${PROJECT_NAME}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/Commands
Expand Down
4 changes: 4 additions & 0 deletions ApplicationLibCode/UserInterface/RiuMainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ RiuMainWindow::RiuMainWindow()
, m_seismicHistogramPanel( nullptr )
{
setAttribute( Qt::WA_DeleteOnClose );
setObjectName( "RiuMainWindow" );

m_mdiArea = new RiuMdiArea( this );
connect( m_mdiArea, SIGNAL( subWindowActivated( QMdiSubWindow* ) ), SLOT( slotSubWindowActivated( QMdiSubWindow* ) ) );
Expand Down Expand Up @@ -503,6 +504,7 @@ void RiuMainWindow::createMenus()

// Export menu actions
QMenu* exportMenu = fileMenu->addMenu( "&Export" );
exportMenu->setObjectName( "ExportMenu" );
exportMenu->addAction( cmdFeatureMgr->action( "RicSnapshotViewToFileFeature" ) );
exportMenu->addAction( m_snapshotAllViewsToFile );
exportMenu->addAction( cmdFeatureMgr->action( "RicAdvancedSnapshotExportFeature" ) );
Expand All @@ -525,6 +527,7 @@ void RiuMainWindow::createMenus()

fileMenu->addSeparator();
QMenu* testMenu = fileMenu->addMenu( "&Testing" );
testMenu->setObjectName( "TestingMenu" );

// Close and Exit actions
fileMenu->addSeparator();
Expand Down Expand Up @@ -584,6 +587,7 @@ void RiuMainWindow::createMenus()

// Windows menu
m_windowMenu = menuBar()->addMenu( "&Windows" );
m_windowMenu->setObjectName( "WindowsMenu" );
connect( m_windowMenu, SIGNAL( aboutToShow() ), SLOT( slotBuildWindowActions() ) );

// Help menu
Expand Down
4 changes: 4 additions & 0 deletions ApplicationLibCode/UserInterface/RiuMenuBarBuildTools.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ QMenu* RiuMenuBarBuildTools::createDefaultFileMenu( QMenuBar* menuBar )

QMenu* fileMenu = new RiuToolTipMenu( menuBar );
fileMenu->setTitle( "&File" );
fileMenu->setObjectName( "FileMenu" );

menuBar->addMenu( fileMenu );

Expand All @@ -60,6 +61,7 @@ QMenu* RiuMenuBarBuildTools::createDefaultEditMenu( QMenuBar* menuBar )
CVF_ASSERT( menuBar && cmdFeatureMgr );

QMenu* editMenu = menuBar->addMenu( "&Edit" );
editMenu->setObjectName( "EditMenu" );
editMenu->addAction( cmdFeatureMgr->action( "RicSnapshotViewToClipboardFeature" ) );
editMenu->addSeparator();
editMenu->addAction( cmdFeatureMgr->action( "RicShowMemoryCleanupDialogFeature" ) );
Expand All @@ -78,6 +80,7 @@ QMenu* RiuMenuBarBuildTools::createDefaultViewMenu( QMenuBar* menuBar )
CVF_ASSERT( menuBar && cmdFeatureMgr );

QMenu* viewMenu = menuBar->addMenu( "&View" );
viewMenu->setObjectName( "ViewMenu" );
viewMenu->addAction( cmdFeatureMgr->action( "RicViewZoomAllFeature" ) );

return viewMenu;
Expand All @@ -92,6 +95,7 @@ QMenu* RiuMenuBarBuildTools::createDefaultHelpMenu( QMenuBar* menuBar )
CVF_ASSERT( menuBar && cmdFeatureMgr );

QMenu* helpMenu = menuBar->addMenu( "&Help" );
helpMenu->setObjectName( "HelpMenu" );
helpMenu->addAction( cmdFeatureMgr->action( "RicHelpAboutFeature" ) );
helpMenu->addAction( cmdFeatureMgr->action( "RicHelpCommandLineFeature" ) );
helpMenu->addAction( cmdFeatureMgr->action( "RicHelpSummaryCommandLineFeature" ) );
Expand Down
2 changes: 2 additions & 0 deletions ApplicationLibCode/UserInterface/RiuPlotMainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ RiuPlotMainWindow::RiuPlotMainWindow()
, m_autoUpdateEnabled( false )
, m_autoUpdateTimerId( -1 )
{
setObjectName( "RiuPlotMainWindow" );

m_mdiArea = new RiuMdiArea( this );
connect( m_mdiArea, SIGNAL( subWindowActivated( QMdiSubWindow* ) ), SLOT( slotSubWindowActivated( QMdiSubWindow* ) ) );

Expand Down
44 changes: 44 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,50 @@ if(RESINSIGHT_ENABLE_GRPC)
add_definitions(-DENABLE_GRPC)
endif()

# ##############################################################################
# Spix UI test automation
# ##############################################################################
option(RESINSIGHT_ENABLE_SPIX
"Enable Spix UI test automation (HTTP/RPC server)" OFF
)
if(RESINSIGHT_ENABLE_SPIX)
# Spix is fetched as source and built against the project's existing Qt6
# rather than installed via vcpkg, so that vcpkg does not pull in qtbase /
# qtdeclarative. anyrpc is consumed via vcpkg; Spix ships its own
# FindAnyRPC.cmake (module mode) and locates the vcpkg-installed library via
# CMAKE_PREFIX_PATH once add_subdirectory(spix) runs.

set(SPIX_BUILD_EXAMPLES
OFF
CACHE BOOL "" FORCE
)
set(SPIX_BUILD_TESTS
OFF
CACHE BOOL "" FORCE
)
set(SPIX_BUILD_QTQUICK
OFF
CACHE BOOL "" FORCE
)
set(SPIX_BUILD_QTWIDGETS
ON
CACHE BOOL "" FORCE
)

include(FetchContent)
FetchContent_Declare(
spix
GIT_REPOSITORY https://github.com/faaxm/spix.git
GIT_TAG a9c9bb178f6ac00068d4d8dfc3e394337e596cf8 # v0.14
)
FetchContent_MakeAvailable(spix)

add_definitions(-DENABLE_SPIX)
# Spix::QtWidgets is an ALIAS target, so it cannot be added to
# THIRD_PARTY_LIBRARIES (see the set_property(... FOLDER ...) walk later in
# this file). Linkage is wired in ApplicationLibCode/CMakeLists.txt.
endif()

# ##############################################################################
# Unity Build
# ##############################################################################
Expand Down
33 changes: 33 additions & 0 deletions GrpcInterface/Python/rips/tests_spix/test_spix_case_load.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import os
import time
import xmlrpc.client

import pytest


def test_case_load_produces_no_errors():
"""First Spix test beyond the smoke check.

Assumes ResInsight was launched with:

--spix-port <n> --case <TestModels>/TEST10K_FLT_LGR_NNC/TEST10K_FLT_LGR_NNC.EGRID

Verifies the full GUI case-load path completes without errors.
The gRPC rips tests do not exercise this path — they use the
command server rather than the GUI's load codepath.
"""
port = os.environ.get("RESINSIGHT_SPIX_PORT")
if not port:
pytest.skip(
"RESINSIGHT_SPIX_PORT not set; ResInsight must be launched "
"with --spix-port <n> --case <path>/TEST10K_FLT_LGR_NNC.EGRID"
)

proxy = xmlrpc.client.ServerProxy(f"http://localhost:{port}/")

# No widget signal to wait on yet (Qt objectNames not seeded);
# let the async case-load settle before reading errors.
time.sleep(2.0)

errors = proxy.getErrors()
assert errors == [], f"Spix reported errors after case load: {errors}"
29 changes: 29 additions & 0 deletions GrpcInterface/Python/rips/tests_spix/test_spix_main_window.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import os
import time
import xmlrpc.client

import pytest


def test_main_window_addressable():
"""Verifies Spix can locate RiuMainWindow by objectName.

Assumes ResInsight was launched with --spix-port <n>.

This is the first test that depends on widget naming. If it fails,
the regression is most likely in the objectName seeding rather
than in Spix itself.
"""
port = os.environ.get("RESINSIGHT_SPIX_PORT")
if not port:
pytest.skip("RESINSIGHT_SPIX_PORT not set")

proxy = xmlrpc.client.ServerProxy(f"http://localhost:{port}/")

# Let the main window finish showing before probing it.
time.sleep(1.0)

assert proxy.existsAndVisible("RiuMainWindow"), (
"RiuMainWindow not found by Spix — check that "
'RiuMainWindow::RiuMainWindow() calls setObjectName("RiuMainWindow")'
)
47 changes: 47 additions & 0 deletions GrpcInterface/Python/rips/tests_spix/test_spix_menus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import os
import time
import xmlrpc.client

import pytest


def test_main_window_menus_reachable():
"""Verifies window branding and that every seeded top-level menu is
reachable from RiuMainWindow via its objectName.

Exercises Spix's getStringProperty RPC against seven QMenu widgets
plus the QMainWindow itself. QMenus parented to a menubar report
isVisible()==False until popped up, so existsAndVisible would
return False; getStringProperty still works because the QObject
tree lookup ignores visibility.

Assumes ResInsight was launched with --spix-port <n>.
"""
port = os.environ.get("RESINSIGHT_SPIX_PORT")
if not port:
pytest.skip("RESINSIGHT_SPIX_PORT not set")

proxy = xmlrpc.client.ServerProxy(f"http://localhost:{port}/")

time.sleep(1.0)

title = proxy.getStringProperty("RiuMainWindow", "windowTitle")
assert "ResInsight" in title, f"Unexpected window title: {title!r}"

for menu in (
"FileMenu",
"EditMenu",
"ViewMenu",
"WindowsMenu",
"HelpMenu",
"ExportMenu",
"TestingMenu",
):
name = proxy.getStringProperty(f"RiuMainWindow//{menu}", "objectName")
assert name == menu, (
f"Menu '{menu}' not reachable from RiuMainWindow "
f"(getString returned {name!r}). Check setObjectName in "
f"RiuMenuBarBuildTools or RiuMainWindow::createMenus()."
)

assert proxy.getErrors() == []
18 changes: 18 additions & 0 deletions GrpcInterface/Python/rips/tests_spix/test_spix_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import os
import xmlrpc.client

import pytest


def test_spix_server_responds():
port = os.environ.get("RESINSIGHT_SPIX_PORT")
if not port:
pytest.skip(
"RESINSIGHT_SPIX_PORT not set; ResInsight must be launched with --spix-port <n>"
)

proxy = xmlrpc.client.ServerProxy(f"http://localhost:{port}/")
# getErrors() takes no widget path, so this verifies the RPC layer
# is alive without depending on any specific widget's objectName.
errors = proxy.getErrors()
assert isinstance(errors, list)
1 change: 1 addition & 0 deletions vcpkg.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"dependencies": [
"anyrpc",
"arrow",
"boost-filesystem",
"boost-spirit",
Expand Down