diff --git a/src/OpenSimCreator/Documents/ModelWarper/ModelWarperConfiguration.cpp b/src/OpenSimCreator/Documents/ModelWarper/ModelWarperConfiguration.cpp index 54d98bbe98..cd69f06c95 100644 --- a/src/OpenSimCreator/Documents/ModelWarper/ModelWarperConfiguration.cpp +++ b/src/OpenSimCreator/Documents/ModelWarper/ModelWarperConfiguration.cpp @@ -1,22 +1,50 @@ #include "ModelWarperConfiguration.h" #include +#include #include +#include +#include +#include +#include osc::mow::ModelWarperConfiguration::ModelWarperConfiguration() { - constructProperties(); + constructProperties(); } osc::mow::ModelWarperConfiguration::ModelWarperConfiguration(const std::filesystem::path& filePath) : - OpenSim::Component{filePath.string()} + OpenSim::Component{filePath.string()} { - constructProperties(); - updateFromXMLDocument(); + constructProperties(); + updateFromXMLDocument(); } void osc::mow::ModelWarperConfiguration::constructProperties() +{} + +void osc::mow::ModelWarperConfiguration::extendFinalizeFromProperties() { - // TODO + if (getProperty_components().empty()) { + return; // BODGE: the OpenSim API throws if you call `getComponentList` on an + } + + // note: it's ok to have the same `StrategyTarget` if the `ComponentStrategy` applies + // to a different type of component + // + // (e.g. if a station warper targets "*", that will capture different components from + // a offset frame warper that targets "*") + using SetElement = std::pair; + std::unordered_set> allStrategyTargets; + for (const auto& warpingStrategy : getComponentList()) { + const std::type_info& targetType = warpingStrategy.getTargetComponentTypeInfo(); + for (int i = 0; i < warpingStrategy.getProperty_StrategyTargets().size(); ++i) { + if (not allStrategyTargets.emplace(&targetType, warpingStrategy.get_StrategyTargets(i)).second) { + std::stringstream ss; + ss << warpingStrategy.get_StrategyTargets(i) << ": duplicate strategy target detected in '" << warpingStrategy.getName() << "': this will confuse the engine and should be resolved"; + OPENSIM_THROW_FRMOBJ(OpenSim::Exception, std::move(ss).str()); + } + } + } } diff --git a/src/OpenSimCreator/Documents/ModelWarper/ModelWarperConfiguration.h b/src/OpenSimCreator/Documents/ModelWarper/ModelWarperConfiguration.h index c11ac03cf8..3d87252a82 100644 --- a/src/OpenSimCreator/Documents/ModelWarper/ModelWarperConfiguration.h +++ b/src/OpenSimCreator/Documents/ModelWarper/ModelWarperConfiguration.h @@ -1,8 +1,16 @@ #pragma once #include +#include +#include +#include #include +#include +#include +#include +#include +#include namespace osc::mow { @@ -10,8 +18,14 @@ namespace osc::mow // components (`StrategyTargets`) during a model warp class ComponentWarpingStrategy : public OpenSim::Component { OpenSim_DECLARE_ABSTRACT_OBJECT(ComponentWarpingStrategy, OpenSim::Component); + public: + OpenSim_DECLARE_LIST_PROPERTY(StrategyTargets, std::string, "a sequence of strategy target strings that this strategy applies to"); protected: - ComponentWarpingStrategy() = default; + ComponentWarpingStrategy(const std::type_info& targetComponentType) : + _targetComponentType{&targetComponentType} + { + constructProperty_StrategyTargets(); + } ComponentWarpingStrategy(const ComponentWarpingStrategy&) = default; ComponentWarpingStrategy(ComponentWarpingStrategy&&) noexcept = default; ComponentWarpingStrategy& operator=(const ComponentWarpingStrategy&) = default; @@ -19,14 +33,55 @@ namespace osc::mow public: ~ComponentWarpingStrategy() noexcept override = default; - // StrategyTargets + const std::type_info& getTargetComponentTypeInfo() const + { + return *_targetComponentType; + } private: + void extendFinalizeFromProperties() override + { + assertStrategyTargetsNotEmpty(); + assertStrategyTargetsAreUnique(); + } + + void assertStrategyTargetsNotEmpty() + { + if (getProperty_StrategyTargets().empty()) { + OPENSIM_THROW_FRMOBJ(OpenSim::Exception, "The property of this component must be populated with at least one entry"); + } + } + + void assertStrategyTargetsAreUnique() + { + const int numStrategyTargets = getProperty_StrategyTargets().size(); + std::unordered_set uniqueStrategyTargets; + uniqueStrategyTargets.reserve(numStrategyTargets); + for (int i = 0; i < numStrategyTargets; ++i) { + const std::string& strategyTarget = get_StrategyTargets(i); + const auto [_, inserted] = uniqueStrategyTargets.emplace(strategyTarget); + if (not inserted) { + std::stringstream ss; + ss << strategyTarget << ": duplicate strategy target detected: all strategy targets must be unique"; + OPENSIM_THROW_FRMOBJ(OpenSim::Exception, std::move(ss).str()); + } + } + } + + const std::type_info* _targetComponentType; + }; + + // abstract interface to a component that is capable of warping `n` other + // components (`StrategyTargets`) of type `T` during a model warp + template T> + class ComponentWarpingStrategyFor : public ComponentWarpingStrategy { + public: + ComponentWarpingStrategyFor() : ComponentWarpingStrategy{typeid(T)} {} }; // abstract interface to a component that is capable of warping `n` // `OpenSim::PhysicalOffsetFrame`s during a model warp - class OffsetFrameWarpingStrategy : public ComponentWarpingStrategy { - OpenSim_DECLARE_ABSTRACT_OBJECT(OffsetFrameWarpingStrategy, ComponentWarpingStrategy); + class OffsetFrameWarpingStrategy : public ComponentWarpingStrategyFor { + OpenSim_DECLARE_ABSTRACT_OBJECT(OffsetFrameWarpingStrategy, ComponentWarpingStrategyFor); }; // concrete implementation of an `OffsetFrameWarpingStrategy` in which @@ -55,7 +110,7 @@ namespace osc::mow // abstract interface to a component that is capable of warping `n` // `OpenSim::Station`s during a model warp - class StationWarpingStrategy : public ComponentWarpingStrategy { + class StationWarpingStrategy : public ComponentWarpingStrategyFor { OpenSim_DECLARE_ABSTRACT_OBJECT(StationWarpingStrategy, ComponentWarpingStrategy); }; @@ -78,19 +133,6 @@ namespace osc::mow OpenSim_DECLARE_CONCRETE_OBJECT(IdentityStationWarpingStrategy, StationWarpingStrategy); }; - // TODO: - // MuscleParameterWarpingStrategies - // MuscleParameterWarpingStrategy - // some_scaling_param - // IdentityMuscleParameterWarpingStrategy - // - // MeshWarpingStrategies - // ThinPlateSplineMeshWarpingStrategy - // - // WrapSurfaceWarpingStrategies - // WrapSurfaceWarpingStrategy - // LeastSquaresProjectionWrapSurfaceWarpingStrategy? - // top-level model warping configuration file class ModelWarperConfiguration final : public OpenSim::Component { OpenSim_DECLARE_CONCRETE_OBJECT(ModelWarperConfiguration, OpenSim::Component); @@ -103,5 +145,6 @@ namespace osc::mow explicit ModelWarperConfiguration(const std::filesystem::path& filePath); private: void constructProperties(); + void extendFinalizeFromProperties() override; }; -} \ No newline at end of file +} diff --git a/src/oscar/Utils/HashHelpers.h b/src/oscar/Utils/HashHelpers.h index fa67e0169b..0c2973832c 100644 --- a/src/oscar/Utils/HashHelpers.h +++ b/src/oscar/Utils/HashHelpers.h @@ -5,6 +5,7 @@ #include #include #include +#include namespace osc { @@ -36,4 +37,20 @@ namespace osc } return rv; } + + // an osc-specific hashing object + // + // think of it as a `std::hash` that's used specifically in situations where + // specializing `std::hash` might be a bad idea (e.g. on `std` library types + // templated on other `std` library types, where there's a nonzero chance the + template + struct Hasher; + + template + struct Hasher> final { + size_t operator()(const std::pair& p) const + { + return hash_of(p.first, p.second); + } + }; } diff --git a/tests/TestOpenSimCreator/Documents/ModelWarper/TestModelWarperConfiguration.cpp b/tests/TestOpenSimCreator/Documents/ModelWarper/TestModelWarperConfiguration.cpp index eb40107d36..212bad859c 100644 --- a/tests/TestOpenSimCreator/Documents/ModelWarper/TestModelWarperConfiguration.cpp +++ b/tests/TestOpenSimCreator/Documents/ModelWarper/TestModelWarperConfiguration.cpp @@ -12,78 +12,162 @@ using namespace osc::mow; namespace { - std::filesystem::path GetFixturePath(const std::filesystem::path& subpath) - { - return std::filesystem::weakly_canonical(std::filesystem::path{OSC_TESTING_RESOURCES_DIR} / subpath); - } + std::filesystem::path GetFixturePath(const std::filesystem::path& subpath) + { + return std::filesystem::weakly_canonical(std::filesystem::path{OSC_TESTING_RESOURCES_DIR} / subpath); + } } TEST(ModelWarperConfiguration, CanDefaultConstruct) { - [[maybe_unused]] ModelWarperConfiguration configuration; + [[maybe_unused]] ModelWarperConfiguration configuration; } TEST(ModelWarperConfiguration, CanSaveAndLoadDefaultConstructedToAndFromXMLFile) { - TemporaryFile temporaryFile; - temporaryFile.close(); // so that `OpenSim::Object::print`'s implementation can open+write to it + TemporaryFile temporaryFile; + temporaryFile.close(); // so that `OpenSim::Object::print`'s implementation can open+write to it - ModelWarperConfiguration configuration; - configuration.print(temporaryFile.absolute_path().string()); + ModelWarperConfiguration configuration; + configuration.print(temporaryFile.absolute_path().string()); - ModelWarperConfiguration loadedConfiguration{temporaryFile.absolute_path().string()}; - loadedConfiguration.finalizeFromProperties(); - loadedConfiguration.finalizeConnections(loadedConfiguration); + ModelWarperConfiguration loadedConfiguration{temporaryFile.absolute_path().string()}; + loadedConfiguration.finalizeFromProperties(); + loadedConfiguration.finalizeConnections(loadedConfiguration); } TEST(ModelWarperConfiguration, LoadingNonExistentFileThrows) { - ASSERT_ANY_THROW({ [[maybe_unused]] ModelWarperConfiguration configuration{GetFixturePath("doesnt_exist")}; }); + ASSERT_ANY_THROW({ [[maybe_unused]] ModelWarperConfiguration configuration{GetFixturePath("doesnt_exist")}; }); } TEST(ModelWarperConfiguration, CanLoadEmptySequence) { - ModelWarperConfiguration configuration{GetFixturePath("Document/ModelWarper/ModelWarperConfiguration/empty_sequence.xml")}; - configuration.finalizeFromProperties(); - configuration.finalizeConnections(configuration); + ModelWarperConfiguration configuration{GetFixturePath("Document/ModelWarper/ModelWarperConfiguration/empty_sequence.xml")}; + configuration.finalizeFromProperties(); + configuration.finalizeConnections(configuration); } TEST(ModelWarperConfiguration, CanLoadTrivialSingleOffsetFrameWarpingStrategy) { - OpenSim::Object::registerType(ProduceErrorOffsetFrameWarpingStrategy{}); + OpenSim::Object::registerType(ProduceErrorOffsetFrameWarpingStrategy{}); - ModelWarperConfiguration configuration{GetFixturePath("Document/ModelWarper/ModelWarperConfiguration/single_offsetframe_warper.xml")}; - configuration.finalizeFromProperties(); - configuration.finalizeConnections(configuration); + ModelWarperConfiguration configuration{GetFixturePath("Document/ModelWarper/ModelWarperConfiguration/single_offsetframe_warper.xml")}; + configuration.finalizeFromProperties(); + configuration.finalizeConnections(configuration); - auto range = configuration.getComponentList(); - const auto numEls = std::distance(range.begin(), range.end()); - ASSERT_EQ(numEls, 1); + auto range = configuration.getComponentList(); + const auto numEls = std::distance(range.begin(), range.end()); + ASSERT_EQ(numEls, 1); } TEST(ModelWarperConfiguration, CanContainAMixtureOfOffsetFrameWarpingStrategies) { - OpenSim::Object::registerType(ProduceErrorOffsetFrameWarpingStrategy{}); - OpenSim::Object::registerType(ThinPlateSplineOnlyTranslationOffsetFrameWarpingStrategy{}); + OpenSim::Object::registerType(ProduceErrorOffsetFrameWarpingStrategy{}); + OpenSim::Object::registerType(ThinPlateSplineOnlyTranslationOffsetFrameWarpingStrategy{}); - ModelWarperConfiguration configuration{GetFixturePath("Document/ModelWarper/ModelWarperConfiguration/mixed_offsetframe_warpers.xml")}; - configuration.finalizeFromProperties(); - configuration.finalizeConnections(configuration); + ModelWarperConfiguration configuration{GetFixturePath("Document/ModelWarper/ModelWarperConfiguration/mixed_offsetframe_warpers.xml")}; + configuration.finalizeFromProperties(); + configuration.finalizeConnections(configuration); - auto range = configuration.getComponentList(); - const auto numEls = std::distance(range.begin(), range.end()); - ASSERT_EQ(numEls, 2); + auto range = configuration.getComponentList(); + const auto numEls = std::distance(range.begin(), range.end()); + ASSERT_EQ(numEls, 2); } TEST(ModelWarperConfiguration, CanLoadTrivialSingleStationWarpingStrategy) { - OpenSim::Object::registerType(ProduceErrorStationWarpingStrategy{}); + OpenSim::Object::registerType(ProduceErrorStationWarpingStrategy{}); - ModelWarperConfiguration configuration{GetFixturePath("Document/ModelWarper/ModelWarperConfiguration/single_station_warper.xml")}; - configuration.finalizeFromProperties(); - configuration.finalizeConnections(configuration); + ModelWarperConfiguration configuration{GetFixturePath("Document/ModelWarper/ModelWarperConfiguration/single_station_warper.xml")}; + configuration.finalizeFromProperties(); + configuration.finalizeConnections(configuration); - auto range = configuration.getComponentList(); - const auto numEls = std::distance(range.begin(), range.end()); - ASSERT_EQ(numEls, 1); + auto range = configuration.getComponentList(); + const auto numEls = std::distance(range.begin(), range.end()); + ASSERT_EQ(numEls, 1); +} + +TEST(ModelWarperConfiguration, CanLoadAMixtureOfStationWarpingStrategies) +{ + OpenSim::Object::registerType(ProduceErrorStationWarpingStrategy{}); + OpenSim::Object::registerType(ThinPlateSplineStationWarpingStrategy{}); + + ModelWarperConfiguration configuration{GetFixturePath("Document/ModelWarper/ModelWarperConfiguration/mixed_station_warpers.xml")}; + configuration.finalizeFromProperties(); + configuration.finalizeConnections(configuration); + + auto range = configuration.getComponentList(); + const auto numEls = std::distance(range.begin(), range.end()); + ASSERT_EQ(numEls, 2); +} + +TEST(ProduceErrorOffsetFrameWarpingStrategy, FinalizeFromPropertiesFailsIfNoStrategyTargets) +{ + ProduceErrorOffsetFrameWarpingStrategy strategy; + ASSERT_ANY_THROW({ strategy.finalizeFromProperties(); }) << "should fail, because the strategy has no targets (ambiguous definition)"; +} + +TEST(ProduceErrorOffsetFrameWarpingStrategy, FinalizeFromPropertiesWorksIfThereIsAStrategyTarget) +{ + ProduceErrorOffsetFrameWarpingStrategy strategy; + strategy.append_StrategyTargets("*"); + ASSERT_NO_THROW({ strategy.finalizeFromProperties(); }); +} + +TEST(ModelWarperConfiguration, LoadingConfigurationContainingStrategyWithTwoTargetsWorksAsExpected) +{ + OpenSim::Object::registerType(ProduceErrorStationWarpingStrategy{}); + + ModelWarperConfiguration configuration{GetFixturePath("Document/ModelWarper/ModelWarperConfiguration/two_strategy_targets.xml")}; + configuration.finalizeFromProperties(); + + const auto* strategy = configuration.findComponent("two_targets"); + ASSERT_EQ(strategy->getProperty_StrategyTargets().size(), 2); + ASSERT_EQ(strategy->get_StrategyTargets(0), "/first/target"); + ASSERT_EQ(strategy->get_StrategyTargets(1), "*"); +} + +TEST(ProduceErrorOffsetFrameWarpingStrategy, FinalizeFromPropertiesThrowsIfDuplicateStrategyTargetsDetected) +{ + // note: this validation check might be relied upon by the validation passes of + // higher-level components (e.g. `ModelWarperConfiguration`) + + ProduceErrorOffsetFrameWarpingStrategy strategy; + strategy.append_StrategyTargets("/some/target"); + strategy.append_StrategyTargets("/some/target"); + + ASSERT_ANY_THROW({ strategy.finalizeFromProperties(); }) << "finalizeFromProperties should throw if duplicate StrategyTargets are declared"; +} + +TEST(ProduceErrorOffsetFrameWarpingStrategy, FinalizeFromPropertiesThrowsIfDuplicateWildcardStrategyTargetsDetected) +{ + // note: this validation check might be relied upon by the validation passes of + // higher-level components (e.g. `ModelWarperConfiguration`) + + ProduceErrorOffsetFrameWarpingStrategy strategy; + strategy.append_StrategyTargets("*"); + strategy.append_StrategyTargets("*"); + + ASSERT_ANY_THROW({ strategy.finalizeFromProperties(); }) << "finalizeFromProperties should throw if duplicate StrategyTargets are declared (even wildcards)"; +} + +TEST(ModelWarperConfiguration, finalizeFromPropertiesThrowsWhenGivenConfigurationContainingTwoStrategiesWithTheSameStrategyTarget) +{ + OpenSim::Object::registerType(ProduceErrorStationWarpingStrategy{}); + OpenSim::Object::registerType(ThinPlateSplineOnlyTranslationOffsetFrameWarpingStrategy{}); + + ModelWarperConfiguration configuration{GetFixturePath("Document/ModelWarper/ModelWarperConfiguration/duplicated_offsetframe_strategytarget.xml")}; + + ASSERT_ANY_THROW({ configuration.finalizeFromProperties(); }); +} + +TEST(ModelWarperConfiguration, finalizeFromPropertiesDoesNotThrowWhenGivenConfigurationContainingTwoDifferentTypesOfStrategiesWithTheSameStrategyTarget) +{ + OpenSim::Object::registerType(ProduceErrorOffsetFrameWarpingStrategy{}); + OpenSim::Object::registerType(ProduceErrorStationWarpingStrategy{}); + + ModelWarperConfiguration configuration{GetFixturePath("Document/ModelWarper/ModelWarperConfiguration/duplicated_but_different_types.xml")}; + + ASSERT_NO_THROW({ configuration.finalizeFromProperties(); }); } diff --git a/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/duplicated_but_different_types.xml b/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/duplicated_but_different_types.xml new file mode 100644 index 0000000000..1d19a24ff1 --- /dev/null +++ b/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/duplicated_but_different_types.xml @@ -0,0 +1,19 @@ + + + + + + + /something/more/specific + + + + + + + /something/more/specific + + + + + diff --git a/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/duplicated_offsetframe_strategytarget.xml b/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/duplicated_offsetframe_strategytarget.xml new file mode 100644 index 0000000000..4c43244a1b --- /dev/null +++ b/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/duplicated_offsetframe_strategytarget.xml @@ -0,0 +1,19 @@ + + + + + + + + /something/more/specific + + + + + /something/more/specific + + + + + diff --git a/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/mixed_offsetframe_warpers.xml b/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/mixed_offsetframe_warpers.xml index 46d4c4d148..f6b7ce6296 100644 --- a/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/mixed_offsetframe_warpers.xml +++ b/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/mixed_offsetframe_warpers.xml @@ -3,10 +3,15 @@ + + * + + + /something/more/specific + - diff --git a/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/mixed_station_warpers.xml b/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/mixed_station_warpers.xml new file mode 100644 index 0000000000..e2ee96b18e --- /dev/null +++ b/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/mixed_station_warpers.xml @@ -0,0 +1,17 @@ + + + + + + + * + + + + + /something/more/specific + + + + + diff --git a/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/single_offsetframe_warper.xml b/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/single_offsetframe_warper.xml index 104b097d4b..c87a8214e9 100644 --- a/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/single_offsetframe_warper.xml +++ b/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/single_offsetframe_warper.xml @@ -3,8 +3,10 @@ + + * + - diff --git a/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/single_station_warper.xml b/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/single_station_warper.xml index 872e0558b5..d90dfe07c3 100644 --- a/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/single_station_warper.xml +++ b/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/single_station_warper.xml @@ -3,8 +3,8 @@ + * - diff --git a/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/two_strategy_targets.xml b/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/two_strategy_targets.xml new file mode 100644 index 0000000000..c8b89fcc2d --- /dev/null +++ b/tests/TestOpenSimCreator/resources/Document/ModelWarper/ModelWarperConfiguration/two_strategy_targets.xml @@ -0,0 +1,13 @@ + + + + + + + /first/target + * + + + + + diff --git a/tests/testoscar/CMakeLists.txt b/tests/testoscar/CMakeLists.txt index 01f6cc68f0..e7b2eb9c94 100644 --- a/tests/testoscar/CMakeLists.txt +++ b/tests/testoscar/CMakeLists.txt @@ -85,6 +85,7 @@ add_executable(testoscar Utils/TestEnumHelpers.cpp Utils/TestFileChangePoller.cpp Utils/TestFilenameExtractor.cpp + Utils/TestHashHelpers.cpp Utils/TestLifetimedPtr.cpp Utils/TestLifetimeWatcher.cpp Utils/TestNonTypelist.cpp @@ -104,7 +105,7 @@ add_executable(testoscar TestingHelpers.cpp TestingHelpers.h testoscar.cpp # entry point - ) +) configure_file( diff --git a/tests/testoscar/Utils/TestHashHelpers.cpp b/tests/testoscar/Utils/TestHashHelpers.cpp new file mode 100644 index 0000000000..d5030e6003 --- /dev/null +++ b/tests/testoscar/Utils/TestHashHelpers.cpp @@ -0,0 +1,32 @@ +#include + +#include + +#include + +using namespace osc; + +TEST(Hasher, CanBeUsedToHashAStdPair) +{ + const std::pair p = {-20, 8}; + const Hasher> hasher; + ASSERT_NE(hasher(p), 0); +} + +TEST(Hasher, StdPairHashChangesWhenFirstElementDiffers) +{ + const std::pair p1 = {-20, 8}; + std::pair p2 = p1; + p2.first += 10; + const Hasher> hasher; + ASSERT_NE(hasher(p1), hasher(p2)); +} + +TEST(Hasher, StdPairHashChangesWhenSecondElementDiffers) +{ + const std::pair p1 = {-20, 8}; + std::pair p2 = p1; + p2.second += 7; + const Hasher> hasher; + ASSERT_NE(hasher(p1), hasher(p2)); +}