diff --git a/Applications/ApplicationsLib/ProjectData.cpp b/Applications/ApplicationsLib/ProjectData.cpp index 5b2209b42d001d700b83881f9c930a9da0172c57..6606f35b60063720f65194515ecf3f1c392726e0 100644 --- a/Applications/ApplicationsLib/ProjectData.cpp +++ b/Applications/ApplicationsLib/ProjectData.cpp @@ -471,33 +471,11 @@ void ProjectData::parseMedia( auto material_id_string = //! \ogs_file_attr{prj__media__medium__id} medium_config.getConfigAttribute<std::string>("id", "0"); - material_id_string.erase( - remove_if(begin(material_id_string), end(material_id_string), - [](unsigned char const c) { return std::isspace(c); }), - end(material_id_string)); - auto const material_ids_strings = - BaseLib::splitString(material_id_string, ','); - - // Convert strings to ints; - std::vector<int> material_ids; - std::transform( - begin(material_ids_strings), end(material_ids_strings), - std::back_inserter(material_ids), [](std::string const& m_id) { - if (auto const it = std::find_if_not( - begin(m_id), end(m_id), - [](unsigned char const c) { return std::isdigit(c); }); - it != end(m_id)) - { - OGS_FATAL( - "Could not parse material ID's from '{:s}'. Please " - "separate multiple material ID's by comma only. " - "Invalid character: '%c'", - m_id, *it); - } - return std::stoi(m_id); - }); - for (auto const& id : material_ids) + auto const material_ids_of_this_medium = + splitMaterialIdString(material_id_string); + + for (auto const& id : material_ids_of_this_medium) { if (_media.find(id) != end(_media)) { @@ -508,15 +486,21 @@ void ProjectData::parseMedia( id); } - _media[id] = - (id == material_ids[0]) - ? MaterialPropertyLib::createMedium( - _mesh_vec[0]->getDimension(), medium_config, - _parameters, - _local_coordinate_system ? &*_local_coordinate_system - : nullptr, - _curves) - : _media[material_ids[0]]; + if (id == material_ids_of_this_medium[0]) + { + _media[id] = MaterialPropertyLib::createMedium( + _mesh_vec[0]->getDimension(), medium_config, _parameters, + _local_coordinate_system ? &*_local_coordinate_system + : nullptr, + _curves); + } + else + { + // This medium has multiple material IDs assigned and this is + // not the first material ID. Therefore we can reuse the medium + // we created before. + _media[id] = _media[material_ids_of_this_medium[0]]; + } } } @@ -1213,3 +1197,54 @@ void ProjectData::parseCurves(std::optional<BaseLib::ConfigTree> const& config) "The curve name is not unique."); } } + +std::vector<int> splitMaterialIdString(std::string const& material_id_string) +{ + auto const material_ids_strings = + BaseLib::splitString(material_id_string, ','); + + std::vector<int> material_ids; + for (auto& mid_str : material_ids_strings) + { + std::size_t num_chars_processed = 0; + int material_id; + try + { + material_id = std::stoi(mid_str, &num_chars_processed); + } + catch (std::invalid_argument&) + { + OGS_FATAL( + "Could not parse material ID from '{}' to a valid " + "integer.", + mid_str); + } + catch (std::out_of_range&) + { + OGS_FATAL( + "Could not parse material ID from '{}'. The integer value " + "of the given string exceeds the permitted range.", + mid_str); + } + + if (num_chars_processed != mid_str.size()) + { + // Not the whole string has been parsed. Check the rest. + if (auto const it = std::find_if_not( + begin(mid_str) + num_chars_processed, end(mid_str), + [](unsigned char const c) { return std::isspace(c); }); + it != end(mid_str)) + { + OGS_FATAL( + "Could not parse material ID from '{}'. Please " + "separate multiple material IDs by comma only. " + "Invalid character: '{}' at position {}.", + mid_str, *it, distance(begin(mid_str), it)); + } + } + + material_ids.push_back(material_id); + }; + + return material_ids; +} diff --git a/Applications/ApplicationsLib/ProjectData.h b/Applications/ApplicationsLib/ProjectData.h index 47b57e4922457a6373bbfd9255ee5053cbd9291b..cd81ac4fc0f6eac90e18e06a2b75629460c7f339 100644 --- a/Applications/ApplicationsLib/ProjectData.h +++ b/Applications/ApplicationsLib/ProjectData.h @@ -141,3 +141,8 @@ private: std::unique_ptr<MathLib::PiecewiseLinearInterpolation>> _curves; }; + +/// Parses a comma separated list of integers. +/// Such lists occur in the medium definition in the OGS prj file. +/// Error messages in this function refer to this specific purpose. +std::vector<int> splitMaterialIdString(std::string const& material_id_string); diff --git a/Tests/ApplicationsLib/TestProjectData.cpp b/Tests/ApplicationsLib/TestProjectData.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5985cbd31fe00c0f5e45b9838e0225ce74093f5a --- /dev/null +++ b/Tests/ApplicationsLib/TestProjectData.cpp @@ -0,0 +1,95 @@ +/** + * \file + * \copyright + * Copyright (c) 2012-2021, OpenGeoSys Community (http://www.opengeosys.org) + * Distributed under a Modified BSD License. + * See accompanying file LICENSE.txt or + * http://www.opengeosys.org/project/license + * + */ + +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include <algorithm> + +#include "Applications/ApplicationsLib/ProjectData.h" + +TEST(ApplicationsLib_ProjectData_SplitIntegerList, EmptyList) +{ + ASSERT_TRUE(splitMaterialIdString("").empty()); +} + +TEST(ApplicationsLib_ProjectData_SplitIntegerList, SingleInt) +{ + using namespace testing; + + EXPECT_THAT(splitMaterialIdString("5"), ContainerEq(std::vector<int>{5})); + + // leading whitespace + EXPECT_THAT(splitMaterialIdString(" 16"), + ContainerEq(std::vector<int>{16})); + + // trailing whitespace + EXPECT_THAT(splitMaterialIdString("23 "), + ContainerEq(std::vector<int>{23})); + + // negative numbers are OK + EXPECT_THAT(splitMaterialIdString("-20"), + ContainerEq(std::vector<int>{-20})); +} + +TEST(ApplicationsLib_ProjectData_SplitIntegerList, SingleIntFail) +{ + // wrong character prefix/suffix + EXPECT_THROW(splitMaterialIdString("x"), std::runtime_error); + EXPECT_THROW(splitMaterialIdString(".5"), std::runtime_error); + EXPECT_THROW(splitMaterialIdString("5?"), std::runtime_error); + EXPECT_THROW(splitMaterialIdString("7 !"), std::runtime_error); + EXPECT_THROW(splitMaterialIdString("8 u"), std::runtime_error); + + // hexadecimal numbers are not accepted + EXPECT_THROW(splitMaterialIdString("0xfa"), std::runtime_error); + + // another integer is not accepted + EXPECT_THROW(splitMaterialIdString("1 2"), std::runtime_error); + + // range exceeeded + EXPECT_THROW( + splitMaterialIdString("1234567890123456789012345678901234567890"), + std::runtime_error); +} + +TEST(ApplicationsLib_ProjectData_SplitIntegerList, IntList) +{ + using namespace testing; + + EXPECT_THAT(splitMaterialIdString("5,6,7"), + ContainerEq(std::vector<int>{5, 6, 7})); + + // whitespace around comma + EXPECT_THAT(splitMaterialIdString("9 ,10, 11,12 , 13"), + ContainerEq(std::vector<int>{9, 10, 11, 12, 13})); + + // trailing comma is ignored + EXPECT_THAT(splitMaterialIdString("20, 22, 24,"), + ContainerEq(std::vector<int>{20, 22, 24})); +} + +TEST(ApplicationsLib_ProjectData_SplitIntegerList, IntListFail) +{ + // only delimiter + EXPECT_THROW(splitMaterialIdString(","), std::runtime_error); + + // empty element + EXPECT_THROW(splitMaterialIdString("5,,6"), std::runtime_error); + + // leading comma + EXPECT_THROW(splitMaterialIdString(",40"), std::runtime_error); + + // missing comma + EXPECT_THROW(splitMaterialIdString("12 20"), std::runtime_error); + + // wrong number in the list + EXPECT_THROW(splitMaterialIdString("1,2,x,5"), std::runtime_error); +} diff --git a/Tests/BaseLib/TestStringTools.cpp b/Tests/BaseLib/TestStringTools.cpp new file mode 100644 index 0000000000000000000000000000000000000000..7a524ab96c6219ece17cd046063b8efee7253b95 --- /dev/null +++ b/Tests/BaseLib/TestStringTools.cpp @@ -0,0 +1,45 @@ +/** + * \file + * \copyright + * Copyright (c) 2012-2021, OpenGeoSys Community (http://www.opengeosys.org) + * Distributed under a Modified BSD License. + * See accompanying file LICENSE.txt or + * http://www.opengeosys.org/project/license + * + */ + +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include "BaseLib/StringTools.h" + +TEST(BaseLibStringTools, SplitString) +{ + using namespace testing; + using namespace std; + + // empty string + EXPECT_THAT(BaseLib::splitString("", ';'), ContainerEq(list<string>{})); + + // no delimiter + EXPECT_THAT(BaseLib::splitString("a", ';'), ContainerEq(list<string>{"a"})); + + // some delimited values + EXPECT_THAT(BaseLib::splitString("a,b,c", ','), + ContainerEq(list<string>{"a", "b", "c"})); + + // leading delimiter + EXPECT_THAT(BaseLib::splitString(",a,b,c", ','), + ContainerEq(list<string>{"", "a", "b", "c"})); + + // double delimiters + EXPECT_THAT(BaseLib::splitString("a,b,,c", ','), + ContainerEq(list<string>{"a", "b", "", "c"})); + + // trailing delimiters are ignored + EXPECT_THAT(BaseLib::splitString("a,b,c,", ','), + ContainerEq(list<string>{"a", "b", "c"})); + + // ... and behave like this if they are on their own: + EXPECT_THAT(BaseLib::splitString(",", ','), ContainerEq(list<string>{""})); +} diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 4e4db65ca4032dd10035d8a8fac6d714574d1586..2fdf7362b13ab46079e2731866be99aa699fffc8 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -24,6 +24,7 @@ append_source_files(TEST_SOURCES GeoLib/IO) append_source_files(TEST_SOURCES MaterialLib) append_source_files(TEST_SOURCES MathLib) append_source_files(TEST_SOURCES MeshLib) +append_source_files(TEST_SOURCES ApplicationsLib) append_source_files(TEST_SOURCES MeshGeoToolsLib) append_source_files(TEST_SOURCES_NUMLIB NumLib) # Disable Unity build for NumLib tests @@ -62,8 +63,10 @@ ogs_add_executable(testrunner ${TEST_SOURCES}) target_link_libraries( testrunner - PRIVATE ApplicationsFileIO + PRIVATE ApplicationsLib + ApplicationsFileIO autocheck + gmock gtest MeshGeoToolsLib MaterialLib