#14741 Add command-feature test infrastructure

Add the ResInsight-featuretests executable: a headless GUI test harness (RiaFeatureTestModelBuilder, RiaFeatureTestTreeView), a runner that triggers Ric*Feature commands through their QAction (RicFeatureExecutionRunner), a generic execution sweep over every registered feature (RicFeatureSweep-Test, RicFeatureExecutionSweep-Test, RicFeatureSubprocessSweep-Test, RicFeatureSweepDenylist), and supporting CMake wiring. Adds RiaRegressionTestRunner::setRunningRegressionTests() so tests can suppress modal dialogs, and shares RimMockSummaryCase as a common test model under TestModels.
This commit is contained in:
Magne Sjaastad committed 2026-09-15 08:15:59 -04:00
1 parent aad5fca262
commit 7fa9d0c5bf
19 files changed
+1499 -1

No files matched your search

@@ -750,6 +750,14 @@ bool RiaRegressionTestRunner::isRunningRegressionTests() const
return m_runningRegressionTests;
}
//--------------------------------------------------------------------------------------------------
/// Used by test executables to enable the regression-test code paths that suppress modal dialogs.
//--------------------------------------------------------------------------------------------------
void RiaRegressionTestRunner::setRunningRegressionTests( bool enabled )
{
m_runningRegressionTests = enabled;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -41,6 +41,7 @@ public:
void executeRegressionTests();
bool isRunningRegressionTests() const;
void setRunningRegressionTests( bool enabled );
bool useOpenMPForGeometryCreation() const;
static void updateRegressionTest( const QString& testRootPath );
+1
View File
@@ -449,4 +449,5 @@ mark_as_advanced(FORCE RESINSIGHT_INCLUDE_APPLICATION_UNIT_TESTS)
if(RESINSIGHT_INCLUDE_APPLICATION_UNIT_TESTS)
enable_testing()
add_subdirectory(UnitTests)
add_subdirectory(FeatureTests)
endif()
@@ -0,0 +1,196 @@
# ResInsight command-feature tests
#
# Separate executable from ResInsight-tests: the feature sweep executes
# Ric*Feature commands that require a RiaGuiApplication, so this target boots a
# full GUI application (offscreen on CI).
if(MSVC)
# TARGET_RUNTIME_DLLS requires 3.21
cmake_minimum_required(VERSION 3.21)
endif(MSVC)
set(SOURCE_FEATURETEST_FILES
${CMAKE_CURRENT_LIST_DIR}/RiaFeatureTestModelBuilder.h
${CMAKE_CURRENT_LIST_DIR}/RiaFeatureTestModelBuilder.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaFeatureTestTreeView.h
${CMAKE_CURRENT_LIST_DIR}/RiaFeatureTestTreeView.cpp
${CMAKE_CURRENT_LIST_DIR}/RicFeatureSweepDenylist.h
${CMAKE_CURRENT_LIST_DIR}/RicFeatureExecutionRunner.h
${CMAKE_CURRENT_LIST_DIR}/RicFeatureExecutionRunner.cpp
${CMAKE_CURRENT_LIST_DIR}/RicFeatureSweep-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RicFeatureExecutionSweep-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RicFeatureSubprocessSweep-Test.cpp
)
include(${CMAKE_CURRENT_LIST_DIR}/Tests/CMakeLists_files.cmake)
# googletest is fetched by the ApplicationLibCode/UnitTests subdirectory, which
# is added before this one. Only fetch it here if that has not happened (e.g.
# this subdirectory is used standalone).
if(NOT TARGET GTest::gtest)
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.15.2
)
set(BUILD_GMOCK
OFF
CACHE BOOL "" FORCE
)
set(BUILD_GTEST
ON
CACHE BOOL "" FORCE
)
set(INSTALL_GTEST
OFF
CACHE BOOL "" FORCE
)
FetchContent_MakeAvailable(googletest)
endif()
# ##############################################################################
# Copy required Dll/.so to output folder
# ##############################################################################
foreach(riFileName ${RI_FILENAMES})
list(
APPEND
copyFeatureTestCommands
COMMAND
${CMAKE_COMMAND}
-E
copy_if_different
${riFileName}
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
)
endforeach()
add_custom_target(PreBuildFileCopyFeatureTest ${copyFeatureTestCommands})
set_property(
TARGET PreBuildFileCopyFeatureTest PROPERTY FOLDER "FileCopyTargetsTest"
)
qt_add_executable(ResInsight-featuretests ${SOURCE_FEATURETEST_FILES} main.cpp)
add_dependencies(ResInsight-featuretests PreBuildFileCopyFeatureTest)
target_include_directories(
ResInsight-featuretests
PUBLIC ${CMAKE_BINARY_DIR}/Generated
"$<TARGET_PROPERTY:ApplicationLibCode,PUBLIC_INCLUDE_DIRECTORIES>"
${RI_PRIVATE_INCLUDES}
${PROJECT_SOURCE_DIR}/Commands
${PROJECT_SOURCE_DIR}/Commands/CompletionExportCommands
${CMAKE_CURRENT_LIST_DIR}
${CMAKE_CURRENT_LIST_DIR}/../TestModels
)
target_compile_features(ResInsight-featuretests PRIVATE cxx_std_20)
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
if(${CMAKE_SYSTEM_NAME} MATCHES "Linux")
list(APPEND THIRD_PARTY_LIBRARIES rt atomic)
endif()
set(LINK_LIBRARIES
${THIRD_PARTY_LIBRARIES}
${OPENGL_LIBRARIES}
${QT_LIBRARIES}
${OPM_LIBRARIES}
${APP_FWK_LIBRARIES}
${VIZ_FWK_LIBRARIES}
ApplicationLibCode
Commands
RigGeoMechDataModel
RifGeoMechFileInterface
)
if(RESINSIGHT_ENABLE_GRPC)
list(APPEND LINK_LIBRARIES GrpcInterface)
endif()
if(RESINSIGHT_USE_ODB_API)
list(APPEND LINK_LIBRARIES RifOdbReader)
endif()
target_link_libraries(
ResInsight-featuretests PUBLIC ${LINK_LIBRARIES} GTest::gtest
)
if(MSVC)
add_custom_command(
TARGET ResInsight-featuretests
POST_BUILD
COMMAND
${CMAKE_COMMAND} -E copy_if_different
$<TARGET_RUNTIME_DLLS:ResInsight-featuretests>
$<TARGET_FILE_DIR:ResInsight-featuretests>
COMMAND_EXPAND_LISTS
)
set_target_properties(
ResInsight-featuretests PROPERTIES COMPILE_FLAGS "/wd4190"
)
endif(MSVC)
# The test executable boots with the "offscreen" QPA platform plugin (see
# main.cpp and the ctest ENVIRONMENT below). windeployqt only deploys the
# "windows" plugin, so copy the offscreen plugin next to the executable so it
# can be loaded at runtime on developer machines and CI.
if(TARGET Qt6::QOffscreenIntegrationPlugin)
add_custom_command(
TARGET ResInsight-featuretests
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory
$<TARGET_FILE_DIR:ResInsight-featuretests>/platforms
COMMAND
${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:Qt6::QOffscreenIntegrationPlugin>
$<TARGET_FILE_DIR:ResInsight-featuretests>/platforms/
COMMENT
"Deploying the Qt offscreen platform plugin for ResInsight-featuretests"
)
endif()
add_test(NAME ResInsight-featuretests COMMAND ResInsight-featuretests)
set_tests_properties(
ResInsight-featuretests PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen"
TIMEOUT 1800
)
# The execution sweep starts one child process per registered Ric*Feature, which
# takes minutes when run serially. Register it as a set of GoogleTest shards
# instead, so ctest -j runs them in parallel while every feature still gets its
# own child process and its own test case.
#
# Each shard is a full application process and each child is another one, so
# raise the shard count only as far as the machine has memory for. Measured on a
# 32 core machine: 4 shards 103 s, 8 shards 62 s, 20 shards 61 s. Going past 8
# buys no wall clock time but doubles the CPU spent, because the shards then
# become unbalanced. GoogleTest splits by test index while the cost per feature
# varies a lot, so one shard draws the expensive features and the run waits for
# it while the others sit idle.
set(RESINSIGHT_FEATURE_SWEEP_SHARDS
8
CACHE STRING
"Number of parallel shards for the command feature execution sweep"
)
math(EXPR RI_LAST_SWEEP_SHARD "${RESINSIGHT_FEATURE_SWEEP_SHARDS} - 1")
foreach(shardIndex RANGE ${RI_LAST_SWEEP_SHARD})
add_test(NAME ResInsight-featuresweep-${shardIndex}
COMMAND ResInsight-featuretests --gtest_also_run_disabled_tests
--gtest_filter=*FeatureSubprocessExecutionTest*
)
set_tests_properties(
ResInsight-featuresweep-${shardIndex}
PROPERTIES
ENVIRONMENT
"QT_QPA_PLATFORM=offscreen;GTEST_TOTAL_SHARDS=${RESINSIGHT_FEATURE_SWEEP_SHARDS};GTEST_SHARD_INDEX=${shardIndex}"
TIMEOUT
1800
)
endforeach()
@@ -0,0 +1,244 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RiaFeatureTestModelBuilder.h"
#include "RiaApplication.h"
#include "RiaDefines.h"
#include "RiaImportEclipseCaseTools.h"
#include "Polygons/RimPolygon.h"
#include "Polygons/RimPolygonCollection.h"
#include "RimEclipseCase.h"
#include "RimEclipseView.h"
#include "RimGeoMechCase.h"
#include "RimGeoMechModels.h"
#include "RimGeoMechView.h"
#include "RimMockSummaryCase.h"
#include "RimOilField.h"
#include "RimProject.h"
#include "RimSummaryCaseMainCollection.h"
#include "RimTools.h"
#include "RimWellPath.h"
#include "RimWellPathCollection.h"
#include "RifEclipseSummaryAddress.h"
#include "RiaTestDataDirectory.h"
#include "Well/RigWellPath.h"
#include "cvfVector3.h"
#include <QString>
namespace
{
//--------------------------------------------------------------------------------------------------
/// Create a named well path with a simple vertical geometry and add it to the active oil field's
/// well path collection. The geometry has more than two measured depths so features that require a
/// real trajectory (laterals, targets, completions) have something to work with.
//--------------------------------------------------------------------------------------------------
RimWellPath* addWellPathWithGeometry( const QString& name )
{
RimOilField* oilField = RimProject::current()->activeOilField();
if ( !oilField || !oilField->wellPathCollection() ) return nullptr;
auto* wellPath = new RimWellPath;
wellPath->setName( name );
// Set a unit system so features that would otherwise prompt for one (e.g. completion features via
// RicWellPathsUnitSystemSettingsImpl::ensureHasUnitSystem) run without a modal dialog.
wellPath->setUnitSystem( RiaDefines::EclipseUnitSystem::UNITS_METRIC );
cvf::ref<RigWellPath> geometry =
new RigWellPath( { cvf::Vec3d( 0, 0, 0 ), cvf::Vec3d( 0, 0, -500 ), cvf::Vec3d( 0, 0, -1000 ) }, { 0.0, 500.0, 1000.0 } );
wellPath->setWellPathGeometry( geometry.p() );
oilField->wellPathCollection()->addWellPath( wellPath );
return wellPath;
}
//--------------------------------------------------------------------------------------------------
/// Add an in-memory summary case with a single field vector. Does not close the project, so it can be
/// composed with the other builders.
//--------------------------------------------------------------------------------------------------
RimSummaryCase* addSummaryCase( const QString& name )
{
RimOilField* oilField = RimProject::current()->activeOilField();
if ( !oilField || !oilField->summaryCaseMainCollection() ) return nullptr;
auto* mockCase = new RimMockSummaryCase;
mockCase->setName( name );
const std::vector<time_t> timeSteps = { 0, 86400, 172800 };
const std::vector<double> values = { 1.0, 2.0, 3.0 };
mockCase->addVector( RifEclipseSummaryAddress::fieldAddress( "FOPT" ), timeSteps, values );
oilField->summaryCaseMainCollection()->addCase( mockCase );
return mockCase;
}
//--------------------------------------------------------------------------------------------------
/// Load the small VTK (.pvd) GeoMech model from the unit test data. Does not close the project.
/// Returns the case, with its first view in geoMechView when one was created.
//--------------------------------------------------------------------------------------------------
RimGeoMechCase* addGeoMechCase( RimGridView** geoMechView )
{
const QString fileName = QString( "%1/RifVtkReader/model.pvd" ).arg( TEST_DATA_DIR );
if ( !RiaApplication::instance()->openOdbCaseFromFile( fileName ) ) return nullptr;
RimOilField* oilField = RimProject::current()->activeOilField();
if ( !oilField || !oilField->geoMechModels() ) return nullptr;
std::vector<RimGeoMechCase*> cases = oilField->geoMechModels()->cases();
if ( cases.empty() ) return nullptr;
if ( geoMechView && !cases.front()->geoMechViews.empty() )
{
*geoMechView = cases.front()->geoMechViews[0];
}
return cases.front();
}
} // namespace
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaFeatureTestModelBuilder::closeProject()
{
RiaApplication::instance()->closeProject();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
FeatureTestModel RiaFeatureTestModelBuilder::eclipseCaseWithResults()
{
closeProject();
FeatureTestModel model;
// Note: openMockModel returns the new case id as a bool, so a successfully created first case
// (id 0) reports as "false". Do not gate on the return value; query the project for the case.
RiaImportEclipseCaseTools::openMockModel( RiaDefines::mockModelBasicWithResults() );
std::vector<RimEclipseCase*> cases = RimProject::current()->eclipseCases();
if ( !cases.empty() )
{
model.eclipseCase = cases.front();
std::vector<RimEclipseView*> views = cases.front()->reservoirViews();
if ( !views.empty() )
{
model.eclipseView = views.front();
// Many features do not read the selection but the active view (activeReservoirView() /
// activeGridView()). Set it here so those features have a valid context. The active-view
// pointer is a dangling-safe caf::PdmPointer, so it is cleared automatically when the
// project (and thus the view) is closed in closeProject().
RiaApplication::instance()->setActiveReservoirView( model.eclipseView );
}
}
return model;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
FeatureTestModel RiaFeatureTestModelBuilder::wellPath()
{
closeProject();
FeatureTestModel model;
model.wellPath = addWellPathWithGeometry( "TestWellPath" );
return model;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
FeatureTestModel RiaFeatureTestModelBuilder::combinedModel()
{
FeatureTestModel model = eclipseCaseWithResults();
model.wellPath = addWellPathWithGeometry( "TestWellPath" );
return model;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
FeatureTestModel RiaFeatureTestModelBuilder::summaryCase()
{
closeProject();
FeatureTestModel model;
model.summaryCase = addSummaryCase( "TestSummaryCase" );
return model;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
FeatureTestModel RiaFeatureTestModelBuilder::geoMechCase()
{
closeProject();
FeatureTestModel model;
model.geoMechCase = addGeoMechCase( &model.geoMechView );
// Features in this domain read the active view rather than the selection.
if ( model.geoMechView ) RiaApplication::instance()->setActiveReservoirView( model.geoMechView );
return model;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
FeatureTestModel RiaFeatureTestModelBuilder::richModel()
{
// Starts from the Eclipse model, which closes the project and sets the active view.
FeatureTestModel model = eclipseCaseWithResults();
model.wellPath = addWellPathWithGeometry( "TestWellPath" );
model.summaryCase = addSummaryCase( "TestSummaryCase" );
model.geoMechCase = addGeoMechCase( &model.geoMechView );
if ( RimPolygonCollection* polygonCollection = RimTools::polygonCollection() )
{
model.polygon = polygonCollection->appendUserDefinedPolygon();
}
// Loading the GeoMech case makes its view active. Most features expect the Eclipse view, so put
// it back; callers that want the GeoMech view active set it explicitly.
if ( model.eclipseView ) RiaApplication::instance()->setActiveReservoirView( model.eclipseView );
return model;
}
@@ -0,0 +1,74 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
class RimEclipseCase;
class RimGeoMechCase;
class RimGridView;
class RimPolygon;
class RimSummaryCase;
class RimWellPath;
//==================================================================================================
/// Builds small in-memory projects used by the command-feature tests.
///
/// The builders start from a clean project (RiaApplication::closeProject) and populate it with the
/// objects a feature scenario needs, returning pointers into the live project so the caller can set
/// the current caf::SelectionManager selection.
//==================================================================================================
struct FeatureTestModel
{
RimEclipseCase* eclipseCase = nullptr;
RimGridView* eclipseView = nullptr;
RimWellPath* wellPath = nullptr;
RimSummaryCase* summaryCase = nullptr;
RimGeoMechCase* geoMechCase = nullptr;
RimGridView* geoMechView = nullptr;
RimPolygon* polygon = nullptr;
};
class RiaFeatureTestModelBuilder
{
public:
// Close the current project, leaving an empty project in place.
static void closeProject();
// A mock Eclipse case with results and a 3D view (via RiaImportEclipseCaseTools::openMockModel).
static FeatureTestModel eclipseCaseWithResults();
// A single well path added to the active oil field's well path collection.
static FeatureTestModel wellPath();
// A single project containing both a mock Eclipse case (with a 3D view) and a well path, so the
// sweep can select different object types without rebuilding the (expensive) Eclipse model.
static FeatureTestModel combinedModel();
// An in-memory summary case (RimMockSummaryCase) with a single field vector, added to the active
// oil field's summary case main collection. No .SMSPEC file is read.
static FeatureTestModel summaryCase();
// A GeoMech case with a 3D view, loaded from the small VTK (.pvd) model in the unit test data.
// The view is made the active reservoir view. Returns an empty model if the case fails to load.
static FeatureTestModel geoMechCase();
// Everything above in one project: Eclipse case and view, well path, summary case, GeoMech case
// and view, and a polygon. Used by the execution sweep so features from every domain find a
// selection they accept. The Eclipse view is left as the active reservoir view.
static FeatureTestModel richModel();
};
@@ -0,0 +1,55 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RiaFeatureTestTreeView.h"
#include "RiaFeatureCommandContext.h"
#include "RimProject.h"
#include "cafPdmUiTreeView.h"
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiaFeatureTestTreeView::RiaFeatureTestTreeView( caf::PdmUiItem* rootItem )
: m_treeView( std::make_unique<caf::PdmUiTreeView>() )
{
caf::PdmUiItem* root = rootItem ? rootItem : RimProject::current();
m_treeView->setPdmItem( root );
// Register the tree view so RicToggleItemsFeatureImpl::findTreeView() picks it up instead of the
// main window project tree, which is not populated with the models built by these tests.
RiaFeatureCommandContext::instance()->setObject( m_treeView.get() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiaFeatureTestTreeView::~RiaFeatureTestTreeView()
{
RiaFeatureCommandContext::instance()->setObject( nullptr );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
caf::PdmUiTreeView* RiaFeatureTestTreeView::treeView()
{
return m_treeView.get();
}
@@ -0,0 +1,60 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include <memory>
namespace caf
{
class PdmUiItem;
class PdmUiTreeView;
} // namespace caf
//==================================================================================================
/// RAII helper that builds a headless project tree view and registers it as the active tree view
/// via RiaFeatureCommandContext.
///
/// Tree-dependent features (e.g. the RicToggleItems*Feature family) resolve the children of the
/// selected object through RicToggleItemsFeatureImpl::findTreeView(), which first consults
/// RiaFeatureCommandContext for an externally supplied caf::PdmUiTreeView before falling back to the
/// RiuMainWindow project tree. RiaGuiApplication::initialize() does create the main window, but its
/// project tree is not populated with the models these tests build, so findTreeView() does not
/// resolve the selected item. This helper supplies a stand-alone tree view rooted at the project
/// instead, which makes those features testable and keeps the tree deterministic.
///
/// The tree view eagerly builds the full tree ordering for its root at construction, so add every
/// object the test needs before creating the helper. The command-context registration is cleared and
/// the tree view destroyed when the helper goes out of scope.
//==================================================================================================
class RiaFeatureTestTreeView
{
public:
// Roots the tree at the given item, or at RimProject::current() when null. The root item itself
// is not shown as a node; its descendants are, so root above the object you intend to select.
explicit RiaFeatureTestTreeView( caf::PdmUiItem* rootItem = nullptr );
~RiaFeatureTestTreeView();
RiaFeatureTestTreeView( const RiaFeatureTestTreeView& ) = delete;
RiaFeatureTestTreeView& operator=( const RiaFeatureTestTreeView& ) = delete;
caf::PdmUiTreeView* treeView();
private:
std::unique_ptr<caf::PdmUiTreeView> m_treeView;
};
@@ -0,0 +1,295 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RicFeatureExecutionRunner.h"
#include "RiaFeatureTestModelBuilder.h"
#include "RiaViewRedrawScheduler.h"
#include "RicFeatureSweepDenylist.h"
#include "RiaApplication.h"
#include "Polygons/RimPolygon.h"
#include "RimEclipseCase.h"
#include "RimGeoMechCase.h"
#include "RimGridView.h"
#include "RimProject.h"
#include "RimSummaryCase.h"
#include "RimWellPath.h"
#include "cafCmdFeature.h"
#include "cafCmdFeatureManager.h"
#include "cafFactory.h"
#include "cafPdmObjectHandle.h"
#include "cafPdmPointer.h"
#include "cafPdmUiItem.h"
#include "cafPdmUiObjectHandle.h"
#include "cafPdmXmlObjectHandle.h"
#include "cafSelectionManager.h"
#include <QAction>
#include <QApplication>
#include <QTimer>
#include <QWidget>
#include <iostream>
#include <set>
#include <vector>
namespace
{
// At most this many selections are executed per feature. A feature that is enabled for a whole class
// of objects would otherwise run dozens of times, which costs runtime and multiplies destructive side
// effects without finding much more. Hitting the cap is logged rather than passing silently.
constexpr size_t maxExecutionsPerFeature = 3;
//--------------------------------------------------------------------------------------------------
/// Closes any modal dialog that appears while a feature is executing.
///
/// A feature that opens a modal dialog is treated as a success (it ran up to the dialog without
/// crashing); the dialog is simply dismissed so the run continues.
//--------------------------------------------------------------------------------------------------
class ModalDialogWatchdog
{
public:
ModalDialogWatchdog()
{
m_timer.setInterval( 200 );
QObject::connect( &m_timer,
&QTimer::timeout,
[]()
{
if ( QWidget* modal = QApplication::activeModalWidget() )
{
modal->close();
}
} );
m_timer.start();
}
~ModalDialogWatchdog() { m_timer.stop(); }
private:
QTimer m_timer;
};
// The combined model is reused across features to avoid the expensive Eclipse rebuild. It is only
// rebuilt when a previously executed feature broke it: closed the project or deleted one of the
// objects the scenarios select (detected via the dangling-safe caf::PdmPointer guards).
FeatureTestModel s_model;
caf::PdmPointer<caf::PdmObjectHandle> s_eclipseCaseGuard;
caf::PdmPointer<caf::PdmObjectHandle> s_eclipseViewGuard;
caf::PdmPointer<caf::PdmObjectHandle> s_wellPathGuard;
bool s_modelBuilt = false;
void collectSelectionCandidates();
void rebuildModel()
{
caf::SelectionManager::instance()->clearAll();
s_model = RiaFeatureTestModelBuilder::richModel();
s_eclipseCaseGuard = s_model.eclipseCase;
s_eclipseViewGuard = s_model.eclipseView;
s_wellPathGuard = s_model.wellPath;
s_modelBuilt = true;
collectSelectionCandidates();
}
bool modelNeedsRebuild()
{
if ( !s_modelBuilt ) return true;
if ( RimProject::current() == nullptr ) return true;
if ( s_eclipseCaseGuard.isNull() ) return true;
if ( s_eclipseViewGuard.isNull() ) return true;
if ( s_wellPathGuard.isNull() ) return true;
return false;
}
// One object per distinct class in the project, held through dangling-safe pointers so a feature that
// deletes objects does not leave the list pointing at freed memory.
std::vector<caf::PdmPointer<caf::PdmObjectHandle>> s_candidates;
//--------------------------------------------------------------------------------------------------
/// Collect the objects to use as selections, by walking the project.
///
/// Command features dispatch on the type of the selected object, so one instance per class is enough
/// and keeps the list to a size that can be tried exhaustively for every feature. Walking the tree
/// rather than hand-picking a few objects is what reaches the collections, sub-collections and
/// definition objects that most features are actually written against.
//--------------------------------------------------------------------------------------------------
void collectSelectionCandidates()
{
s_candidates.clear();
RimProject* project = RimProject::current();
if ( !project ) return;
std::set<QString> seenClasses;
for ( caf::PdmObjectHandle* object : project->descendantsIncludingThisOfType<caf::PdmObjectHandle>() )
{
if ( !object ) continue;
// Ask for the capabilities directly. The uiCapability() and xmlCapability() accessors assert
// when the capability is missing rather than returning null, so they cannot be used to test
// for it, and not every object in the project has both.
auto* xmlCapability = object->capability<caf::PdmXmlObjectHandle>();
auto* uiCapability = object->capability<caf::PdmUiObjectHandle>();
if ( !xmlCapability || !uiCapability ) continue;
if ( seenClasses.insert( xmlCapability->classKeyword() ).second )
{
s_candidates.push_back( object );
}
}
}
//--------------------------------------------------------------------------------------------------
/// Select a candidate object, and make the view it belongs to the active one.
///
/// Many features read the active view instead of the selection, so the two are kept consistent:
/// selecting something inside the GeoMech view must not leave the Eclipse view active.
//--------------------------------------------------------------------------------------------------
bool applySelection( caf::PdmObjectHandle* object )
{
caf::SelectionManager::instance()->clearAll();
if ( !object ) return false;
auto* uiCapability = object->capability<caf::PdmUiObjectHandle>();
if ( !uiCapability ) return false;
RimGridView* activeView = object->firstAncestorOrThisOfType<RimGridView>();
if ( !activeView ) activeView = s_model.eclipseView;
RiaApplication::instance()->setActiveReservoirView( activeView );
caf::SelectionManager::instance()->setSelectedItem( uiCapability );
return true;
}
} // namespace
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RicFeatureExecutionRunner::executeFeature( const std::string& commandId )
{
if ( isFeatureDenylisted( commandId ) ) return true;
caf::CmdFeature* feature = caf::CmdFeatureManager::instance()->getCommandFeature( commandId );
if ( !feature ) return false;
if ( modelNeedsRebuild() ) rebuildModel();
size_t executionCount = 0;
for ( size_t i = 0; i < s_candidates.size(); ++i )
{
// A previously executed feature may have deleted objects or closed the project. Rebuilding
// invalidates the candidate list, so rebuild it too and restart from the same index.
if ( modelNeedsRebuild() ) rebuildModel();
caf::PdmObjectHandle* candidate = s_candidates[i].p();
if ( !candidate ) continue;
auto* candidateXml = candidate->capability<caf::PdmXmlObjectHandle>();
if ( !candidateXml ) continue;
const QString candidateName = candidateXml->classKeyword();
if ( !applySelection( candidate ) ) continue;
// The enabled state depends on the selection just applied. Note this call is itself part of
// what is being tested: a feature whose isCommandEnabled() crashes is a defect too.
if ( !feature->canFeatureBeExecuted() ) continue;
if ( executionCount >= maxExecutionsPerFeature )
{
std::cout << "[capped] " << commandId << " is enabled for more than " << maxExecutionsPerFeature
<< " object types; remaining ones are not executed" << std::endl;
break;
}
++executionCount;
// Attribution line: printed and flushed before execution so a hard crash in a child process
// still identifies the culprit in the captured output.
std::cout << "[exec] " << candidateName.toStdString() << " : " << commandId << std::endl;
ModalDialogWatchdog watchdog;
// Trigger through the QAction rather than calling actionTriggered() directly. Features that
// read caf::CmdFeature::userData() get it from qobject_cast<QAction*>( sender() ), which
// asserts when the slot is invoked as a plain function call. Going through the action is also
// how the application itself invokes a feature.
if ( QAction* action = feature->action() )
{
action->trigger();
}
else
{
feature->actionTriggered( false );
}
// Deliberately do NOT pump the event loop here.
//
// RiaGuiApplication::initialize() creates and shows the main window, so the 3D viewer widgets
// exist and Qt has paint events queued for them. Offscreen there is no OpenGL context, and
// caf::Viewer::paintGL() dereferences the null context, which shows up as an access violation
// blamed on whichever feature happened to run. Measured: with processEvents() the cell-filter
// features all crash; without it they pass. Discarding the scheduled redraws is not enough,
// because the paint events come from Qt rather than from RiaViewRedrawScheduler.
//
// Exercising real rendering requires the software-GL tier, not this one, so drop the pending
// redraws and leave the event loop alone.
RiaViewRedrawScheduler::instance()->clearViewsScheduledForUpdate();
// Force a rebuild before the next selection, so every execution starts from an identical
// model. Without this, a destructive feature leaves debris that makes a later execution fail
// for reasons that have nothing to do with the object then selected: deleting an intersection
// result definition and then a GeoMech view aborts, while deleting the view on a clean model
// is fine. Order dependent failures like that are false reports, and the guards below cannot
// catch them because they only track the few objects they name.
s_modelBuilt = false;
if ( RimProject::current() == nullptr ) return false;
}
// Phase marker: everything above is feature execution, everything after is harness teardown. A
// child that crashes without printing this crashed inside the feature; one that crashes after it
// crashed while closing the project, which is a harness problem and not a feature defect.
std::cout << executionCompleteMarker << " " << commandId << std::endl;
return true;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<std::string> RicFeatureExecutionRunner::allRegisteredFeatureIds()
{
return caf::Factory<caf::CmdFeature, std::string>::instance()->allKeys();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RicFeatureExecutionRunner::releaseModel()
{
caf::SelectionManager::instance()->clearAll();
RiaFeatureTestModelBuilder::closeProject();
s_modelBuilt = false;
}
@@ -0,0 +1,58 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include <string>
#include <vector>
//==================================================================================================
/// Executes a single Ric*Feature against synthetic selections.
///
/// Shared by two callers:
/// - the in-process sweep (RicFeatureExecutionSweep-Test.cpp), which runs every feature in this
/// process and is therefore killed by the first feature that aborts,
/// - the subprocess sweep (RicFeatureSubprocessSweep-Test.cpp), which re-runs this executable once
/// per feature via the --feature-exec command line switch, so an abort or hang only takes down
/// that one child process.
//==================================================================================================
namespace RicFeatureExecutionRunner
{
// Command line switch used to run a single feature and exit; see main.cpp.
constexpr const char* featureExecSwitch = "--feature-exec=";
// Printed by the child once the feature has been executed under every applicable scenario, before
// any harness teardown. Lets the parent tell a crash inside the feature (marker absent) from a crash
// while closing the project afterwards (marker present).
constexpr const char* executionCompleteMarker = "[feature-execution-complete]";
// Execute the feature under every scenario where it reports itself enabled.
//
// Returns false only for conditions the caller can meaningfully report: an unknown command id, or a
// feature that left the project in an invalid state. A feature that aborts (CVF_ASSERT / CAF_ASSERT)
// or hangs never returns at all - that is precisely what subprocess isolation is for. Denylisted
// features return true without being executed.
bool executeFeature( const std::string& commandId );
// The command ids of all registered Ric*Feature commands. Shared by both sweeps: defining it in each
// of them breaks the unity build, where the two files end up in one translation unit.
std::vector<std::string> allRegisteredFeatureIds();
// Drop the shared model and clear the selection. Call when a suite of features has been executed.
void releaseModel();
} // namespace RicFeatureExecutionRunner
@@ -0,0 +1,67 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "gtest/gtest.h"
#include "RicFeatureExecutionRunner.h"
#include "RicFeatureSweepDenylist.h"
#include "RimProject.h"
#include <string>
#include <vector>
//--------------------------------------------------------------------------------------------------
/// Executes each feature in this process against synthetic selections, asserting that execution does
/// not crash and leaves a valid project.
///
/// DISABLED by default, and it cannot be made a gate. Many features assume the framework only invokes
/// them in a valid GUI selection context (e.g. a right-click on a specific object). Executing them
/// with a synthetic selection can violate that assumption and trip a CVF_ASSERT / CAF_ASSERT, which
/// aborts the process rather than throwing - so a single offender ends the whole run and hides every
/// feature after it.
///
/// Prefer FeatureSubprocessExecutionTest (RicFeatureSubprocessSweep-Test.cpp), which runs each
/// feature in its own child process and therefore isolates aborts and hangs. This in-process variant
/// is kept because it is much faster when you already know the feature under test is well behaved:
/// --gtest_also_run_disabled_tests --gtest_filter=*FeatureExecutionTest*
//--------------------------------------------------------------------------------------------------
class FeatureExecutionTest : public ::testing::TestWithParam<std::string>
{
public:
static void TearDownTestSuite() { RicFeatureExecutionRunner::releaseModel(); }
};
TEST_P( FeatureExecutionTest, DISABLED_ExecuteWhenEnabledDoesNotCrash )
{
const std::string& commandId = GetParam();
if ( isFeatureDenylisted( commandId ) )
{
GTEST_SKIP() << "Denylisted: " << featureSweepDenylist().at( commandId );
}
EXPECT_TRUE( RicFeatureExecutionRunner::executeFeature( commandId ) ) << commandId << " left the project in an invalid state";
EXPECT_TRUE( RimProject::current() != nullptr );
}
INSTANTIATE_TEST_SUITE_P( AllFeatures,
FeatureExecutionTest,
::testing::ValuesIn( RicFeatureExecutionRunner::allRegisteredFeatureIds() ),
[]( const testing::TestParamInfo<std::string>& info ) { return info.param; } );
@@ -0,0 +1,114 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "gtest/gtest.h"
#include "RicFeatureExecutionRunner.h"
#include "RicFeatureSweepDenylist.h"
#include "cafCmdFeature.h"
#include "cafFactory.h"
#include <QCoreApplication>
#include <QProcess>
#include <QString>
#include <QStringList>
#include <string>
#include <vector>
namespace
{
// Generous enough for a feature that loads the mock Eclipse model in a cold process, short enough
// that a genuinely hung feature does not stall the sweep.
constexpr int featureTimeoutMs = 120000;
} // namespace
//--------------------------------------------------------------------------------------------------
/// Executes every feature in its own child process, so a feature that aborts or hangs is isolated
/// and attributed to its own test case.
///
/// The in-process sweep (RicFeatureExecutionSweep-Test.cpp) cannot be a gate: features that assume a
/// valid GUI selection context trip CVF_ASSERT/CAF_ASSERT, which abort the process rather than throw,
/// so the first offender ends the whole run and hides every feature after it. Here the parent re-runs
/// this same executable with --feature-exec=<id> (see main.cpp), which executes exactly one feature
/// and exits. The parent then classifies the child:
///
/// - exit code 0 -> feature ran (or was never enabled / is denylisted)
/// - non-zero exit or crash -> the feature aborted or crashed
/// - no exit within the timeout -> the feature hung (blocking dialog, external process, network)
///
/// DISABLED by default: one cold process per feature is far too slow for the fast suite. Run it
/// deliberately, e.g. in a nightly job:
/// ResInsight-featuretests --gtest_also_run_disabled_tests --gtest_filter=*FeatureSubprocess*
/// A failure names the offending feature id, which is either a real bug to fix or a new denylist
/// entry with a documented reason.
//--------------------------------------------------------------------------------------------------
class FeatureSubprocessExecutionTest : public ::testing::TestWithParam<std::string>
{
};
TEST_P( FeatureSubprocessExecutionTest, DISABLED_ExecuteInIsolationDoesNotCrash )
{
const std::string& commandId = GetParam();
if ( isFeatureDenylisted( commandId ) )
{
GTEST_SKIP() << "Denylisted: " << featureSweepDenylist().at( commandId );
}
const QString exePath = QCoreApplication::applicationFilePath();
ASSERT_FALSE( exePath.isEmpty() );
QProcess child;
QStringList arguments;
arguments << QString::fromStdString( RicFeatureExecutionRunner::featureExecSwitch + commandId );
child.setProcessChannelMode( QProcess::MergedChannels );
child.start( exePath, arguments );
ASSERT_TRUE( child.waitForStarted( 30000 ) ) << "Could not start child process for " << commandId;
if ( !child.waitForFinished( featureTimeoutMs ) )
{
child.kill();
child.waitForFinished( 5000 );
FAIL() << commandId << " did not finish within " << featureTimeoutMs << " ms (hung).\n"
<< "Output:\n"
<< child.readAll().toStdString();
}
const QString output = QString::fromUtf8( child.readAll() );
// The marker separates feature execution from harness teardown, so a crash is attributed to the
// right place instead of blaming the feature for a teardown problem.
const bool featureExecutionCompleted = output.contains( RicFeatureExecutionRunner::executionCompleteMarker );
ASSERT_EQ( QProcess::NormalExit, child.exitStatus() )
<< commandId
<< ( featureExecutionCompleted ? " crashed during harness teardown (not a feature defect)" : " crashed while executing the feature" )
<< ".\nOutput:\n"
<< output.toStdString();
EXPECT_EQ( 0, child.exitCode() ) << commandId << " exited with a failure code.\nOutput:\n" << output.toStdString();
}
INSTANTIATE_TEST_SUITE_P( AllFeatures,
FeatureSubprocessExecutionTest,
::testing::ValuesIn( RicFeatureExecutionRunner::allRegisteredFeatureIds() ),
[]( const testing::TestParamInfo<std::string>& info ) { return info.param; } );
@@ -0,0 +1,97 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "gtest/gtest.h"
#include "cafCmdFeature.h"
#include "cafCmdFeatureManager.h"
#include "cafFactory.h"
#include "cafSelectionManager.h"
#include <string>
#include <vector>
//--------------------------------------------------------------------------------------------------
/// The command ids of all registered Ric*Feature commands.
///
/// gtest evaluates INSTANTIATE_TEST_SUITE_P generators lazily (via the generated _EvalGenerator_
/// function) at the start of RUN_ALL_TESTS, i.e. after the singletons have been created in main().
/// The factory is therefore fully populated when this is called, so there is no static-init-order
/// problem with reading it here.
//--------------------------------------------------------------------------------------------------
static std::vector<std::string> allRegisteredFeatureIds()
{
return caf::Factory<caf::CmdFeature, std::string>::instance()->allKeys();
}
//--------------------------------------------------------------------------------------------------
/// All Ric*Feature command ids are registered and were instantiated by the CmdFeatureManager.
///
/// This is the M1 de-risk test: booting the feature-test executable creates a RiaGuiApplication
/// (offscreen on CI) and a caf::CmdFeatureManager, whose constructor instantiates every feature
/// registered via CAF_CMD_SOURCE_INIT. Reaching this test therefore already proves the full set of
/// features survives linking into this target and can be constructed. Here we assert the expected
/// number of registrations is present.
///
/// Note: this test deliberately does not call getCommandFeature()/refreshEnabledState() for every
/// feature, because that path invokes isCommandEnabled() with an empty selection. That behaviour is
/// exercised separately and per-feature by FeatureEnabledState.* so that a crash in one feature does
/// not mask the rest.
//--------------------------------------------------------------------------------------------------
TEST( FeatureSweep, AllFeaturesRegistered )
{
auto* factory = caf::Factory<caf::CmdFeature, std::string>::instance();
ASSERT_TRUE( factory != nullptr );
std::vector<std::string> commandIds = factory->allKeys();
EXPECT_GT( commandIds.size(), 400u ) << "Expected the full set of Ric*Feature commands to be registered";
ASSERT_TRUE( caf::CmdFeatureManager::instance() != nullptr );
}
//--------------------------------------------------------------------------------------------------
/// Querying a feature's enabled state with an empty selection must not crash.
///
/// isCommandEnabled() is called by the framework whenever it refreshes actions (menus, toolbars,
/// keyboard-shortcut dispatch). Many features read the current selection to decide; if they
/// dereference the result of caf::SelectionManager queries without a null check, they crash when
/// nothing (or the wrong type) is selected. This parameterized test runs one case per feature so a
/// crash in one feature is reported against that feature id and does not hide the others.
//--------------------------------------------------------------------------------------------------
class FeatureEnabledStateTest : public ::testing::TestWithParam<std::string>
{
protected:
void SetUp() override { caf::SelectionManager::instance()->clearAll(); }
void TearDown() override { caf::SelectionManager::instance()->clearAll(); }
};
TEST_P( FeatureEnabledStateTest, EmptySelectionDoesNotCrash )
{
const std::string& commandId = GetParam();
caf::CmdFeature* feature = caf::CmdFeatureManager::instance()->getCommandFeature( commandId );
ASSERT_TRUE( feature != nullptr );
// Must not crash / must not throw with an empty selection.
EXPECT_NO_FATAL_FAILURE( (void)feature->canFeatureBeExecuted() );
}
INSTANTIATE_TEST_SUITE_P( AllFeatures,
FeatureEnabledStateTest,
::testing::ValuesIn( allRegisteredFeatureIds() ),
[]( const testing::TestParamInfo<std::string>& info ) { return info.param; } );
@@ -0,0 +1,86 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include <map>
#include <string>
//--------------------------------------------------------------------------------------------------
/// Command ids that the execution sweep must not trigger via onActionTriggered().
///
/// The sweep protects against modal dialogs with a watchdog that closes any that appear, so the list
/// below focuses on features that block or cause side effects the watchdog cannot handle: process or
/// application termination, launching external processes, blocking network calls, and recursively
/// invoking the test/regression machinery. Modal file/preferences dialogs are included as well to
/// avoid the per-call watchdog delay. Every entry has a reason.
//--------------------------------------------------------------------------------------------------
inline const std::map<std::string, std::string>& featureSweepDenylist()
{
static const std::map<std::string, std::string> deny = {
// Application / process lifecycle
{ "RicExitApplicationFeature", "quits the application, terminating the test process" },
{ "RicCloseProjectFeature", "tears down the project mid-sweep; covered by curated tests" },
{ "RicOpenProjectFeature", "opens a QFileDialog to pick a project file" },
{ "RicOpenLastUsedFileFeature", "loads an arbitrary file from user settings" },
{ "RicSaveProjectFeature", "may open a save QFileDialog and writes to disk" },
{ "RicSaveProjectAsFeature", "opens a save QFileDialog" },
{ "RicSaveProjectNoGlobalPathsFeature", "opens a save QFileDialog" },
// External processes
{ "RicExecuteScriptFeature", "launches an external Octave/Python process" },
{ "RicExecuteScriptForCasesFeature", "launches an external process" },
{ "RicNewOctaveScriptFeature", "opens an external script editor process" },
{ "RicNewPythonScriptFeature", "opens an external script editor process" },
{ "RicOpenInTextEditorFeature", "launches an external text editor process" },
// Blocking network access
{ "RicHoloLensCreateSessionFeature", "opens a network session to the sharing server" },
{ "RicHoloLensCreateDummyFiledBackedSessionFeature", "starts a HoloLens session" },
{ "RicHoloLensExportToSharingServerFeature", "uploads over the network" },
{ "RicHoloLensAutoExportToSharingServerFeature", "uploads over the network" },
{ "RicHoloLensExportToFolderFeature", "opens a folder dialog and exports" },
{ "RicHoloLensTerminateSessionFeature", "acts on a live network session" },
{ "RicDeleteOsduTokenFeature", "modifies stored OSDU credentials" },
{ "RicDeleteSumoTokenFeature", "modifies stored Sumo credentials" },
// Recursive test / regression machinery
{ "RicLaunchRegressionTestsFeature", "recursively runs the regression-test suite" },
{ "RicLaunchRegressionTestDialogFeature", "opens the regression-test dialog" },
// Modal editors and preference dialogs
{ "RicEditPreferencesFeature", "opens the modal preferences dialog" },
{ "RicThemeColorEditorFeature", "opens a modal color-editor dialog" },
{ "RicShowMemoryCleanupDialogFeature", "opens a modal memory-cleanup dialog" },
// Requires a rendering 3D viewer. The offscreen platform plugin creates the viewer widget but
// no OpenGL context, so drawing dereferences a null context in caf::Viewer::paintGL(). These
// work in the real application and belong in a software-OpenGL test tier.
{ "RicTogglePerspectiveViewFeature", "changes the projection of the active viewer, which needs an OpenGL context" },
{ "RicNewContourMapViewFeature", "creates and draws a new 3D view, which needs an OpenGL context" },
};
return deny;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
inline bool isFeatureDenylisted( const std::string& commandId )
{
return featureSweepDenylist().find( commandId ) != featureSweepDenylist().end();
}
+118
View File
@@ -0,0 +1,118 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "gtest/gtest.h"
#include "RiaGuiApplication.h"
#include "RiaQuantityInfoTools.h"
#include "RiaRegressionTestRunner.h"
#include "RicFeatureExecutionRunner.h"
#include "cafCmdFeatureManager.h"
#include "cafPdmDefaultObjectFactory.h"
#include "cvfAssert.h"
#include <QApplication>
#include <QLocale>
#include <string>
#ifdef WIN32
#include <windows.h>
#include <cstdlib>
#endif
//--------------------------------------------------------------------------------------------------
/// Entry point for the command-feature test executable.
///
/// Unlike ResInsight-tests (which boots a headless RiaConsoleApplication), the feature sweep
/// executes Ric*Feature commands that call RiaGuiApplication::instance(). We therefore boot a full
/// RiaGuiApplication. On CI this is run with QT_QPA_PLATFORM=offscreen so no display is required.
//--------------------------------------------------------------------------------------------------
int main( int argc, char** argv )
{
// Default to the offscreen platform plugin unless the environment already selected one. This
// lets the executable run on headless CI without an X server / display.
if ( qEnvironmentVariableIsEmpty( "QT_QPA_PLATFORM" ) )
{
qputenv( "QT_QPA_PLATFORM", "offscreen" );
}
// Route failed CVF asserts to the console instead of the native "Assertion Failed" message box.
// That message box is a native (non-Qt) modal dialog the sweep watchdog cannot close, which would
// hang the run when a feature is executed with a selection that violates its preconditions.
cvf::Assert::setReportMode( cvf::Assert::CONSOLE );
#ifdef WIN32
// Keep the process from popping Windows Error Reporting / CRT abort dialogs during the sweep so a
// crashing or asserting feature fails deterministically instead of blocking on a dialog.
SetErrorMode( SEM_NOGPFAULTERRORBOX | SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX );
_set_abort_behavior( 0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT );
#endif
// Create the singletons the features depend on before the application object is created, in the
// same order as RiaMainTools::initializeSingletons().
caf::CmdFeatureManager::createSingleton();
RiaRegressionTestRunner::createSingleton();
caf::PdmDefaultObjectFactory::createSingleton();
RiaQuantityInfoTools::initializeSummaryKeywords();
RiaGuiApplication app( argc, argv );
app.initialize();
// Enable the regression-test code paths that suppress modal error dialogs, so features that
// would otherwise pop a QMessageBox during the sweep log to the console instead of blocking.
RiaRegressionTestRunner::instance()->setRunningRegressionTests( true );
QLocale::setDefault( QLocale( QLocale::English, QLocale::UnitedStates ) );
setlocale( LC_NUMERIC, "C" );
// Child mode for the subprocess sweep: execute a single feature and exit without running any
// gtest cases. The parent (RicFeatureSubprocessSweep-Test.cpp) spawns one child per feature, so a
// feature that aborts on a failed CVF_ASSERT/CAF_ASSERT, or hangs, only takes down its own
// process and is attributed to that feature id.
const std::string execSwitch = RicFeatureExecutionRunner::featureExecSwitch;
for ( int i = 1; i < argc; ++i )
{
const std::string arg = argv[i];
if ( arg.rfind( execSwitch, 0 ) == 0 )
{
// Unbuffered stdout: a failing CAF_ASSERT prints the file and line with std::printf and
// then calls std::abort(), which does not flush. Buffered output would drop exactly the
// message identifying where the child died.
setvbuf( stdout, nullptr, _IONBF, 0 );
const std::string commandId = arg.substr( execSwitch.size() );
const bool ok = RicFeatureExecutionRunner::executeFeature( commandId );
// Close the project before RiaGuiApplication is destroyed. Tearing down a live project
// (cases, views) during application destruction crashes, which would make every child
// look like a crashing feature.
RicFeatureExecutionRunner::releaseModel();
return ok ? 0 : 1;
}
}
testing::InitGoogleTest( &argc, argv );
int result = RUN_ALL_TESTS();
return result;
}
+2 -1
View File
@@ -169,7 +169,7 @@ set(SOURCE_UNITTEST_FILES
${CMAKE_CURRENT_LIST_DIR}/RigCompletionDataGridCell-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/cafVecIjk-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaSummaryAddressCollectionTools-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RimMockSummaryCase.h
${CMAKE_CURRENT_LIST_DIR}/../TestModels/RimMockSummaryCase.h
${CMAKE_CURRENT_LIST_DIR}/RimMockSummaryCase-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RimSummaryCalculation-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RifMultipleSummaryReaders-Test.cpp
@@ -282,6 +282,7 @@ target_include_directories(
${RI_PRIVATE_INCLUDES}
${PROJECT_SOURCE_DIR}/Commands
${PROJECT_SOURCE_DIR}/Commands/CompletionExportCommands
${CMAKE_CURRENT_LIST_DIR}/../TestModels
)
target_compile_features(ResInsight-tests PRIVATE cxx_std_20)
+22
View File
@@ -22,6 +22,27 @@ python -m ruff format test_polygons.py
python -m ruff check --fix test_polygons.py
```
### CMake Formatting
CMake files (`CMakeLists.txt`, `*.cmake`) are formatted with `cmake-format` from the
[`cmakelang`](https://pypi.org/project/cmakelang/) package, using the repository configuration
`cmake/cmake-format.py`. The `cmake-format` GitHub workflow enforces this on every push and opens a
"Fixes by cmake-format" pull request when a file is not formatted.
- **If `cmakelang` is available**, run `cmake-format` in place on any CMake file you changed, so the
formatting fix lands in your commit instead of a follow-up bot PR:
```bash
cmake-format -c cmake/cmake-format.py -i ApplicationLibCode/FeatureTests/CMakeLists.txt
```
Always pass `-c cmake/cmake-format.py` (the repo config) and `-i` (edit in place). Install with
`python -m pip install --user cmakelang` if it is missing; if installing is not possible, skip this
step and rely on the CI workflow.
- On Windows, `cmake-format` writes LF line endings, so `git status` may show a formatted file as
modified even when the content is unchanged. Use `git diff --quiet -- <file>` to confirm whether
there is any real formatting drift.
## Copyright Headers
- New files must use the **current year** (the year the file is created) in the copyright header
@@ -279,3 +300,4 @@ When creating commits:
- Use issue number at the start of the title: `#12773 Python: Add API for creating valve templates`
- Follow git conventions for commit messages
- Always run python formatting/check on changed files before commits
- When you change a CMake file and `cmakelang` is available, run `cmake-format` on it before committing (see [CMake Formatting](#cmake-formatting))
+1
View File
@@ -75,6 +75,7 @@ ResInsight includes Python integration via gRPC when `RESINSIGHT_ENABLE_GRPC=ON`
- When creating a git commit for a feature request use the issue number at the start of the title, e.g "#12773 Python: Add API for creating valve templates"
- When creating a commit use git conventions
- Always run python formatting/check on changed files before commits
- When you change a CMake file and `cmakelang` is available, run `cmake-format -c cmake/cmake-format.py -i <file>` before committing (see `docs/agents/coding-style.md`)
### PR Conventions
- Do not include a test plan in the PR description