Files
ResInsight/ApplicationLibCode/UnitTests/RimDataFilterCollection-Test.cpp
T
Magne Sjaastad 9911c49d2b #14710 Support ensemble data filters in ensemble contour maps
Replace the built-in formation filter with ensemble data filters. Load ensemble data filters after project read, and parent new views before assigning the case so the ensemble filter collection is resolved correctly.
2026-09-28 10:30:12 +02:00

306 lines
12 KiB
C++

/////////////////////////////////////////////////////////////////////////////////
//
// 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 "RigActiveCellInfo.h"
#include "RimCellFilter.h"
#include "RimCellRangeFilter.h"
#include "RimCombinedFilter.h"
#include "RimDataFilterCollection.h"
#include "RimDataFilterInView.h"
#include "RimDataFilterInViewCollection.h"
#include "RimEclipseResultCase.h"
#include "RimEclipseView.h"
#include "RimEclipseViewCollection.h"
#include "RimReservoirGridEnsemble.h"
#include "cafSignal.h"
#include <memory>
namespace
{
//--------------------------------------------------------------------------------------------------
/// Lightweight slot owner used to capture caf::Signal emissions in tests.
//--------------------------------------------------------------------------------------------------
class SignalCounter : public caf::SignalObserver
{
public:
int count = 0;
void onSignal( const caf::SignalEmitter* /*emitter*/ ) { ++count; }
};
} // namespace
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RimDataFilterCollection, emptyCollectionHasNoActiveFilters )
{
RimDataFilterCollection coll;
EXPECT_EQ( size_t{ 0 }, coll.count() );
EXPECT_TRUE( coll.isEmpty() );
EXPECT_TRUE( coll.filters().empty() );
EXPECT_FALSE( coll.hasActiveFilters() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RimDataFilterCollection, addFilterIncrementsCount )
{
RimDataFilterCollection coll;
auto* a = new RimCellRangeFilter();
coll.addFilter( a );
EXPECT_EQ( size_t{ 1 }, coll.count() );
ASSERT_EQ( size_t{ 1 }, coll.filters().size() );
EXPECT_EQ( a, coll.filters().front() );
auto* b = new RimCellRangeFilter();
coll.addFilter( b );
EXPECT_EQ( size_t{ 2 }, coll.count() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RimDataFilterCollection, removeFilterDeletesAndDecrements )
{
RimDataFilterCollection coll;
auto* a = new RimCellRangeFilter();
coll.addFilter( a );
EXPECT_EQ( size_t{ 1 }, coll.count() );
coll.removeFilter( a );
EXPECT_EQ( size_t{ 0 }, coll.count() );
EXPECT_TRUE( coll.filters().empty() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RimDataFilterCollection, addAndRemoveEmitFiltersChangedSignal )
{
RimDataFilterCollection coll;
SignalCounter counter;
coll.filtersChanged.connect( &counter, &SignalCounter::onSignal );
auto* a = new RimCellRangeFilter();
coll.addFilter( a );
EXPECT_EQ( 1, counter.count );
coll.removeFilter( a );
EXPECT_EQ( 2, counter.count );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RimDataFilterCollection, childFilterChangeForwardsSignal )
{
RimDataFilterCollection coll;
SignalCounter counter;
coll.filtersChanged.connect( &counter, &SignalCounter::onSignal );
auto* a = new RimCellRangeFilter();
coll.addFilter( a );
const int afterAdd = counter.count;
a->triggerFilterChanged();
EXPECT_EQ( afterAdd + 1, counter.count );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RimDataFilterCollection, hasActiveFiltersReflectsChildState )
{
RimDataFilterCollection coll;
auto* a = new RimCellRangeFilter();
auto* b = new RimCellRangeFilter();
coll.addFilter( a );
coll.addFilter( b );
// Default-constructed RimCellFilter has isActive == true.
EXPECT_TRUE( coll.hasActiveFilters() );
a->setActive( false );
b->setActive( false );
EXPECT_FALSE( coll.hasActiveFilters() );
a->setActive( true );
EXPECT_TRUE( coll.hasActiveFilters() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RimDataFilterCollection, addNewRangeFilterReturnsConfigured )
{
RimDataFilterCollection coll;
RimCellRangeFilter* rangeFilter = coll.addNewRangeFilter();
ASSERT_NE( nullptr, rangeFilter );
EXPECT_EQ( size_t{ 1 }, coll.count() );
EXPECT_EQ( rangeFilter, coll.filters().front() );
// setDefaultValues(-1, -1) leaves IJK starts >= 1 (1-based Eclipse indexing).
EXPECT_GE( rangeFilter->startIndexI(), 1 );
EXPECT_GE( rangeFilter->startIndexJ(), 1 );
EXPECT_GE( rangeFilter->startIndexK(), 1 );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RimDataFilterCollection, addNewCombinedFilterReturnsEmpty )
{
RimDataFilterCollection coll;
RimCombinedFilter* combined = coll.addNewCombinedFilter();
ASSERT_NE( nullptr, combined );
EXPECT_EQ( size_t{ 1 }, coll.count() );
EXPECT_TRUE( combined->filters().empty() );
// A combined filter must be deletable so the generic delete context-menu item is offered (#14148).
EXPECT_TRUE( combined->isDeletable() );
}
//--------------------------------------------------------------------------------------------------
/// Adding a child to a combined filter that lives in the collection should bubble up the
/// filterChanged signal as a collection-level filtersChanged emission.
//--------------------------------------------------------------------------------------------------
TEST( RimDataFilterCollection, combinedFilterChildEmitsCollectionSignal )
{
RimDataFilterCollection coll;
RimCombinedFilter* combined = coll.addNewCombinedFilter();
ASSERT_NE( nullptr, combined );
SignalCounter counter;
coll.filtersChanged.connect( &counter, &SignalCounter::onSignal );
auto* child = combined->addNewFilter<RimCellRangeFilter>( []( RimCellRangeFilter* ) {} );
ASSERT_NE( nullptr, child );
// RimCombinedFilter::addFilter calls triggerFilterChanged, so the combined filter's signal
// fires; the collection has connected to that signal and re-emits filtersChanged.
EXPECT_GE( counter.count, 1 );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RimDataFilterCollection, pdmRoundTripPreservesFilters )
{
QString xml;
{
RimDataFilterCollection original;
original.addNewRangeFilter();
original.addNewCombinedFilter();
ASSERT_EQ( size_t{ 2 }, original.count() );
xml = original.writeObjectToXmlString();
}
RimDataFilterCollection restored;
restored.readObjectFromXmlString( xml, caf::PdmDefaultObjectFactory::instance() );
restored.resolveReferencesRecursively();
EXPECT_EQ( size_t{ 2 }, restored.count() );
auto items = restored.filters();
ASSERT_EQ( size_t{ 2 }, items.size() );
EXPECT_NE( nullptr, dynamic_cast<RimCellRangeFilter*>( items[0] ) );
EXPECT_NE( nullptr, dynamic_cast<RimCombinedFilter*>( items[1] ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RimDataFilterCollection, ensembleViewUsesEnsembleDataFilters )
{
auto eclipseCase = std::make_unique<RimEclipseResultCase>();
auto ensemble = std::make_unique<RimReservoirGridEnsemble>();
auto* view = new RimEclipseView();
view->setEclipseCase( eclipseCase.get() );
EXPECT_EQ( eclipseCase->dataFilterCollection(), view->dataFiltersInView()->sourceCollection() );
ensemble->addView( view );
EXPECT_EQ( ensemble->dataFilterCollection(), view->dataFiltersInView()->sourceCollection() );
}
//--------------------------------------------------------------------------------------------------
/// A view created via a case's own view collection should still resolve the ensemble's data filters.
//--------------------------------------------------------------------------------------------------
TEST( RimDataFilterCollection, viewForSingleRealizationUsesEnsembleDataFilters )
{
auto ensemble = std::make_unique<RimReservoirGridEnsemble>();
auto* eclipseCase = new RimEclipseResultCase();
ensemble->addCase( eclipseCase );
RimEclipseView* view = eclipseCase->viewCollection()->addView( eclipseCase );
EXPECT_EQ( ensemble->dataFilterCollection(), view->dataFiltersInView()->sourceCollection() );
}
//--------------------------------------------------------------------------------------------------
/// A filter added to the ensemble after the view exists must still appear as a wrapper in the view.
//--------------------------------------------------------------------------------------------------
TEST( RimDataFilterCollection, viewWrapperAppearsWhenEnsembleFilterAddedAfterViewExists )
{
auto ensemble = std::make_unique<RimReservoirGridEnsemble>();
auto* eclipseCase = new RimEclipseResultCase();
ensemble->addCase( eclipseCase );
RimEclipseView* view = eclipseCase->viewCollection()->addView( eclipseCase );
ASSERT_EQ( ensemble->dataFilterCollection(), view->dataFiltersInView()->sourceCollection() );
EXPECT_TRUE( view->dataFiltersInView()->wrappers().empty() );
ensemble->dataFilterCollection()->addNewPropertyFilter();
ASSERT_EQ( size_t{ 1 }, view->dataFiltersInView()->wrappers().size() );
EXPECT_EQ( ensemble->dataFilterCollection()->filters().front(), view->dataFiltersInView()->wrappers().front()->sourceFilter() );
}
//--------------------------------------------------------------------------------------------------
/// Reproduces RicNewViewForGridEnsembleFeature's "New View" command: the view is created via the
/// ensemble's own view collection, which used to lose the ensemble's data filter because the case
/// was assigned before the view was parented. See #14710.
//--------------------------------------------------------------------------------------------------
TEST( RimDataFilterCollection, viewCreatedViaEnsembleViewCollectionUsesEnsembleDataFilters )
{
auto ensemble = std::make_unique<RimReservoirGridEnsemble>();
auto* eclipseCase = new RimEclipseResultCase();
ensemble->addCase( eclipseCase );
ensemble->dataFilterCollection()->addNewPropertyFilter();
// Exercises the same call path as RicNewViewForGridEnsembleFeature.
RimEclipseView* view = ensemble->viewCollection()->addView( eclipseCase );
EXPECT_EQ( ensemble->dataFilterCollection(), view->dataFiltersInView()->sourceCollection() );
ASSERT_EQ( size_t{ 1 }, view->dataFiltersInView()->wrappers().size() );
}