diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..b0a8210 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,71 @@ +name: coverage-ci + +on: + workflow_dispatch: + + push: + branches: + - master + - feature/** + - improvement/** + - bugfix/** + + pull_request: + branches: + - master + - feature/** + - improvement/** + - bugfix/** + +jobs: + test_coverage: + if: >- + ! contains(toJSON(github.event.commits.*.message), '[skip ci]') && + ! contains(toJSON(github.event.commits.*.message), '[skip github]') + + runs-on: ubuntu-latest + + name: "Coverage" + + container: + image: gcc:latest + options: -v /usr/local:/host_usr_local + + steps: + - uses: actions/checkout@v1 + + - uses: friendlyanon/fetch-core-count@v1 + id: cores + + - name: CMake + run: echo "/host_usr_local/bin" >> $GITHUB_PATH + + - name: Install dependencies + run: script/ci_install_deps.sh + + - name: Install test coverage tools + run: | + apt update + apt install -y gcovr lcov + + - name: Configure + run: cmake -S test -B build -D CMAKE_BUILD_TYPE=Debug -D CMAKE_CXX_FLAGS="-Wall -fprofile-arcs -ftest-coverage --coverage" + + - name: Build + run: cmake --build build -j ${{steps.cores.outputs.count}} + + - name: Run + working-directory: build + run: ctest --output-on-failure -j ${{steps.cores.outputs.count}} + + - name: Generate coverage report + run: | + lcov -d . -c -o out.info --rc lcov_branch_coverage=1 --no-external + lcov -e out.info '*include/ss*hpp' -o filtered.info + + - name: Invoke coveralls + uses: coverallsapp/github-action@v2 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + file: filtered.info + format: lcov diff --git a/README.md b/README.md index 85b5877..7a4c2de 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ [![windows-msys2-clang](https://github.com/red0124/ssp/workflows/win-msys2-clang-ci/badge.svg)](https://github.com/red0124/ssp/actions/workflows/win-msys2-clang.yml) [![win-msvc-ci](https://github.com/red0124/ssp/workflows/win-msvc-ci/badge.svg)](https://github.com/red0124/ssp/actions/workflows/win-msvc.yml) [![single-header-ci](https://github.com/red0124/ssp/workflows/single-header-ci/badge.svg)](https://github.com/red0124/ssp/actions/workflows/single-header.yml) +[![coverage](https://coveralls.io/repos/github/red0124/ssp/badge.svg?branch=master/coverage_ci)](https://coveralls.io/github/red0124/ssp?branch=master/coverage_ci) A header only "csv" parser which is fast and versatile with modern C++ api. Requires compiler with C++17 support. [Can also be used to convert strings to specific types.](#the-converter) diff --git a/include/ss/converter.hpp b/include/ss/converter.hpp index 8fce18d..c91fee7 100644 --- a/include/ss/converter.hpp +++ b/include/ss/converter.hpp @@ -379,7 +379,6 @@ private: return extract_tuple(elems); } - // do not know how to specialize by return type :( template no_void_validator_tup_t> convert_impl( const split_data& elems, const std::tuple*) { diff --git a/include/ss/extract.hpp b/include/ss/extract.hpp index 246ea9a..a26828d 100644 --- a/include/ss/extract.hpp +++ b/include/ss/extract.hpp @@ -1,13 +1,13 @@ #pragma once #include "type_traits.hpp" +#include #include #include #include #include #include #include -#include #ifndef SSP_DISABLE_FAST_FLOAT #include @@ -16,7 +16,6 @@ #include #endif -// TODO try from_chars for integer conversions namespace ss { //////////////// @@ -42,17 +41,24 @@ std::enable_if_t, std::optional> to_num( template std::enable_if_t, std::optional> to_num( const char* const begin, const char* const end) { + static_assert(!std::is_same_v, + "Conversion to long double is disabled"); + constexpr static auto buff_max = 64; - char buff[buff_max]; + char short_buff[buff_max]; size_t string_range = std::distance(begin, end); + std::string long_buff; + char* buff; if (string_range > buff_max) { - return std::nullopt; + long_buff = std::string{begin, end}; + buff = long_buff.data(); + } else { + buff = short_buff; + buff[string_range] = '\0'; + std::copy_n(begin, string_range, buff); } - std::copy_n(begin, string_range, buff); - buff[string_range] = '\0'; - T ret; char* parse_end = nullptr; @@ -60,8 +66,6 @@ std::enable_if_t, std::optional> to_num( ret = std::strtof(buff, &parse_end); } else if constexpr (std::is_same_v) { ret = std::strtod(buff, &parse_end); - } else if constexpr (std::is_same_v) { - ret = std::strtold(buff, &parse_end); } if (parse_end != buff + string_range) { diff --git a/include/ss/parser.hpp b/include/ss/parser.hpp index 54126bb..db80dd3 100644 --- a/include/ss/parser.hpp +++ b/include/ss/parser.hpp @@ -1,6 +1,5 @@ #pragma once -// TODO add single header tests #include "common.hpp" #include "converter.hpp" #include "exception.hpp" diff --git a/ssp.hpp b/ssp.hpp index e6fbdba..cb0bc2e 100644 --- a/ssp.hpp +++ b/ssp.hpp @@ -1465,7 +1465,6 @@ public: #else #endif -// TODO try from_chars for integer conversions namespace ss { //////////////// @@ -1491,17 +1490,24 @@ std::enable_if_t, std::optional> to_num( template std::enable_if_t, std::optional> to_num( const char* const begin, const char* const end) { + static_assert(!std::is_same_v, + "Conversion to long double is disabled"); + constexpr static auto buff_max = 64; - char buff[buff_max]; + char short_buff[buff_max]; size_t string_range = std::distance(begin, end); + std::string long_buff; + char* buff; if (string_range > buff_max) { - return std::nullopt; + long_buff = std::string{begin, end}; + buff = long_buff.data(); + } else { + buff = short_buff; + buff[string_range] = '\0'; + std::copy_n(begin, string_range, buff); } - std::copy_n(begin, string_range, buff); - buff[string_range] = '\0'; - T ret; char* parse_end = nullptr; @@ -1509,8 +1515,6 @@ std::enable_if_t, std::optional> to_num( ret = std::strtof(buff, &parse_end); } else if constexpr (std::is_same_v) { ret = std::strtod(buff, &parse_end); - } else if constexpr (std::is_same_v) { - ret = std::strtold(buff, &parse_end); } if (parse_end != buff + string_range) { @@ -2017,7 +2021,6 @@ private: return extract_tuple(elems); } - // do not know how to specialize by return type :( template no_void_validator_tup_t> convert_impl( const split_data& elems, const std::tuple*) { @@ -2133,7 +2136,6 @@ private: } /* ss */ -// TODO add single header tests namespace ss { diff --git a/test/test_extractions.cpp b/test/test_extractions.cpp index b8dd858..4cab853 100644 --- a/test/test_extractions.cpp +++ b/test/test_extractions.cpp @@ -275,3 +275,20 @@ TEST_CASE("extract test functions for std::variant") { } } } + +TEST_CASE("extract test with long number string") { + { + std::string string_num = + std::string(20, '1') + "." + std::string(20, '2'); + + CHECK_FLOATING_CONVERSION_LONG_NUMBER(string_num, float, stof); + CHECK_FLOATING_CONVERSION_LONG_NUMBER(string_num, double, stod); + } + + { + std::string string_num = + std::string(50, '1') + "." + std::string(50, '2'); + + CHECK_FLOATING_CONVERSION_LONG_NUMBER(string_num, double, stod); + } +} diff --git a/test/test_extractions_without_fast_float.cpp b/test/test_extractions_without_fast_float.cpp index 3f5cd40..2c9a984 100644 --- a/test/test_extractions_without_fast_float.cpp +++ b/test/test_extractions_without_fast_float.cpp @@ -9,11 +9,11 @@ TEST_CASE( CHECK_FLOATING_CONVERSION(123.456, float); CHECK_FLOATING_CONVERSION(123.456, double); - CHECK_FLOATING_CONVERSION(69, float); - CHECK_FLOATING_CONVERSION(69, double); + CHECK_FLOATING_CONVERSION(59, float); + CHECK_FLOATING_CONVERSION(59, double); - CHECK_FLOATING_CONVERSION(420., float); - CHECK_FLOATING_CONVERSION(420., double); + CHECK_FLOATING_CONVERSION(4210., float); + CHECK_FLOATING_CONVERSION(4210., double); CHECK_FLOATING_CONVERSION(0.123, float); CHECK_FLOATING_CONVERSION(0.123, double); @@ -130,3 +130,20 @@ TEST_CASE("extract test functions for std::variant without fast float") { } } } + +TEST_CASE("extract test with long number string without fast float") { + { + std::string string_num = + std::string(20, '1') + "." + std::string(20, '2'); + + CHECK_FLOATING_CONVERSION_LONG_NUMBER(string_num, float, stof); + CHECK_FLOATING_CONVERSION_LONG_NUMBER(string_num, double, stod); + } + + { + std::string string_num = + std::string(50, '1') + "." + std::string(50, '2'); + + CHECK_FLOATING_CONVERSION_LONG_NUMBER(string_num, double, stod); + } +} diff --git a/test/test_helpers.hpp b/test/test_helpers.hpp index b7f362f..15d20f6 100644 --- a/test/test_helpers.hpp +++ b/test/test_helpers.hpp @@ -1,10 +1,10 @@ #pragma once #include +#include #include #include #include #include -#include #ifdef CMAKE_GITHUB_CI #include @@ -75,6 +75,18 @@ struct unique_file_name { CHECK_LT(std::abs(t.value() - type(-input)), eps); \ } +#define CHECK_FLOATING_CONVERSION_LONG_NUMBER(STRING_NUMBER, TYPE, CONVERTER) \ + { \ + auto begin = STRING_NUMBER.c_str(); \ + auto end = begin + STRING_NUMBER.size(); \ + \ + auto number = ss::to_num(begin, end); \ + REQUIRE(number.has_value()); \ + \ + auto expected_number = CONVERTER(STRING_NUMBER); \ + CHECK_EQ(number.value(), expected_number); \ + } + #define CHECK_INVALID_CONVERSION(input, type) \ { \ std::string s = input; \ diff --git a/test/test_parser.cpp b/test/test_parser.cpp index 94e6c4c..e7089ff 100644 --- a/test/test_parser.cpp +++ b/test/test_parser.cpp @@ -1491,6 +1491,104 @@ TEST_CASE("parser test invalid header fields usage") { test_invalid_fields({"Int,String,Int", "1,hi,3"}, {"Int", "String", "Int"}); } +template +void test_invalid_rows_with_header() { + unique_file_name f{"test_parser"}; + { + std::ofstream out{f.name}; + out << "Int,String,Double" << std::endl; + out << "1,line1,2.34" << std::endl; + out << "2,line2" << std::endl; + out << "3,line3,67.8" << std::endl; + out << "4,line4,67.8,9" << std::endl; + out << "5,line5,9.10" << std::endl; + out << "six,line6,10.11" << std::endl; + } + + { + ss::parser p{f.name}; + + p.use_fields("Int", "String", "Double"); + using data = std::tuple; + std::vector i; + + CHECK(p.valid()); + + while (!p.eof()) { + try { + const auto& t = p.template get_next(); + if (p.valid()) { + i.push_back(t); + } + } catch (const ss::exception&) { + continue; + } + } + + std::vector expected = {{1, "line1", 2.34}, + {3, "line3", 67.8}, + {5, "line5", 9.10}}; + CHECK_EQ(i, expected); + } + + { + ss::parser p{f.name}; + + p.use_fields("Double", "Int"); + using data = std::tuple; + std::vector i; + + CHECK(p.valid()); + + while (!p.eof()) { + try { + const auto& t = p.template get_next(); + if (p.valid()) { + i.push_back(t); + } + } catch (const ss::exception&) { + continue; + } + } + + std::vector expected = {{2.34, 1}, {67.8, 3}, {9.10, 5}}; + CHECK_EQ(i, expected); + } + + { + ss::parser p{f.name}; + + p.use_fields("String", "Double"); + using data = std::tuple; + std::vector i; + + CHECK(p.valid()); + + while (!p.eof()) { + try { + const auto& t = p.template get_next(); + if (p.valid()) { + i.push_back(t); + } + } catch (const ss::exception&) { + continue; + } + } + + std::vector expected = {{"line1", 2.34}, + {"line3", 67.8}, + {"line5", 9.10}, + {"line6", 10.11}}; + CHECK_EQ(i, expected); + } +} + +TEST_CASE("parser test invalid rows with header") { + test_invalid_rows_with_header(); + test_invalid_rows_with_header(); + test_invalid_rows_with_header(); +} + template void test_ignore_empty_impl(const std::vector& data) { unique_file_name f{"test_parser"}; diff --git a/test/test_splitter.cpp b/test/test_splitter.cpp index bf3ba16..201de9d 100644 --- a/test/test_splitter.cpp +++ b/test/test_splitter.cpp @@ -775,6 +775,23 @@ TEST_CASE("splitter test resplit unterminated quote") { CHECK_EQ(words(vec), expected); } } + + { + ss::converter, ss::escape<'\\'>, ss::multiline> c; + auto& s = c.splitter; + auto vec = expect_unterminated_quote(s, R"("just\"some","ra)"); + std::vector expected{"just\"some"}; + auto w = words(vec); + w.pop_back(); + CHECK_EQ(w, expected); + REQUIRE(s.unterminated_quote()); + { + auto new_line = buff.append(R"(n,dom",str\"ings)"); + // invalid resplit size + vec = c.resplit(new_line, 4); + CHECK(!s.valid()); + } + } } TEST_CASE("splitter test resplit unterminated quote with exceptions") { @@ -1040,47 +1057,57 @@ TEST_CASE("splitter test resplit unterminated quote with exceptions") { } } -TEST_CASE("splitter test invalid splits") { - ss::converter, ss::trim<' '>, - ss::escape<'\\'>> - c; +template +void test_invalid_splits() { + ss::converter, ss::trim<' '>, ss::escape<'\\'>, Ts...> c; auto& s = c.splitter; + auto check_error_msg = [&] { + if constexpr (ss::setup::string_error) { + CHECK_FALSE(s.error_msg().empty()); + } + }; + // empty delimiter s.split(buff("some,random,strings"), ""); CHECK_FALSE(s.valid()); CHECK_FALSE(s.unterminated_quote()); - CHECK_FALSE(s.error_msg().empty()); + check_error_msg(); // mismatched delimiter s.split(buff(R"(some,"random,"strings")")); CHECK_FALSE(s.valid()); CHECK_FALSE(s.unterminated_quote()); - CHECK_FALSE(s.error_msg().empty()); + check_error_msg(); // unterminated escape s.split(buff(R"(some,random,strings\)")); CHECK_FALSE(s.valid()); CHECK_FALSE(s.unterminated_quote()); - CHECK_FALSE(s.error_msg().empty()); + check_error_msg(); // unterminated escape s.split(buff(R"(some,random,"strings\)")); CHECK_FALSE(s.valid()); CHECK_FALSE(s.unterminated_quote()); - CHECK_FALSE(s.error_msg().empty()); + check_error_msg(); // unterminated quote s.split(buff("some,random,\"strings")); CHECK_FALSE(s.valid()); CHECK(s.unterminated_quote()); - CHECK_FALSE(s.error_msg().empty()); + check_error_msg(); // invalid resplit char new_line[] = "some"; c.resplit(new_line, strlen(new_line)); CHECK_FALSE(s.valid()); - CHECK_FALSE(s.error_msg().empty()); + check_error_msg(); +} + +TEST_CASE("splitter test invalid splits") { + test_invalid_splits(); + test_invalid_splits(); } TEST_CASE("splitter test invalid splits with exceptions") {