diff --git a/include/util/timezones.hpp b/include/util/timezones.hpp index ef2111700..87ae48640 100644 --- a/include/util/timezones.hpp +++ b/include/util/timezones.hpp @@ -3,6 +3,7 @@ #include "util/log.hpp" +#include #include #include @@ -31,13 +32,14 @@ class Timezoner public: Timezoner() = default; - Timezoner(std::string tz_filename, std::time_t utc_time_now); + Timezoner(const char geojson[], std::time_t utc_time_now); + Timezoner(const boost::filesystem::path &tz_shapes_filename, std::time_t utc_time_now); struct tm operator()(const point_t &point) const; - private: - void LoadLocalTimesRTree(const std::string &tz_shapes_filename, std::time_t utc_time); - void ValidateFeature(const rapidjson::Value &feature, const std::string &filename); + void ValidateFeature(const rapidjson::Value &feature); + void ValidateCoordinate(const rapidjson::Value &coordinate); + void LoadLocalTimesRTree(rapidjson::Document &geojson, std::time_t utc_time); struct tm default_time; rtree_t rtree; diff --git a/src/util/timezones.cpp b/src/util/timezones.cpp index d28af538d..97150f08c 100644 --- a/src/util/timezones.cpp +++ b/src/util/timezones.cpp @@ -2,6 +2,8 @@ #include "util/log.hpp" #include "util/timezones.hpp" +#include +#include #include #include "rapidjson/document.h" @@ -19,73 +21,29 @@ namespace osrm namespace updater { -Timezoner::Timezoner(std::string tz_filename, std::time_t utc_time_now) +Timezoner::Timezoner(const char geojson[], std::time_t utc_time_now) { util::Log() << "Time zone validation based on UTC time : " << utc_time_now; // Thread safety: MT-Unsafe const:env default_time = *gmtime(&utc_time_now); - LoadLocalTimesRTree(tz_filename, utc_time_now); + rapidjson::Document doc; + doc.Parse(geojson); + LoadLocalTimesRTree(doc, utc_time_now); } -void Timezoner::ValidateFeature(const rapidjson::Value &feature, const std::string &filename) +Timezoner::Timezoner(const boost::filesystem::path &tz_shapes_filename, std::time_t utc_time_now) { - if (!feature.HasMember("type")) - { - throw osrm::util::exception("Failed to parse " + filename + - ". Feature is missing type member."); - } else if (!feature["type"].IsString()) - { - throw osrm::util::exception("Failed to parse " + filename + - ". Feature non-string type member."); - } - if (!feature.HasMember("properties")) - { - throw osrm::util::exception("Failed to parse " + filename + - ". Feature is missing properties member."); - } - else if (!feature.GetObject()["properties"].IsObject()) - { - throw osrm::util::exception("Failed to parse " + filename + - ". Feature has non-object properties member."); - } - if (!feature["properties"].GetObject().HasMember("TZID")) - { - throw osrm::util::exception("Failed to parse " + filename + - ". Feature is missing TZID member in properties."); - } - else if (!feature["properties"].GetObject()["TZID"].IsString()) - { - throw osrm::util::exception("Failed to parse " + filename + - ". Feature has non-string TZID value."); - } - if (!feature.HasMember("geometry")) - { - throw osrm::util::exception("Failed to parse " + filename + - ". Feature is missing geometry member."); - } - else if (!feature.GetObject()["geometry"].IsObject()) - { - throw osrm::util::exception("Failed to parse " + filename + - ". Feature non-object geometry member."); - } + util::Log() << "Time zone validation based on UTC time : " << utc_time_now; + // Thread safety: MT-Unsafe const:env + default_time = *gmtime(&utc_time_now); - if (!feature["geometry"].GetObject().HasMember("type")) - throw osrm::util::exception("Failed to parse " + filename + - ". Feature geometry is missing type member."); - if (!feature["geometry"].GetObject().HasMember("coordinates")) - throw osrm::util::exception("Failed to parse " + filename + - ". Feature geometry is missing coordinates member."); -} - -void Timezoner::LoadLocalTimesRTree(const std::string &tz_shapes_filename, std::time_t utc_time) -{ if (tz_shapes_filename.empty()) throw osrm::util::exception("Missing time zone geojson file"); - std::ifstream file(tz_shapes_filename.data()); + boost::filesystem::ifstream file(tz_shapes_filename); if (!file.is_open()) - throw osrm::util::exception("failed to open " + tz_shapes_filename); + throw osrm::util::exception("failed to open " + tz_shapes_filename.string()); - util::Log() << "Parsing " + tz_shapes_filename; + util::Log() << "Parsing " + tz_shapes_filename.string(); rapidjson::IStreamWrapper isw(file); rapidjson::Document geojson; geojson.ParseStream(isw); @@ -93,15 +51,89 @@ void Timezoner::LoadLocalTimesRTree(const std::string &tz_shapes_filename, std:: { auto error_code = geojson.GetParseError(); auto error_offset = geojson.GetErrorOffset(); - throw osrm::util::exception("Failed to parse " + tz_shapes_filename + " with error " + + throw osrm::util::exception("Failed to parse " + tz_shapes_filename.string() + " with error " + std::to_string(error_code) + ". JSON malformed at " + std::to_string(error_offset)); } - if (!geojson.HasMember("FeatureCollection")) - throw osrm::util::exception("Failed to parse " + tz_shapes_filename + - ". Expecting a geojson feature collection."); - if (!geojson["FeatureCollection"].GetObject().HasMember("features")) - throw osrm::util::exception("Failed to parse " + tz_shapes_filename + - ". Missing features list."); + LoadLocalTimesRTree(geojson, utc_time_now); +} + +void Timezoner::ValidateCoordinate(const rapidjson::Value &coordinate) +{ + if (!coordinate.IsArray()) + throw osrm::util::exception("Failed to parse time zone file. Feature geometry has a non-array coordinate."); + if (coordinate.Capacity() != 2) + { + throw osrm::util::exception("Failed to parse time zone file. Feature geometry has a malformed coordinate with more than 2 values."); + } else { + for (rapidjson::SizeType i = 0; i < coordinate.Size(); i++) + { + if (!coordinate[i].IsNumber() && !coordinate[i].IsDouble()) + throw osrm::util::exception("Failed to parse time zone file. Feature geometry has a non-number coordinate."); + } + } +} + +void Timezoner::ValidateFeature(const rapidjson::Value &feature) +{ + if (!feature.HasMember("type")) + { + throw osrm::util::exception("Failed to parse time zone file. Feature is missing type member."); + } else if (!feature["type"].IsString()) + { + throw osrm::util::exception("Failed to parse time zone file. Feature has non-string type member."); + } + if (!feature.HasMember("properties")) + { + throw osrm::util::exception("Failed to parse time zone file. Feature is missing properties member."); + } + else if (!feature.GetObject()["properties"].IsObject()) + { + throw osrm::util::exception("Failed to parse time zone file. Feature has non-object properties member."); + } + if (!feature["properties"].GetObject().HasMember("TZID")) + { + throw osrm::util::exception("Failed to parse time zone file. Feature is missing TZID member in properties."); + } + else if (!feature["properties"].GetObject()["TZID"].IsString()) + { + throw osrm::util::exception("Failed to parse time zone file. Feature has non-string TZID value."); + } + if (!feature.HasMember("geometry")) + { + throw osrm::util::exception("Failed to parse time zone file. Feature is missing geometry member."); + } + else if (!feature.GetObject()["geometry"].IsObject()) + { + throw osrm::util::exception("Failed to parse time zone file. Feature non-object geometry member."); + } + + if (!feature["geometry"].GetObject().HasMember("type")) + { + throw osrm::util::exception("Failed to parse time zone file. Feature geometry is missing type member."); + } else if (!feature["geometry"].GetObject()["type"].IsString()) { + throw osrm::util::exception("Failed to parse time zone file. Feature geometry has non-string type member."); + } + if (!feature["geometry"].GetObject().HasMember("coordinates")) + { + throw osrm::util::exception("Failed to parse time zone file. Feature geometry is missing coordinates member."); + } else if (!feature["geometry"].GetObject()["coordinates"].IsArray()) { + throw osrm::util::exception("Failed to parse time zone file. Feature geometry has a non-array coordinates member."); + } + const auto coord_array = feature["geometry"].GetObject()["coordinates"].GetArray(); + if (coord_array.Empty()) + throw osrm::util::exception("Failed to parse time zone file. Feature geometry coordinates member is empty."); + if (!coord_array[0].IsArray()) + throw osrm::util::exception("Failed to parse time zone file. Feature geometry coordinates array has non-array outer ring."); +} + +void Timezoner::LoadLocalTimesRTree(rapidjson::Document &geojson, std::time_t utc_time) +{ + if (!geojson.HasMember("type")) + throw osrm::util::exception("Failed to parse time zone file. Missing type member."); + if (!geojson["type"].IsString()) + throw osrm::util::exception("Failed to parse time zone file. Missing string-based type member."); + if (!geojson.HasMember("features")) + throw osrm::util::exception("Failed to parse time zone file. Missing features list."); // Lambda function that returns local time in the tzname time zone // Thread safety: MT-Unsafe const:env @@ -120,11 +152,11 @@ void Timezoner::LoadLocalTimesRTree(const std::string &tz_shapes_filename, std:: return it->second; }; BOOST_ASSERT(geojson["features"].IsArray()); - const rapidjson::Value &features_array = geojson["features"].GetArray(); + const auto &features_array = geojson["features"].GetArray(); std::vector polygons; for (rapidjson::SizeType i = 0; i < features_array.Size(); i++) { - ValidateFeature(features_array[i], tz_shapes_filename); + ValidateFeature(features_array[i]); const std::string &feat_type = features_array[i].GetObject()["geometry"].GetObject()["type"].GetString(); if (feat_type == "polygon") { @@ -137,7 +169,7 @@ void Timezoner::LoadLocalTimesRTree(const std::string &tz_shapes_filename, std:: .GetArray(); for (rapidjson::SizeType i = 0; i < coords_outer_array.Size(); ++i) { - // polygon.outer().emplace_back(object->padfX[vertex], object->padfY[vertex]); + ValidateCoordinate(coords_outer_array[i]); const auto &coords = coords_outer_array[i].GetArray(); polygon.outer().emplace_back(coords[0].GetDouble(), coords[1].GetDouble()); } @@ -151,9 +183,10 @@ void Timezoner::LoadLocalTimesRTree(const std::string &tz_shapes_filename, std:: } else { - util::Log() << "Skipping non-polygon shape in timezone file " + tz_shapes_filename; + util::Log(logDEBUG) << "Skipping non-polygon shape in timezone file"; } } + util::Log() << "Parsed " << polygons.size() << "time zone polygons." << std::endl; // Create R-tree for collected shape polygons rtree = rtree_t(polygons); } diff --git a/unit_tests/CMakeLists.txt b/unit_tests/CMakeLists.txt index 835059018..5fb90e1b4 100644 --- a/unit_tests/CMakeLists.txt +++ b/unit_tests/CMakeLists.txt @@ -14,6 +14,10 @@ file(GLOB CustomizerTestsSources customizer_tests.cpp customizer/*.cpp) +file(GLOB UpdaterTestsSources + updater_tests.cpp + updater/*.cpp) + file(GLOB LibraryTestsSources library_tests.cpp library/*.cpp) @@ -55,6 +59,11 @@ add_executable(customizer-tests ${CustomizerTestsSources} $ $ $) +add_executable(updater-tests + EXCLUDE_FROM_ALL + ${UpdaterTestsSources} + $ $ $) + add_executable(library-tests EXCLUDE_FROM_ALL ${LibraryTestsSources}) @@ -82,6 +91,7 @@ if(NOT WIN32 AND NOT Boost_USE_STATIC_LIBS) add_definitions(-DBOOST_TEST_DYN_LINK) endif() +set(UPDATER_TEST_DATA_DIR "${CMAKE_SOURCE_DIR}/unit_tests/updater") set(TEST_DATA_DIR "${CMAKE_SOURCE_DIR}/test/data") add_dependencies(library-tests osrm-extract osrm-contract osrm-partition) # We can't run this Makefile on windows @@ -93,6 +103,7 @@ target_compile_definitions(extractor-tests PRIVATE COMPILE_DEFINITIONS OSRM_FIXT target_compile_definitions(library-tests PRIVATE COMPILE_DEFINITIONS OSRM_TEST_DATA_DIR="${TEST_DATA_DIR}") target_compile_definitions(library-extract-tests PRIVATE COMPILE_DEFINITIONS OSRM_TEST_DATA_DIR="${TEST_DATA_DIR}") target_compile_definitions(library-contract-tests PRIVATE COMPILE_DEFINITIONS OSRM_TEST_DATA_DIR="${TEST_DATA_DIR}") +target_compile_definitions(updater-tests PRIVATE COMPILE_DEFINITIONS TEST_DATA_DIR="${UPDATER_TEST_DATA_DIR}") target_include_directories(engine-tests PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(library-tests PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) @@ -101,16 +112,18 @@ target_include_directories(library-contract-tests PUBLIC ${CMAKE_CURRENT_SOURCE_ target_include_directories(util-tests PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(partition-tests PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(customizer-tests PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(updater-tests PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(engine-tests ${ENGINE_LIBRARIES} ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY} ${MAYBE_SHAPEFILE}) -target_link_libraries(extractor-tests ${EXTRACTOR_LIBRARIES} ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY} ${MAYBE_SHAPEFILE}) -target_link_libraries(partition-tests ${PARTITIONER_LIBRARIES} ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY} ${MAYBE_SHAPEFILE}) -target_link_libraries(customizer-tests ${CUSTOMIZER_LIBRARIES} ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY} ${MAYBE_SHAPEFILE}) +target_link_libraries(engine-tests ${ENGINE_LIBRARIES} ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) +target_link_libraries(extractor-tests ${EXTRACTOR_LIBRARIES} ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) +target_link_libraries(partition-tests ${PARTITIONER_LIBRARIES} ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) +target_link_libraries(customizer-tests ${CUSTOMIZER_LIBRARIES} ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) +target_link_libraries(updater-tests ${UPDATER_LIBRARIES} ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) target_link_libraries(library-tests osrm ${ENGINE_LIBRARIES} ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) target_link_libraries(library-extract-tests osrm_extract ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) target_link_libraries(library-contract-tests osrm_contract ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) -target_link_libraries(server-tests osrm ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY} ${MAYBE_SHAPEFILE}) -target_link_libraries(util-tests ${UTIL_LIBRARIES} ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY} ${MAYBE_SHAPEFILE}) +target_link_libraries(server-tests osrm ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) +target_link_libraries(util-tests ${UTIL_LIBRARIES} ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) add_custom_target(tests - DEPENDS engine-tests extractor-tests partition-tests customizer-tests library-tests library-extract-tests server-tests util-tests) + DEPENDS engine-tests extractor-tests partition-tests updater-tests customizer-tests library-tests library-extract-tests server-tests util-tests) diff --git a/unit_tests/updater/test.geojson b/unit_tests/updater/test.geojson new file mode 100644 index 000000000..fd1ff4774 --- /dev/null +++ b/unit_tests/updater/test.geojson @@ -0,0 +1,38 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "TZID": "Europe\\/Berlin" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 8.28369140625, + 48.88277959345126 + ], + [ + 8.57757568359375, + 48.88277959345126 + ], + [ + 8.57757568359375, + 49.07206662261101 + ], + [ + 8.28369140625, + 49.07206662261101 + ], + [ + 8.28369140625, + 48.88277959345126 + ] + ], [] + ] + } + } + ] +} diff --git a/unit_tests/updater/timezoner.cpp b/unit_tests/updater/timezoner.cpp new file mode 100644 index 000000000..fef7aa444 --- /dev/null +++ b/unit_tests/updater/timezoner.cpp @@ -0,0 +1,62 @@ +#include "util/exception.hpp" +#include "util/timezones.hpp" + +#include +#include + +BOOST_AUTO_TEST_SUITE(timezoner) + +using namespace osrm; +using namespace osrm::updater; + +BOOST_AUTO_TEST_CASE(timezoner_test) +{ + const char json[] = + "{ \"type\" : \"FeatureCollection\", \"features\": [" + "{ \"type\" : \"Feature\"," + "\"properties\" : { \"TZID\" : \"Europe/Berlin\"}, \"geometry\" : { \"type\": \"polygon\", " + "\"coordinates\": [[[8.28369,48.88277], [8.57757, " + "48.88277], [8.57757, 49.07206], [8.28369, " + "49.07206], [8.28369, 48.88277]]] }} ]}"; + std::time_t now = time(0); + BOOST_CHECK_NO_THROW(Timezoner tz(json, now)); + + boost::filesystem::path test_path(TEST_DATA_DIR "/test.geojson"); + BOOST_CHECK_NO_THROW(Timezoner tz(test_path, now)); + + // missing opening bracket + const char bad[] = + "\"type\" : \"FeatureCollection\", \"features\": [" + "{ \"type\" : \"Feature\"," + "\"properties\" : { \"TZID\" : \"Europe/Berlin\"}, \"geometry\" : { \"type\": \"polygon\", " + "\"coordinates\": [[[8.28369,48.88277], [8.57757, " + "48.88277], [8.57757, 49.07206], [8.28369, " + "49.07206], [8.28369, 48.88277]]] }} ]}"; + BOOST_CHECK_THROW(Timezoner tz(bad, now), util::exception); + + // missing/malformed FeatureCollection type field + const char missing_type[] = + "{ \"FeatureCollection\", \"features\": [" + "{ \"type\" : \"Feature\"," + "\"properties\" : { \"TZID\" : \"Europe/Berlin\"}, \"geometry\" : { \"type\": \"polygon\", " + "\"coordinates\": [[[8.28369,48.88277], [8.57757, " + "48.88277], [8.57757, 49.07206], [8.28369, " + "49.07206], [8.28369, 48.88277]]] }} ]}"; + BOOST_CHECK_THROW(Timezoner tz(missing_type, now), util::exception); + + const char missing_featc[] = + "{ \"type\" : \"Collection\", \"features\": [" + "{ \"type\" : \"Feature\"," + "\"properties\" : { \"TZID\" : \"Europe/Berlin\"}, \"geometry\" : { \"type\": \"polygon\", " + "\"coordinates\": [[[8.28369,48.88277], [8.57757, " + "48.88277], [8.57757, 49.07206], [8.28369, " + "49.07206], [8.28369, 48.88277]]] }} ]}"; + BOOST_CHECK_THROW(Timezoner tz(missing_featc, now), util::exception); + + // missing features list + const char missing_features[] = + "{ \"type\" : \"FeatureCollection\", \"features\": null }"; + BOOST_CHECK_THROW(Timezoner tz(missing_features, now), util::exception); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/unit_tests/updater_tests.cpp b/unit_tests/updater_tests.cpp new file mode 100644 index 000000000..1d4ad85d7 --- /dev/null +++ b/unit_tests/updater_tests.cpp @@ -0,0 +1,7 @@ +#define BOOST_TEST_MODULE customizer tests + +#include + +/* + * This file will contain an automatically generated main function. + */