mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-24 22:27:55 -05:00
#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:
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 );
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
File renamed without changes.
@@ -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,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))
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user