diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 5bf54b0..cbff088 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -24,7 +24,7 @@ jobs: matrix: name: [ - ubuntu-latest-gcc-12, + ubuntu-22.04-gcc-12, ubuntu-20.04-clang-13, ubuntu-22.04-clang-14, ubuntu-22.04-clang-15, @@ -33,8 +33,8 @@ jobs: build_type: [Debug, Release] include: - - name: ubuntu-latest-gcc-12 - os: ubuntu-latest + - name: ubuntu-22.04-gcc-12 + os: ubuntu-22.04 compiler: gcc version: "12" diff --git a/include/flexi_cfg/config/classes.h b/include/flexi_cfg/config/classes.h index e4e0e0f..30104cf 100644 --- a/include/flexi_cfg/config/classes.h +++ b/include/flexi_cfg/config/classes.h @@ -10,9 +10,10 @@ #include #include #include +#include -#include "flexi_cfg/utils.h" #include "flexi_cfg/logger.h" +#include "flexi_cfg/utils.h" #define DEBUG_CLASSES 0 #define PRINT_SRC 0 // NOLINT(cppcoreguidelines-macro-usage) @@ -178,10 +179,13 @@ class ConfigList : public ConfigBaseClonable { void stream(std::ostream& os) const override { os << "["; - for (size_t i = 0; i < data.size() - 1; ++i) { - os << data[i] << ", "; + for (auto it = data.begin(); it != data.end(); it = std::next(it)) { + os << *it; + if (std::next(it) != data.end()) { + os << ", "; + } } - os << data.back() << "]"; + os << "]"; } std::vector> data; diff --git a/include/flexi_cfg/config/grammar.h b/include/flexi_cfg/config/grammar.h index af37b56..0a0d2f5 100644 --- a/include/flexi_cfg/config/grammar.h +++ b/include/flexi_cfg/config/grammar.h @@ -98,18 +98,25 @@ struct BOOLEAN : peg::sor {}; struct STRING : peg::seq, peg::plus>, peg::one<'"'>> {}; +template +struct LIST_CONTENT_ : peg::list, peg::space> { using element = Element; }; + +template +// TODO: Figure out how to support 'peg::if_must<>' here instead of 'peg::seq<>' so that we can get +// a better error message. +struct LIST_ : peg::seq, TAIL, SBc> { + using begin = SBo; + using end = SBc; + using element = typename Content::element; +}; + struct LIST; struct VALUE_LOOKUP; struct VALUE : peg::sor {}; // 'seq' is used here so that the 'VALUE' action will collect the location information. struct LIST_ELEMENT : peg::seq {}; -struct LIST_CONTENT : peg::list {}; -// Should the 'space' here be a 'blank'? Allow multi-line lists (w/o \)? -struct LIST : peg::seq { - using begin = SBo; - using end = SBc; - using element = LIST_ELEMENT; -}; +struct LIST_CONTENT : LIST_CONTENT_ {}; +struct LIST : LIST_ {}; struct EXPRESSION : peg::seq {}; @@ -128,13 +135,8 @@ struct VALUE_LOOKUP : peg::seq {}; -struct PROTO_LIST_CONTENT : peg::list_must {}; -// Should the 'space' here be a 'blank'? Allow multi-line lists (w/o \)? -struct PROTO_LIST : peg::if_must { - using begin = SBo; - using end = SBc; - using element = PROTO_LIST_ELEMENT; -}; +struct PROTO_LIST_CONTENT : LIST_CONTENT_ {}; +struct PROTO_LIST : LIST_ {}; struct KV_NOMINAL : peg::sor {}; @@ -208,6 +210,9 @@ inline constexpr const char* error_message = nullptr; // clang-format off template <> inline constexpr auto error_message = "expected a closing '}'"; +template <> inline constexpr auto error_message = "invalid list in 'struct'"; +template <> inline constexpr auto error_message = "invalid list in 'struct'"; +template <> inline constexpr auto error_message = "invalid element in struct list"; template <> inline constexpr auto error_message = "invalid list in 'proto'"; template <> inline constexpr auto error_message = "invalid list in 'proto'"; template <> inline constexpr auto error_message = "invalid element in proto list"; diff --git a/tests/config_grammar_test.cpp b/tests/config_grammar_test.cpp index 980fa09..e4a5d75 100644 --- a/tests/config_grammar_test.cpp +++ b/tests/config_grammar_test.cpp @@ -31,7 +31,7 @@ template void checkResult(const std::string& input, flexi_cfg::config::types::Type expected_type, std::optional& out) { std::optional ret; - ASSERT_NO_THROW(ret.emplace(runTest(input))); + ASSERT_NO_THROW(ret.emplace(runTest(input))) << "INPUT: '" << input << "'"; ASSERT_TRUE(ret.has_value()); if (ret.has_value()) { ASSERT_TRUE(ret.value().first); @@ -376,6 +376,29 @@ TEST(ConfigGrammar, STRING) { } } +const std::vector list_test_cases = { + "[1, 2, 3]", "[1.0, 2., -3.3]", R"(["one", "two", "three"])", "[0x123, 0Xabc, 0xA1B2F9]", + "[0.123, $(ref.var), 3.456]", + // TODO(miker2): Add support for expressions in lists + // R"([12, {{ 2^14 - 1}}, 0.32])", // Expressions in lists + + // Verify that lists can contain newlines + R"([1, + 2, + 3])", + // Verify that a list can contain leading and trailing comments: + R"([# comment + 1, 2, 3 # comment + # comment + ])", + "[]", // Empty lists are supported + // Verify that an empty list containing a comment is supported + R"([ + # This is a multi-line + # comment + ])", + "[$(ref.var2), $(ref.var1), 3.456]"}; + TEST(ConfigGrammar, LIST) { auto checkList = [](const std::string& input) { std::optional out; @@ -383,30 +406,10 @@ TEST(ConfigGrammar, LIST) { flexi_cfg::config::types::ConfigList>(input, flexi_cfg::config::types::Type::kList, out); }; - { - const std::string content = "[1, 2, 3]"; - checkList(content); - } - { - const std::string content = "[1.0, 2., -3.3]"; - checkList(content); - } - { - const std::string content = R"(["one", "two", "three"])"; - checkList(content); - } - { - const std::string content = "[0x123, 0Xabc, 0xA1B2F9]"; - checkList(content); - } - { - const std::string content = "[0.123, $(ref.var), 3.456]"; - checkList(content); - } - { - const std::string content = "[$(ref.var2), $(ref.var1), 3.456]"; + for (const auto& content : list_test_cases) { checkList(content); } + { // Non-homogeneous lists are not allowed const std::string content = R"([12, "two", 10.2])"; @@ -589,8 +592,11 @@ TEST(ConfigGrammar, PROTOLIST) { checkResult, flexi_cfg::config::types::ConfigList>(input, flexi_cfg::config::types::Type::kList, out); - out->print(std::cout); }; + // All valid "LIST" cases are also valid "PROTO_LIST" cases + for (const auto& content : list_test_cases) { + checkProtoList(content); + } { const std::string content = "[3, 4, ${TEST}]"; checkProtoList(content); diff --git a/tests/config_parse_test.cpp b/tests/config_parse_test.cpp index 3e63017..867c32d 100644 --- a/tests/config_parse_test.cpp +++ b/tests/config_parse_test.cpp @@ -26,7 +26,7 @@ TEST_P(InputString, ParseTree) { } TEST_P(InputString, Parse) { - flexi_cfg::logger::setLevel(flexi_cfg::logger::Severity::INFO); + setLevel(flexi_cfg::logger::Severity::INFO); auto parse = []() { peg::memory_input in(GetParam(), "From content"); flexi_cfg::config::ActionData out; @@ -39,7 +39,7 @@ TEST_P(InputString, Parse) { } TEST_P(InputString, Reader) { - flexi_cfg::logger::setLevel(flexi_cfg::logger::Severity::INFO); + setLevel(flexi_cfg::logger::Severity::INFO); flexi_cfg::Reader cfg({}, ""); // Nominally, we wouldn't do this, but we need a mechanism to // capture the output of 'parse' from within the "try/catch" block @@ -63,8 +63,19 @@ TEST_P(InputString, Reader) { EXPECT_EQ(cfg.getValue("test2.n_key"), true); EXPECT_EQ(cfg.getType("test2.n_key"), flexi_cfg::config::types::Type::kBoolean); EXPECT_TRUE(cfg.exists("test2.inner.list")); - EXPECT_EQ(cfg.getValue>("test2.inner.list"), std::vector({1, 2, 3, 4})); EXPECT_EQ(cfg.getType("test2.inner.list"), flexi_cfg::config::types::Type::kList); + EXPECT_EQ(cfg.getValue>("test2.inner.list"), std::vector({1, 2, 3, 4})); + EXPECT_TRUE(cfg.exists("test2.inner.emptyList")); + EXPECT_EQ(cfg.getType("test2.inner.emptyList"), flexi_cfg::config::types::Type::kList); + EXPECT_EQ(cfg.getValue>("test2.inner.emptyList"), std::vector({})); + EXPECT_TRUE(cfg.exists("test2.inner.listWithComment")); + EXPECT_EQ(cfg.getType("test2.inner.listWithComment"), flexi_cfg::config::types::Type::kList); + EXPECT_EQ(cfg.getValue>("test2.inner.listWithComment"), std::vector({0, 2})); + EXPECT_TRUE(cfg.exists("test2.inner.listWithTrailingComment")); + EXPECT_EQ(cfg.getType("test2.inner.listWithTrailingComment"), + flexi_cfg::config::types::Type::kList); + EXPECT_EQ(cfg.getValue>("test2.inner.listWithTrailingComment"), + std::vector({0, 2})); EXPECT_TRUE(cfg.exists("test2")); EXPECT_EQ(cfg.getType("test2"), flexi_cfg::config::types::Type::kStruct); EXPECT_TRUE(cfg.exists("test1")); @@ -111,6 +122,15 @@ struct test2 { struct inner { list = [1, 2, 3, 4] + emptyList = [] + listWithComment = [ +# I don't matter + 0, 2 + ] + listWithTrailingComment = [ + 0, + 2# I don't matter + ] } } @@ -139,16 +159,15 @@ auto baseDir() -> const std::filesystem::path& { auto filenameGenerator() -> std::vector { std::vector files; - auto genFilename = [](std::size_t idx) -> std::filesystem::path { - return baseDir() / ("config_example" + std::to_string(idx) + ".cfg"); - }; - for (size_t idx = 1;; ++idx) { - const auto cfg_file = genFilename(idx); - if (!std::filesystem::exists(cfg_file)) { - break; + for (const auto& entry : std::filesystem::directory_iterator(baseDir())) { + if (entry.is_regular_file()) { + if (const auto& file = entry.path().filename().string(); + file.starts_with("config_example") && file.ends_with(".cfg")) { + files.emplace_back(file); + } } - files.emplace_back(cfg_file.filename()); } + std::ranges::sort(files); return files; } } // namespace @@ -189,7 +208,7 @@ TEST_P(FileInput, ConfigReaderParseRootDir) { INSTANTIATE_TEST_SUITE_P(ConfigParse, FileInput, testing::ValuesIn(filenameGenerator())); TEST(ConfigParse, ConfigRoot) { - flexi_cfg::logger::setLevel(flexi_cfg::logger::Severity::DEBUG); + setLevel(flexi_cfg::logger::Severity::DEBUG); EXPECT_NO_THROW(flexi_cfg::Parser::parse( std::filesystem::path("config_root/test/config_example_base.cfg"), baseDir())); }