From 7dc07e77e874959293f28a0d3e7e3ee5bbcee0c8 Mon Sep 17 00:00:00 2001 From: Master92 Date: Tue, 29 Aug 2023 09:50:53 +0200 Subject: [PATCH 01/19] Add Entry class --- src/CMakeLists.txt | 4 +- src/Entry.cpp | 64 ++++++++++++++ src/Entry.h | 61 +++++++++++++ tests/CMakeLists.txt | 1 + tests/EntryTest.cpp | 206 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 src/Entry.cpp create mode 100644 src/Entry.h create mode 100644 tests/EntryTest.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 39d378b..09f7c5f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,6 +4,8 @@ set(CMAKE_DEBUG_POSTFIX d) set(SOURCES cppIni.cpp + + Entry.cpp ) set(API_HEADERS @@ -12,7 +14,7 @@ set(API_HEADERS LIST(TRANSFORM API_HEADERS PREPEND ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/) set(HEADERS - + Entry.h ) add_library(${PROJECT_NAME} ${SOURCES} ${API_HEADERS} ${HEADERS}) diff --git a/src/Entry.cpp b/src/Entry.cpp new file mode 100644 index 0000000..69c2722 --- /dev/null +++ b/src/Entry.cpp @@ -0,0 +1,64 @@ +#include "Entry.h" + +#include + +/// \details An Entry is a key-value pair in a configuration file +Entry::Entry(const Entry& other) + : m_key(other.m_key) + , m_size(other.m_size) +{ + m_data = std::malloc(m_size); + memcpy(m_data, other.m_data, m_size); +} + +Entry::~Entry() +{ + free(m_data); +} + +auto Entry::operator==(const Entry& other) const -> bool +{ + if (m_key != other.m_key) { + return false; + } + + if (m_data != other.m_data) { + return std::memcmp(m_data, other.m_data, m_size) == 0; + } + + return true; +} + +auto Entry::operator=(const Entry& other) -> Entry& +{ + if (this == &other) { + return *this; + } + + if (this->m_size != other.m_size) { + return *this; + } + + m_key = other.m_key; + memcpy(m_data, other.m_data, m_size); + + return *this; +} + +auto Entry::operator=(Entry&& other) noexcept -> Entry& +{ + if (this == &other) { + return *this; + } + + if (this->m_size != other.m_size) { + return *this; + } + + m_key = std::move(other.m_key); + free(m_data); + m_data = other.m_data; + other.m_data = nullptr; + + return *this; +} diff --git a/src/Entry.h b/src/Entry.h new file mode 100644 index 0000000..0429221 --- /dev/null +++ b/src/Entry.h @@ -0,0 +1,61 @@ +#pragma once + +#include + +/// \brief The Entry class +/// \details An entry is a key-value pair. The key is a string and the value can be any type. It has a parent Section +/// held by a (non-owning) pointer. +/// \note The value is stored in a void pointer and must be casted to the correct type. +/// \note The value is copied into the Entry and not moved. +/// \note The parent Section is a pointer to the Section object that contains this Entry. +class Entry { +public: + constexpr Entry() = default; ///< Default constructor + virtual ~Entry(); ///< Destructor + + Entry(const Entry& other); ///< Copy constructor + + template + constexpr Entry(std::string_view key, T value); ///< Constructor with key, value and parent Section pointer (default nullptr) + + auto key() const -> std::string_view { return m_key; } ///< Key as std::string_view + + template + constexpr auto value() const -> const T& { return *reinterpret_cast(m_data); } ///< Value as const reference + constexpr auto data() const -> const void* { return m_data; } ///< Pointer to the value + constexpr auto parent() const -> const Section* { return m_parent; } ///< Parent Section + constexpr auto size() const -> size_t { return m_size; } ///< Size in bytes + + auto setKey(std::string_view key) -> void { m_key = key; } ///< Set the key + template auto setData(T value) -> void; ///< Set the value + + auto operator==(const Entry& other) const -> bool; ///< Equality operator + auto operator!=(const Entry& other) const -> bool { return not (*this == other); } ///< Inequality operator + + auto operator=(const Entry& other) -> Entry&; ///< Copy assignment operator + auto operator=(Entry&& other) noexcept -> Entry&; ///< Move assignment operator + +private: + std::string m_key {}; + const size_t m_size {0}; + void* m_data {nullptr}; +}; + +template +constexpr Entry::Entry(std::string_view key, T value) + : m_key(key) + , m_size(sizeof(std::remove_all_extents_t)) +{ + m_data = std::malloc(m_size); + *reinterpret_cast*>(m_data) = std::move(value); +} + +template +auto Entry::setData(T value) -> void +{ + if (m_size != sizeof(std::remove_all_extents_t)) { + return; + } + + *reinterpret_cast(m_data) = std::move(value); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a067488..86ec7ba 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,6 +4,7 @@ find_package(doctest REQUIRED) set(TEST_SOURCES ConstructionTest.cpp + EntryTest.cpp ) add_executable(${PROJECT_NAME}_tests ${TEST_SOURCES}) diff --git a/tests/EntryTest.cpp b/tests/EntryTest.cpp new file mode 100644 index 0000000..1a49fa5 --- /dev/null +++ b/tests/EntryTest.cpp @@ -0,0 +1,206 @@ +#include + +#include "../src/Entry.h" + +TEST_CASE("Entry construction") +{ + Entry e; + + Entry meaningful("Blubb", 42); + CHECK_EQ(meaningful.key(), "Blubb"); + CHECK_EQ(meaningful.value(), 42); + + const int a = 69; + Entry second("Bla", a); +} + +struct DataStructure +{ + int a {42}; + float b {69.f}; + double c {1337.}; + + explicit constexpr DataStructure(int val) : a(val) {} + + constexpr auto operator <=>(const DataStructure& other) const = default; +}; + +TEST_CASE_TEMPLATE("Entry construction with base type", T, char, int, float, DataStructure) +{ + constexpr const char* key = "Entry"; + Entry e(key, T(42)); + + CHECK_EQ(e.key(), key); + CHECK_EQ(e.value(), T(42)); + + const auto value = T(0); + Entry e2(key, value); + CHECK_EQ(e2.key(), key); + CHECK_EQ(e2.value(), value); +} + +TEST_CASE("Entry equality") +{ + constexpr auto key = "Test"; + constexpr auto value = 4211; + + Entry e1{key, value}; + Entry e2{key, value}; + Entry e3{key, 1337}; + Entry e4{"Bla", value}; + + CHECK_EQ(e1, e1); + CHECK_EQ(e1, e2); + CHECK_NE(e1, e3); + CHECK_NE(e1, e4); +} + +TEST_CASE("Entry assignment") +{ + constexpr auto key = "Test"; + constexpr auto value = 4211; + + Entry e1{key, value}; + const Entry e2{key, value}; + const Entry e3{key, 1337}; + const Entry e4{"Bla", value}; + const Entry e5{key, DataStructure(42)}; + + e1 = e1; + CHECK_EQ(e1, e2); + + e1 = e2; + CHECK_EQ(e1, e2); + + e1 = e3; + CHECK_EQ(e1, e3); + + e1 = e4; + CHECK_EQ(e1, e4); + + e1 = e5; + CHECK_NE(e1, e5); + CHECK_EQ(e1, e4); +} + +TEST_CASE("Entry movement") +{ + constexpr auto key = "Test"; + constexpr auto value = 4211; + + Entry e1{key, value}; + const Entry e1Copy{e1}; + + Entry e2{key, value}; + const Entry e2Copy{e2}; + + Entry e3{key, 1337}; + const Entry e3Copy{e3}; + + Entry e4{"Bla", value}; + const Entry e4Copy{e4}; + + Entry e5{key, DataStructure(42)}; + const Entry e5Copy{e5}; + + e1 = std::move(e2); + CHECK_EQ(e1, e2Copy); + + e1 = std::move(e3); + CHECK_EQ(e1, e3Copy); + + e1 = std::move(e4); + CHECK_EQ(e1, e4Copy); + + e1 = std::move(e5); + CHECK_NE(e1, e5Copy); + CHECK_EQ(e1, e4Copy); + CHECK_EQ(e5, e5Copy); // I know we moved from e5, but it should still be valid +} + +TEST_CASE_TEMPLATE("Size of member types", T, char, int, double, DataStructure) +{ + constexpr auto key = "Test"; + constexpr auto value = 4211; + + Entry e{key, T(value)}; + + CHECK_EQ(e.size(), sizeof(T)); +} + +TEST_CASE("Changing the key of an Element") +{ + constexpr auto key = "Test"; + constexpr auto value = 4211; + constexpr auto key2 = "Test2"; + + REQUIRE_NE(key, key2); + + Entry e{key, value}; + + CHECK_EQ(e.key(), key); + + e.setKey(key2); + + CHECK_EQ(e.key(), key2); +} + +TEST_CASE_TEMPLATE("Changing the value of an Element", T, char, int, double, DataStructure) +{ + constexpr auto key = "Test"; + constexpr auto value = 4211; + constexpr auto valueObject = T(value); + constexpr auto valueObject2 = T(1337); + + REQUIRE_NE(valueObject, valueObject2); + + Entry e{key, valueObject}; + + CHECK_EQ(e.value(), valueObject); + + e.setData(valueObject2); + + CHECK_EQ(e.value(), valueObject2); +} + +#if 0 +#if __has_include("windows.h") + +#define WINDOWS_LEAN_AND_MEAN +#include +#include +#include +#include + +static SIZE_T currentMemoryUsage() +{ + PROCESS_MEMORY_COUNTERS_EX pmc; + GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc)); + return pmc.WorkingSetSize; +} + +TEST_CASE("Check for Entry memory leaks") +{ + auto memoryStart = currentMemoryUsage(); + SIZE_T intermediateUsage; + + std::cout << memoryStart << std::endl; + { + std::vector v; + + for (size_t i = 0; i < size_t(10'000'000); i++) { + v.emplace_back("asd", i); + } + + intermediateUsage = currentMemoryUsage(); + std::cout << intermediateUsage << std::endl; + CHECK_GT(intermediateUsage - memoryStart, 100'000'000); + } + + std::cout << currentMemoryUsage() << std::endl; + + CHECK_GT(intermediateUsage - currentMemoryUsage(), 10'000'000); +} + +#endif +#endif From 72061847e65f4a6380a2a90bec27fe1e65ae3c0a Mon Sep 17 00:00:00 2001 From: Master92 Date: Tue, 29 Aug 2023 09:41:17 +0200 Subject: [PATCH 02/19] Add Section class --- src/CMakeLists.txt | 2 ++ src/Section.cpp | 24 +++++++++++++++++ src/Section.h | 40 ++++++++++++++++++++++++++++ tests/CMakeLists.txt | 1 + tests/SectionTest.cpp | 62 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 129 insertions(+) create mode 100644 src/Section.cpp create mode 100644 src/Section.h create mode 100644 tests/SectionTest.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 09f7c5f..1f311bb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,6 +5,7 @@ set(CMAKE_DEBUG_POSTFIX d) set(SOURCES cppIni.cpp + Section.cpp Entry.cpp ) @@ -14,6 +15,7 @@ set(API_HEADERS LIST(TRANSFORM API_HEADERS PREPEND ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/) set(HEADERS + Section.h Entry.h ) diff --git a/src/Section.cpp b/src/Section.cpp new file mode 100644 index 0000000..9cf2f1f --- /dev/null +++ b/src/Section.cpp @@ -0,0 +1,24 @@ +#include "Section.h" + +Section::Section(std::string_view title, Section* parent) + : m_title(title) + , m_parent(parent) +{ + +} + +/// \note The entry is moved into the vector +/// \arg entry The Entry to add +auto Section::addEntry(Entry entry) -> void +{ + m_entries.push_back(std::move(entry)); +} + +auto Section::fqTitle() const -> std::string +{ + if (m_parent == nullptr) { + return m_title; + } + + return m_parent->fqTitle() + "." + m_title; +} diff --git a/src/Section.h b/src/Section.h new file mode 100644 index 0000000..6d66ea7 --- /dev/null +++ b/src/Section.h @@ -0,0 +1,40 @@ +#pragma once + +#include "Entry.h" + +#include + +/// \brief Represents a section in a configuration file +/// \details A section is a collection of Entry objects with a title (e.g. [Section]) in a configuration file +/// \note A section has a title and a list of Entry objects +class Section { +public: + explicit Section(std::string_view title, Section* parent = nullptr); ///< Constructor with title + + auto title() const -> std::string_view { return m_title; } ///< Title as std::string_view + auto fqTitle() const -> std::string; ///< Fully qualified title (e.g. "Section1.Section2") + + auto parent() const -> const Section* { return m_parent; } ///< Parent Section + auto isSubsection() const -> bool { return m_parent != nullptr; } ///< Returns true if this Section is a subsection + + auto addEntry(Entry entry) -> void; ///< Add an Entry object to the section + + template + auto createEntry(std::string_view key, T value) -> void; ///< Create an Entry object in place and add it to the section + + constexpr auto entries() const -> const std::vector& { return m_entries; } ///< List of Entry objects + +private: + std::string m_title; + std::vector m_entries; + Section *m_parent {nullptr}; +}; + +/// \details The parameters are forwarded to the Entry constructor and a pointer to this Section object is added as the parent +/// \arg key The key of the Entry +/// \arg value The value of the Entry +template +auto Section::createEntry(std::string_view key, T value) -> void +{ + m_entries.emplace_back(key, std::move(value), this); +} \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 86ec7ba..77ac485 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,6 +5,7 @@ find_package(doctest REQUIRED) set(TEST_SOURCES ConstructionTest.cpp EntryTest.cpp + SectionTest.cpp ) add_executable(${PROJECT_NAME}_tests ${TEST_SOURCES}) diff --git a/tests/SectionTest.cpp b/tests/SectionTest.cpp new file mode 100644 index 0000000..fcffb64 --- /dev/null +++ b/tests/SectionTest.cpp @@ -0,0 +1,62 @@ +#include + +#include "../src/Section.h" + +class SectionFixture +{ +public: + SectionFixture() : s(title) {} + +protected: + constexpr static auto title = "Test section"; + Section s; +}; + +TEST_CASE("Empty section") +{ + constexpr auto title = "Test section"; + + Section s{title}; + + CHECK_EQ(s.title(), title); + CHECK_EQ(s.entries().size(), 0); +} + +TEST_CASE("Subsection title construction") +{ + constexpr auto title = "Test section"; + constexpr auto subSectionTitle = "Subsection"; + + Section section{title}; + Section subSection{subSectionTitle, §ion}; + + CHECK_EQ(section.title(), title); + CHECK_EQ(subSection.parent(), §ion); + CHECK(subSection.isSubsection()); + CHECK_EQ(subSection.title(), subSectionTitle); + CHECK_EQ(subSection.fqTitle(), std::string(title) + "." + subSectionTitle); + CHECK_EQ(subSection.entries().size(), 0); +} + +TEST_CASE_FIXTURE(SectionFixture, "Add existing entries") +{ + Entry e{"key", 42}; + s.addEntry(e); + + CHECK_EQ(s.entries().size(), 1); + CHECK_EQ(s.entries().at(0), e); +} + +TEST_CASE_FIXTURE(SectionFixture, "Create entry during insertion") +{ + constexpr auto key = "Test Key"; + constexpr auto value = 42; + + s.createEntry(key, value); + + const Entry checkEntry{key, value}; + + CHECK_EQ(s.entries().size(), 1); + CHECK_EQ(s.entries().at(0), checkEntry); + CHECK_NE(s.entries().at(0).data(), checkEntry.data()); +} From 3d08d0c53572334b81c76bbf44006b9b6b87d075 Mon Sep 17 00:00:00 2001 From: Master92 Date: Tue, 29 Aug 2023 09:50:11 +0200 Subject: [PATCH 03/19] Add Section->Entry relationship --- src/Entry.cpp | 7 ++++++ src/Entry.h | 8 +++++-- tests/SectionTest.cpp | 55 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/Entry.cpp b/src/Entry.cpp index 69c2722..d05a5ad 100644 --- a/src/Entry.cpp +++ b/src/Entry.cpp @@ -1,4 +1,5 @@ #include "Entry.h" +#include "Section.h" #include @@ -22,6 +23,10 @@ auto Entry::operator==(const Entry& other) const -> bool return false; } + if (m_parent != other.m_parent) { + return false; + } + if (m_data != other.m_data) { return std::memcmp(m_data, other.m_data, m_size) == 0; } @@ -41,6 +46,7 @@ auto Entry::operator=(const Entry& other) -> Entry& m_key = other.m_key; memcpy(m_data, other.m_data, m_size); + m_parent = other.m_parent; return *this; } @@ -59,6 +65,7 @@ auto Entry::operator=(Entry&& other) noexcept -> Entry& free(m_data); m_data = other.m_data; other.m_data = nullptr; + m_parent = other.m_parent; return *this; } diff --git a/src/Entry.h b/src/Entry.h index 0429221..5a72bcc 100644 --- a/src/Entry.h +++ b/src/Entry.h @@ -2,6 +2,8 @@ #include +class Section; + /// \brief The Entry class /// \details An entry is a key-value pair. The key is a string and the value can be any type. It has a parent Section /// held by a (non-owning) pointer. @@ -16,7 +18,7 @@ class Entry { Entry(const Entry& other); ///< Copy constructor template - constexpr Entry(std::string_view key, T value); ///< Constructor with key, value and parent Section pointer (default nullptr) + constexpr Entry(std::string_view key, T value, Section* parent = nullptr); ///< Constructor with key, value and parent Section pointer (default nullptr) auto key() const -> std::string_view { return m_key; } ///< Key as std::string_view @@ -39,12 +41,14 @@ class Entry { std::string m_key {}; const size_t m_size {0}; void* m_data {nullptr}; + Section* m_parent {nullptr}; }; template -constexpr Entry::Entry(std::string_view key, T value) +constexpr Entry::Entry(std::string_view key, T value, Section* parent) : m_key(key) , m_size(sizeof(std::remove_all_extents_t)) + , m_parent(parent) { m_data = std::malloc(m_size); *reinterpret_cast*>(m_data) = std::move(value); diff --git a/tests/SectionTest.cpp b/tests/SectionTest.cpp index fcffb64..8311041 100644 --- a/tests/SectionTest.cpp +++ b/tests/SectionTest.cpp @@ -38,6 +38,59 @@ TEST_CASE("Subsection title construction") CHECK_EQ(subSection.entries().size(), 0); } +TEST_CASE("Entry with parent") +{ + constexpr auto title = "Test section"; + constexpr auto key = "Test key"; + constexpr auto value = 42; + + Section s{title}; + Entry e{key, value, &s}; + s.createEntry(key, value); + + CHECK_EQ(e.key(), key); + CHECK_EQ(e.value(), value); + CHECK_EQ(e.parent(), &s); + + CHECK_EQ(s.entries().size(), 1); + CHECK_EQ(s.entries().at(0).parent(), &s); +} + +TEST_CASE("Entry with subsection as parent") +{ + constexpr auto title = "Test section"; + constexpr auto subSectionTitle = "Subsection"; + constexpr auto key = "Test key"; + + Section section{title}; + Section subSection{subSectionTitle, §ion}; + + subSection.createEntry(key, 42); + CHECK_EQ(subSection.entries().at(0).parent(), &subSection); +} + +TEST_CASE("Entry assignment with different parents") +{ + constexpr auto title1 = "Test section 1"; + constexpr auto title2 = "Test section 2"; + constexpr auto key1 = "Test key"; + constexpr auto key2 = "Test key 2"; + constexpr auto value1 = 42; + constexpr auto value2 = 1337; + + Section section1{title1}; + Section section2{title2}; + + Entry e1{key1, value1, §ion1}; + Entry e2{key2, value2, §ion2}; + REQUIRE_EQ(e1.parent(), §ion1); + REQUIRE_EQ(e2.parent(), §ion2); + CHECK_NE(e1.parent(), e2.parent()); + + e1 = e2; + CHECK_EQ(e1.parent(), e2.parent()); +} + TEST_CASE_FIXTURE(SectionFixture, "Add existing entries") { Entry e{"key", 42}; @@ -54,7 +107,7 @@ TEST_CASE_FIXTURE(SectionFixture, "Create entry during insertion") s.createEntry(key, value); - const Entry checkEntry{key, value}; + const Entry checkEntry{key, value, &s}; CHECK_EQ(s.entries().size(), 1); CHECK_EQ(s.entries().at(0), checkEntry); From dfdd677abca60980b17aba70bdc5b21ed50c6b70 Mon Sep 17 00:00:00 2001 From: Master92 Date: Tue, 29 Aug 2023 09:51:53 +0200 Subject: [PATCH 04/19] Add construction of fully qualified key --- src/Entry.cpp | 9 +++++++++ src/Entry.h | 1 + tests/SectionTest.cpp | 1 + 3 files changed, 11 insertions(+) diff --git a/src/Entry.cpp b/src/Entry.cpp index d05a5ad..7aa84d0 100644 --- a/src/Entry.cpp +++ b/src/Entry.cpp @@ -69,3 +69,12 @@ auto Entry::operator=(Entry&& other) noexcept -> Entry& return *this; } + +auto Entry::fqKey() const -> std::string +{ + if (m_parent == nullptr) { + return m_key; + } + + return m_parent->fqTitle() + "." + m_key; +} diff --git a/src/Entry.h b/src/Entry.h index 5a72bcc..47fec0b 100644 --- a/src/Entry.h +++ b/src/Entry.h @@ -27,6 +27,7 @@ class Entry { constexpr auto data() const -> const void* { return m_data; } ///< Pointer to the value constexpr auto parent() const -> const Section* { return m_parent; } ///< Parent Section constexpr auto size() const -> size_t { return m_size; } ///< Size in bytes + auto fqKey() const -> std::string; ///< Fully qualified key (e.g. "Section1.Section2.Key") auto setKey(std::string_view key) -> void { m_key = key; } ///< Set the key template auto setData(T value) -> void; ///< Set the value diff --git a/tests/SectionTest.cpp b/tests/SectionTest.cpp index 8311041..0213d40 100644 --- a/tests/SectionTest.cpp +++ b/tests/SectionTest.cpp @@ -67,6 +67,7 @@ TEST_CASE("Entry with subsection as parent") subSection.createEntry(key, 42); CHECK_EQ(subSection.entries().at(0).parent(), &subSection); + CHECK_EQ(subSection.entries().at(0).fqKey(), std::string(title) + "." + subSectionTitle + "." + key); } TEST_CASE("Entry assignment with different parents") From 913f004f2ebd7f25145847b5a3d64a5fdc313385 Mon Sep 17 00:00:00 2001 From: Master92 Date: Tue, 29 Aug 2023 10:04:58 +0200 Subject: [PATCH 05/19] Fix MSVC needs inclusion of std headers for Doctest --- tests/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 77ac485..09315c1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -13,6 +13,7 @@ target_link_libraries(${PROJECT_NAME}_tests doctest::doctest cppIni) target_compile_definitions(${PROJECT_NAME}_tests PUBLIC DOCTEST_CONFIG_NO_MULTITHREADING + DOCTEST_CONFIG_USE_STD_HEADERS ) target_include_directories(${PROJECT_NAME} From 9e5fd25a24e923dd858890a67afce12e443f4b75 Mon Sep 17 00:00:00 2001 From: Master92 Date: Tue, 29 Aug 2023 13:41:23 +0200 Subject: [PATCH 06/19] Simplify Entry by storing strings --- src/Entry.cpp | 69 ----------------------------------- src/Entry.h | 69 ++++++++++++++++++++++++----------- tests/EntryTest.cpp | 87 +++++++++++++++++++++++++++++++++++---------- 3 files changed, 116 insertions(+), 109 deletions(-) diff --git a/src/Entry.cpp b/src/Entry.cpp index 7aa84d0..03a1363 100644 --- a/src/Entry.cpp +++ b/src/Entry.cpp @@ -1,75 +1,6 @@ #include "Entry.h" #include "Section.h" -#include - -/// \details An Entry is a key-value pair in a configuration file -Entry::Entry(const Entry& other) - : m_key(other.m_key) - , m_size(other.m_size) -{ - m_data = std::malloc(m_size); - memcpy(m_data, other.m_data, m_size); -} - -Entry::~Entry() -{ - free(m_data); -} - -auto Entry::operator==(const Entry& other) const -> bool -{ - if (m_key != other.m_key) { - return false; - } - - if (m_parent != other.m_parent) { - return false; - } - - if (m_data != other.m_data) { - return std::memcmp(m_data, other.m_data, m_size) == 0; - } - - return true; -} - -auto Entry::operator=(const Entry& other) -> Entry& -{ - if (this == &other) { - return *this; - } - - if (this->m_size != other.m_size) { - return *this; - } - - m_key = other.m_key; - memcpy(m_data, other.m_data, m_size); - m_parent = other.m_parent; - - return *this; -} - -auto Entry::operator=(Entry&& other) noexcept -> Entry& -{ - if (this == &other) { - return *this; - } - - if (this->m_size != other.m_size) { - return *this; - } - - m_key = std::move(other.m_key); - free(m_data); - m_data = other.m_data; - other.m_data = nullptr; - m_parent = other.m_parent; - - return *this; -} - auto Entry::fqKey() const -> std::string { if (m_parent == nullptr) { diff --git a/src/Entry.h b/src/Entry.h index 47fec0b..5821a20 100644 --- a/src/Entry.h +++ b/src/Entry.h @@ -7,60 +7,87 @@ class Section; /// \brief The Entry class /// \details An entry is a key-value pair. The key is a string and the value can be any type. It has a parent Section /// held by a (non-owning) pointer. -/// \note The value is stored in a void pointer and must be casted to the correct type. +/// \note The value is stored as a string. /// \note The value is copied into the Entry and not moved. /// \note The parent Section is a pointer to the Section object that contains this Entry. class Entry { public: constexpr Entry() = default; ///< Default constructor - virtual ~Entry(); ///< Destructor + virtual ~Entry() = default; ///< Destructor - Entry(const Entry& other); ///< Copy constructor + Entry(const Entry& other) = default; ///< Copy constructor template constexpr Entry(std::string_view key, T value, Section* parent = nullptr); ///< Constructor with key, value and parent Section pointer (default nullptr) auto key() const -> std::string_view { return m_key; } ///< Key as std::string_view - template - constexpr auto value() const -> const T& { return *reinterpret_cast(m_data); } ///< Value as const reference - constexpr auto data() const -> const void* { return m_data; } ///< Pointer to the value + template auto value() const -> T; ///< Value as type T + auto data() const -> std::string_view { return m_data; } ///< Value as std::string_view constexpr auto parent() const -> const Section* { return m_parent; } ///< Parent Section - constexpr auto size() const -> size_t { return m_size; } ///< Size in bytes auto fqKey() const -> std::string; ///< Fully qualified key (e.g. "Section1.Section2.Key") auto setKey(std::string_view key) -> void { m_key = key; } ///< Set the key template auto setData(T value) -> void; ///< Set the value - auto operator==(const Entry& other) const -> bool; ///< Equality operator - auto operator!=(const Entry& other) const -> bool { return not (*this == other); } ///< Inequality operator - - auto operator=(const Entry& other) -> Entry&; ///< Copy assignment operator - auto operator=(Entry&& other) noexcept -> Entry&; ///< Move assignment operator + constexpr auto operator<=>(const Entry& other) const = default; ///< Spaceship operator + auto operator=(const Entry& other) -> Entry& = default; ///< Copy assignment operator + auto operator=(Entry&& other) -> Entry& = default; ///< Move assignment operator private: std::string m_key {}; - const size_t m_size {0}; - void* m_data {nullptr}; + std::string m_data {}; Section* m_parent {nullptr}; }; template constexpr Entry::Entry(std::string_view key, T value, Section* parent) : m_key(key) - , m_size(sizeof(std::remove_all_extents_t)) , m_parent(parent) { - m_data = std::malloc(m_size); - *reinterpret_cast*>(m_data) = std::move(value); + m_data = std::to_string(value); +} + +template<> +inline Entry::Entry(std::string_view key, std::string value, Section* parent) + : m_key(key) + , m_data(std::move(value)) + , m_parent(parent) +{ +} + +template<> +inline Entry::Entry(std::string_view key, const char* value, Section* parent) + : m_key(key) + , m_data(std::string(value)) + , m_parent(parent) +{ } template auto Entry::setData(T value) -> void { - if (m_size != sizeof(std::remove_all_extents_t)) { - return; - } + m_data = std::to_string(value); +} - *reinterpret_cast(m_data) = std::move(value); +template<> +inline auto Entry::setData(std::string value) -> void +{ + m_data = std::move(value); } + +template<> inline auto Entry::value() const -> char { return std::stoi(m_data); } +template<> inline auto Entry::value() const -> short { return std::stoi(m_data); } +template<> inline auto Entry::value() const -> int { return std::stoi(m_data); } +template<> inline auto Entry::value() const -> long { return std::stol(m_data); } +template<> inline auto Entry::value() const -> long long { return std::stoll(m_data); } +template<> inline auto Entry::value() const -> unsigned char { return std::stoull(m_data); } +template<> inline auto Entry::value() const -> unsigned short { return std::stoull(m_data); } +template<> inline auto Entry::value() const -> unsigned int { return std::stoull(m_data); } +template<> inline auto Entry::value() const -> unsigned long { return std::stoull(m_data); } +template<> inline auto Entry::value() const -> unsigned long long { return std::stoull(m_data); } +template<> inline auto Entry::value() const -> float { return std::stof(m_data); } +template<> inline auto Entry::value() const -> double { return std::stod(m_data); } +template<> inline auto Entry::value() const -> long double { return std::stold(m_data); } +template<> inline auto Entry::value() const -> std::string { return m_data; } +template<> inline auto Entry::value() const -> const char* { return data().data(); } diff --git a/tests/EntryTest.cpp b/tests/EntryTest.cpp index 1a49fa5..7c773c7 100644 --- a/tests/EntryTest.cpp +++ b/tests/EntryTest.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include "../src/Entry.h" @@ -23,9 +25,29 @@ struct DataStructure explicit constexpr DataStructure(int val) : a(val) {} constexpr auto operator <=>(const DataStructure& other) const = default; + + [[nodiscard]] auto toString() const -> std::string { return std::to_string(a) + " " + std::to_string(b) + " " + std::to_string(c); } + + friend auto Entry::value() const -> DataStructure; +}; + +template<> +auto Entry::value() const -> DataStructure +{ + std::vector splitValues; + std::istringstream stream{m_data}; + for (std::string line; std::getline(stream, line, ' ');) { + splitValues.push_back(line); + } + + DataStructure result(std::stoi(splitValues[0])); + result.b = std::stof(splitValues[1]); + result.c = std::stod(splitValues[2]); + + return result; }; -TEST_CASE_TEMPLATE("Entry construction with base type", T, char, int, float, DataStructure) +TEST_CASE_TEMPLATE("Entry construction with base type", T, char, int, float) { constexpr const char* key = "Entry"; Entry e(key, T(42)); @@ -39,6 +61,28 @@ TEST_CASE_TEMPLATE("Entry construction with base type", T, char, int, float, Dat CHECK_EQ(e2.value(), value); } +TEST_CASE_TEMPLATE("Entry with string as data", T, const char*, std::string) +{ + constexpr auto key = "Test"; + const T value = "TestValue"; + + Entry e(key, value); + + CHECK_EQ(e.key(), key); + CHECK_EQ(e.value(), value); +} + +TEST_CASE("Entry with DataStructure") +{ + constexpr auto key = "Test"; + constexpr auto value = 4211; + + Entry e(key, DataStructure(value).toString()); + + CHECK_EQ(e.key(), key); + CHECK_EQ(e.value().a, value); +} + TEST_CASE("Entry equality") { constexpr auto key = "Test"; @@ -64,7 +108,7 @@ TEST_CASE("Entry assignment") const Entry e2{key, value}; const Entry e3{key, 1337}; const Entry e4{"Bla", value}; - const Entry e5{key, DataStructure(42)}; + const Entry e5{key, DataStructure(42).toString()}; e1 = e1; CHECK_EQ(e1, e2); @@ -79,8 +123,7 @@ TEST_CASE("Entry assignment") CHECK_EQ(e1, e4); e1 = e5; - CHECK_NE(e1, e5); - CHECK_EQ(e1, e4); + CHECK_EQ(e1, e5); } TEST_CASE("Entry movement") @@ -100,7 +143,7 @@ TEST_CASE("Entry movement") Entry e4{"Bla", value}; const Entry e4Copy{e4}; - Entry e5{key, DataStructure(42)}; + Entry e5{key, DataStructure(42).toString()}; const Entry e5Copy{e5}; e1 = std::move(e2); @@ -113,19 +156,7 @@ TEST_CASE("Entry movement") CHECK_EQ(e1, e4Copy); e1 = std::move(e5); - CHECK_NE(e1, e5Copy); - CHECK_EQ(e1, e4Copy); - CHECK_EQ(e5, e5Copy); // I know we moved from e5, but it should still be valid -} - -TEST_CASE_TEMPLATE("Size of member types", T, char, int, double, DataStructure) -{ - constexpr auto key = "Test"; - constexpr auto value = 4211; - - Entry e{key, T(value)}; - - CHECK_EQ(e.size(), sizeof(T)); + CHECK_EQ(e1, e5Copy); } TEST_CASE("Changing the key of an Element") @@ -145,7 +176,7 @@ TEST_CASE("Changing the key of an Element") CHECK_EQ(e.key(), key2); } -TEST_CASE_TEMPLATE("Changing the value of an Element", T, char, int, double, DataStructure) +TEST_CASE_TEMPLATE("Changing the value of an Element", T, char, int, double) { constexpr auto key = "Test"; constexpr auto value = 4211; @@ -163,6 +194,24 @@ TEST_CASE_TEMPLATE("Changing the value of an Element", T, char, int, double, Dat CHECK_EQ(e.value(), valueObject2); } +TEST_CASE("Changing the value of an Element with a std::string") +{ + constexpr auto key = "Test"; + constexpr auto value = 4211; + const auto valueObject = std::to_string(value); + const auto valueObject2 = std::to_string(1337); + + REQUIRE_NE(valueObject, valueObject2); + + Entry e{key, valueObject}; + + CHECK_EQ(e.value(), valueObject); + + e.setData(valueObject2); + + CHECK_EQ(e.value(), valueObject2); +} + #if 0 #if __has_include("windows.h") From 996a438946bc45d17c1cdd8d4f3460c3a4a5eaa2 Mon Sep 17 00:00:00 2001 From: Master92 Date: Tue, 29 Aug 2023 13:43:50 +0200 Subject: [PATCH 07/19] Find Entries from within a section by their names --- src/Section.cpp | 11 +++++++++++ src/Section.h | 1 + tests/SectionTest.cpp | 28 +++++++++++++++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/Section.cpp b/src/Section.cpp index 9cf2f1f..6f2cb39 100644 --- a/src/Section.cpp +++ b/src/Section.cpp @@ -22,3 +22,14 @@ auto Section::fqTitle() const -> std::string return m_parent->fqTitle() + "." + m_title; } + +auto Section::findEntry(std::string_view name) const -> const Entry* +{ + for (const auto& entry : m_entries) { + if (entry.key() == name) { + return &entry; + } + } + + return nullptr; +} diff --git a/src/Section.h b/src/Section.h index 6d66ea7..fc08c15 100644 --- a/src/Section.h +++ b/src/Section.h @@ -24,6 +24,7 @@ class Section { constexpr auto entries() const -> const std::vector& { return m_entries; } ///< List of Entry objects + auto findEntry(std::string_view name) const -> const Entry*; ///< Find an Entry object by name private: std::string m_title; std::vector m_entries; diff --git a/tests/SectionTest.cpp b/tests/SectionTest.cpp index 0213d40..b4b697f 100644 --- a/tests/SectionTest.cpp +++ b/tests/SectionTest.cpp @@ -112,5 +112,31 @@ TEST_CASE_FIXTURE(SectionFixture, "Create entry during insertion") CHECK_EQ(s.entries().size(), 1); CHECK_EQ(s.entries().at(0), checkEntry); - CHECK_NE(s.entries().at(0).data(), checkEntry.data()); +} + +TEST_CASE_FIXTURE(SectionFixture, "Find an Entry by name") +{ + constexpr auto key = "Test Key"; + constexpr auto value = 42; + + s.createEntry(key, value); + + const auto e = s.findEntry(key); + + REQUIRE_NE(e, nullptr); + CHECK_EQ(e, &s.entries().at(0)); + CHECK_EQ(e->key(), key); + CHECK_EQ(e->value(), value); +} + +TEST_CASE_FIXTURE(SectionFixture, "Try to find an Entry that doesn't exist") +{ + constexpr auto key = "Test Key"; + constexpr auto value = 42; + + s.createEntry(key, value); + + const auto e = s.findEntry("Nonexistent key"); + + CHECK_EQ(e, nullptr); } From 4e9ab05169b488e2ee5cfc0fecdb52f717267183 Mon Sep 17 00:00:00 2001 From: Master92 Date: Tue, 29 Aug 2023 14:12:05 +0200 Subject: [PATCH 08/19] Use unordered_map instead of vector for entries --- src/Section.cpp | 10 +++++----- src/Section.h | 10 +++++----- tests/SectionTest.cpp | 17 ++++++++++------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/Section.cpp b/src/Section.cpp index 6f2cb39..b55129e 100644 --- a/src/Section.cpp +++ b/src/Section.cpp @@ -11,7 +11,7 @@ Section::Section(std::string_view title, Section* parent) /// \arg entry The Entry to add auto Section::addEntry(Entry entry) -> void { - m_entries.push_back(std::move(entry)); + m_entries.insert(std::make_pair(entry.key(), std::move(entry))); } auto Section::fqTitle() const -> std::string @@ -25,10 +25,10 @@ auto Section::fqTitle() const -> std::string auto Section::findEntry(std::string_view name) const -> const Entry* { - for (const auto& entry : m_entries) { - if (entry.key() == name) { - return &entry; - } + const auto entry = m_entries.find(std::string(name)); + + if (entry != m_entries.cend()) { + return &entry->second; } return nullptr; diff --git a/src/Section.h b/src/Section.h index fc08c15..48d0069 100644 --- a/src/Section.h +++ b/src/Section.h @@ -2,7 +2,7 @@ #include "Entry.h" -#include +#include /// \brief Represents a section in a configuration file /// \details A section is a collection of Entry objects with a title (e.g. [Section]) in a configuration file @@ -22,12 +22,12 @@ class Section { template auto createEntry(std::string_view key, T value) -> void; ///< Create an Entry object in place and add it to the section - constexpr auto entries() const -> const std::vector& { return m_entries; } ///< List of Entry objects + constexpr auto entries() const -> const auto& { return m_entries; } ///< List of Entry objects auto findEntry(std::string_view name) const -> const Entry*; ///< Find an Entry object by name private: std::string m_title; - std::vector m_entries; + std::unordered_map m_entries; Section *m_parent {nullptr}; }; @@ -37,5 +37,5 @@ class Section { template auto Section::createEntry(std::string_view key, T value) -> void { - m_entries.emplace_back(key, std::move(value), this); -} \ No newline at end of file + m_entries.emplace(key, Entry{key, value, this}); +} diff --git a/tests/SectionTest.cpp b/tests/SectionTest.cpp index b4b697f..3be569d 100644 --- a/tests/SectionTest.cpp +++ b/tests/SectionTest.cpp @@ -53,7 +53,7 @@ TEST_CASE("Entry with parent") CHECK_EQ(e.parent(), &s); CHECK_EQ(s.entries().size(), 1); - CHECK_EQ(s.entries().at(0).parent(), &s); + CHECK_EQ(s.entries().at(key).parent(), &s); } TEST_CASE("Entry with subsection as parent") @@ -66,8 +66,8 @@ TEST_CASE("Entry with subsection as parent") Section subSection{subSectionTitle, §ion}; subSection.createEntry(key, 42); - CHECK_EQ(subSection.entries().at(0).parent(), &subSection); - CHECK_EQ(subSection.entries().at(0).fqKey(), std::string(title) + "." + subSectionTitle + "." + key); + CHECK_EQ(subSection.entries().at(key).parent(), &subSection); + CHECK_EQ(subSection.entries().at(key).fqKey(), std::string(title) + "." + subSectionTitle + "." + key); } TEST_CASE("Entry assignment with different parents") @@ -94,11 +94,14 @@ TEST_CASE("Entry assignment with different parents") TEST_CASE_FIXTURE(SectionFixture, "Add existing entries") { - Entry e{"key", 42}; + constexpr auto key = "Test Key"; + constexpr auto value = 42; + + Entry e{key, value}; s.addEntry(e); CHECK_EQ(s.entries().size(), 1); - CHECK_EQ(s.entries().at(0), e); + CHECK_EQ(s.entries().at(key), e); } TEST_CASE_FIXTURE(SectionFixture, "Create entry during insertion") @@ -111,7 +114,7 @@ TEST_CASE_FIXTURE(SectionFixture, "Create entry during insertion") const Entry checkEntry{key, value, &s}; CHECK_EQ(s.entries().size(), 1); - CHECK_EQ(s.entries().at(0), checkEntry); + CHECK_EQ(s.entries().at(key), checkEntry); } TEST_CASE_FIXTURE(SectionFixture, "Find an Entry by name") @@ -124,7 +127,7 @@ TEST_CASE_FIXTURE(SectionFixture, "Find an Entry by name") const auto e = s.findEntry(key); REQUIRE_NE(e, nullptr); - CHECK_EQ(e, &s.entries().at(0)); + CHECK_EQ(e, &s.entries().at(key)); CHECK_EQ(e->key(), key); CHECK_EQ(e->value(), value); } From 3fa9935fd60c4bda6c980472601fc73c2addefc6 Mon Sep 17 00:00:00 2001 From: Master92 Date: Tue, 29 Aug 2023 14:12:26 +0200 Subject: [PATCH 09/19] Return Section's parents constexpr --- src/Section.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Section.h b/src/Section.h index 48d0069..5f7c2f8 100644 --- a/src/Section.h +++ b/src/Section.h @@ -14,7 +14,7 @@ class Section { auto title() const -> std::string_view { return m_title; } ///< Title as std::string_view auto fqTitle() const -> std::string; ///< Fully qualified title (e.g. "Section1.Section2") - auto parent() const -> const Section* { return m_parent; } ///< Parent Section + constexpr auto parent() const -> const Section* { return m_parent; } ///< Parent Section auto isSubsection() const -> bool { return m_parent != nullptr; } ///< Returns true if this Section is a subsection auto addEntry(Entry entry) -> void; ///< Add an Entry object to the section From acbe3f3ea3a0637b6489e6b5256fe5e0b9e29435 Mon Sep 17 00:00:00 2001 From: Master92 Date: Tue, 29 Aug 2023 14:12:42 +0200 Subject: [PATCH 10/19] Add Entry ctor for std::string_view --- src/Entry.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Entry.h b/src/Entry.h index 5821a20..6bdf9dd 100644 --- a/src/Entry.h +++ b/src/Entry.h @@ -56,6 +56,14 @@ inline Entry::Entry(std::string_view key, std::string value, Section* parent) { } +template<> +inline Entry::Entry(std::string_view key, std::string_view value, Section* parent) + : m_key(key) + , m_data(std::string(value)) + , m_parent(parent) +{ +} + template<> inline Entry::Entry(std::string_view key, const char* value, Section* parent) : m_key(key) From d1def7a651c34104b1c28d2ebb7445973f817130 Mon Sep 17 00:00:00 2001 From: Master92 Date: Tue, 29 Aug 2023 14:30:19 +0200 Subject: [PATCH 11/19] Add equality operators for Sections --- src/Section.cpp | 14 ++++++++++++++ src/Section.h | 3 +++ tests/SectionTest.cpp | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/src/Section.cpp b/src/Section.cpp index b55129e..b4a1597 100644 --- a/src/Section.cpp +++ b/src/Section.cpp @@ -1,5 +1,7 @@ #include "Section.h" +#include + Section::Section(std::string_view title, Section* parent) : m_title(title) , m_parent(parent) @@ -33,3 +35,15 @@ auto Section::findEntry(std::string_view name) const -> const Entry* return nullptr; } + +auto Section::operator==(const Section& other) const -> bool +{ + if (m_title != other.m_title) { + return false; + } + + return std::ranges::all_of(m_entries, [&other](const auto& entry) { + const auto otherEntry = other.findEntry(entry.first); + return otherEntry and entry.second.data() == otherEntry->data(); + }); +} diff --git a/src/Section.h b/src/Section.h index 5f7c2f8..809737e 100644 --- a/src/Section.h +++ b/src/Section.h @@ -25,6 +25,9 @@ class Section { constexpr auto entries() const -> const auto& { return m_entries; } ///< List of Entry objects auto findEntry(std::string_view name) const -> const Entry*; ///< Find an Entry object by name + + auto operator==(const Section& other) const -> bool; ///< Equality operator + auto operator!=(const Section& other) const -> bool = default; ///< Inequality operator private: std::string m_title; std::unordered_map m_entries; diff --git a/tests/SectionTest.cpp b/tests/SectionTest.cpp index 3be569d..38a8fa0 100644 --- a/tests/SectionTest.cpp +++ b/tests/SectionTest.cpp @@ -143,3 +143,36 @@ TEST_CASE_FIXTURE(SectionFixture, "Try to find an Entry that doesn't exist") CHECK_EQ(e, nullptr); } + +TEST_CASE("Equality of two sections") +{ + constexpr auto title = "Test section"; + constexpr auto key = "Test key"; + constexpr auto value = 42; + + Section s1{title}; + Section s2{title}; + + s1.createEntry(key, value); + s2.createEntry(key, value); + + CHECK_EQ(s1, s2); +} + +TEST_CASE("Inequality of two sections") +{ + constexpr auto title = "Test section"; + constexpr auto title2 = "Test section 2"; + constexpr auto key = "Test key"; + constexpr auto value = 42; + + Section s1{title}; + Section s2{title}; + Section s3{title2}; + + s1.createEntry(key, value); + s3.createEntry(key, value); + + CHECK_NE(s1, s2); + CHECK_NE(s1, s3); +} From 9ef6c66f5ef94ae4fd2a148f0215c9f882eb62bd Mon Sep 17 00:00:00 2001 From: Master92 Date: Tue, 29 Aug 2023 14:37:39 +0200 Subject: [PATCH 12/19] Add documentation --- src/Section.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Section.cpp b/src/Section.cpp index b4a1597..e5ce6f7 100644 --- a/src/Section.cpp +++ b/src/Section.cpp @@ -16,6 +16,9 @@ auto Section::addEntry(Entry entry) -> void m_entries.insert(std::make_pair(entry.key(), std::move(entry))); } +/// If the Section is a top-level Section, the title is returned. +/// Otherwise, the title is prefixed with the parent's fully qualified title and a dot. +/// \returns The fully qualified title of the Section. auto Section::fqTitle() const -> std::string { if (m_parent == nullptr) { @@ -25,6 +28,8 @@ auto Section::fqTitle() const -> std::string return m_parent->fqTitle() + "." + m_title; } +/// \param name The name of the Entry to find. +/// \returns A pointer to the Entry if found, nullptr otherwise. auto Section::findEntry(std::string_view name) const -> const Entry* { const auto entry = m_entries.find(std::string(name)); @@ -36,6 +41,9 @@ auto Section::findEntry(std::string_view name) const -> const Entry* return nullptr; } +/// \details Two Sections are equal if they have the same title and the same entries. Their parents are not compared. +/// \param other The Section to compare to. +/// \returns true if the Sections are equal, false otherwise. auto Section::operator==(const Section& other) const -> bool { if (m_title != other.m_title) { From f9e9debb1943e775e3c6b9e86c638e2b90ce4ebd Mon Sep 17 00:00:00 2001 From: Master92 Date: Tue, 29 Aug 2023 14:41:54 +0200 Subject: [PATCH 13/19] Add basic File parsing --- src/CMakeLists.txt | 6 ++-- src/File.cpp | 84 ++++++++++++++++++++++++++++++++++++++++++++ src/File.h | 32 +++++++++++++++++ tests/CMakeLists.txt | 2 ++ tests/FileTest.cpp | 36 +++++++++++++++++++ tests/res/test.ini | 3 ++ 6 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 src/File.cpp create mode 100644 src/File.h create mode 100644 tests/FileTest.cpp create mode 100644 tests/res/test.ini diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1f311bb..4a3429e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,8 +5,9 @@ set(CMAKE_DEBUG_POSTFIX d) set(SOURCES cppIni.cpp - Section.cpp Entry.cpp + File.cpp + Section.cpp ) set(API_HEADERS @@ -15,8 +16,9 @@ set(API_HEADERS LIST(TRANSFORM API_HEADERS PREPEND ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/) set(HEADERS - Section.h Entry.h + File.h + Section.h ) add_library(${PROJECT_NAME} ${SOURCES} ${API_HEADERS} ${HEADERS}) diff --git a/src/File.cpp b/src/File.cpp new file mode 100644 index 0000000..679d526 --- /dev/null +++ b/src/File.cpp @@ -0,0 +1,84 @@ +#include "File.h" + +#include +#include + +/// \param filename The filename of the file to open. +File::File(std::string_view filename) +: m_filename{filename} +{ + open(); +} + +/// \param filename The filename of the file to open. +File File::open(std::string_view filename) +{ + return File{filename}; +} + +/// \throws std::runtime_error if the file cannot be opened. +void File::open() +{ + if (m_filename.empty()) { + throw std::runtime_error{"Filename is empty"}; + } + + parse(); +} + +/// \param title The title of the Section to find. +/// \returns A pointer to the Section if found, nullptr otherwise. +auto File::findSection(std::string_view title) const -> const Section* +{ + for (const auto& section : m_sections) { + if (section.fqTitle() == title) { + return §ion; + } + } + + return nullptr; +} + +/// \param name The name of the Entry to find. +/// \returns A pointer to the Entry if found, nullptr otherwise. +/// \see Section::findEntry +auto File::findEntry(std::string_view name) const -> const Entry* +{ + const Section* section = nullptr; + + if (name.find('.')) { + section = findSection(name.substr(0, name.find_last_of('.'))); + } + + if (section) { + return section->findEntry(name.substr(name.find_last_of('.') + 1)); + } + + return nullptr; +} + +/// \details This function is called by the constructor. It should not be called directly. +/// \throws std::runtime_error if the file cannot be opened. +/// \see File::open for the public function. +auto File::parse() -> void +{ + auto content = std::ifstream{m_filename}; + + for (std::string line; std::getline(content, line);) { + const std::string_view lineView{line}; + + if (lineView.empty()) { + continue; + } + + if (line[0] == '[') { + if (line.find('.') != std::string::npos) { + m_sections.emplace_back(lineView.substr(lineView.find_last_of('.') + 1, lineView.find_last_of(']') - 1), &m_sections.back()); + } else { + m_sections.emplace_back(lineView.substr(1, lineView.find_last_of(']') - 1)); + } + } else { + m_sections.back().createEntry(lineView.substr(0, lineView.find('=')), lineView.substr(lineView.find('=') + 1)); + } + } +} diff --git a/src/File.h b/src/File.h new file mode 100644 index 0000000..b200373 --- /dev/null +++ b/src/File.h @@ -0,0 +1,32 @@ +#pragma once + +#include "Section.h" + +#include + +/// \brief Represents a file on disk. +/// A file is a collection of Sections. +class File { +public: + explicit File(std::string_view filename); ///< Constructor. + virtual ~File() = default; ///< Destructor. + + static File open(std::string_view filename); ///< Open a file. Throws if the file cannot be opened. + void open(); ///< Open the file. Throws if the file cannot be opened. + + auto findSection(std::string_view title) const -> const Section*; ///< Find a Section by title. + auto findEntry(std::string_view name) const -> const Entry*; ///< Find an Entry by name. + + constexpr auto sections() const -> const auto& { return m_sections; } + + auto operator==(const File& other) const -> bool = default; ///< Equality operator. + auto operator!=(const File& other) const -> bool = default; ///< Inequality operator. + +private: + void parse(); ///< Parse the file. + +private: + std::string m_filename{}; + + std::vector
m_sections{}; +}; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 09315c1..b400dc5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,6 +5,7 @@ find_package(doctest REQUIRED) set(TEST_SOURCES ConstructionTest.cpp EntryTest.cpp + FileTest.cpp SectionTest.cpp ) @@ -14,6 +15,7 @@ target_compile_definitions(${PROJECT_NAME}_tests PUBLIC DOCTEST_CONFIG_NO_MULTITHREADING DOCTEST_CONFIG_USE_STD_HEADERS + WORKING_DIR="${CMAKE_CURRENT_SOURCE_DIR}" ) target_include_directories(${PROJECT_NAME} diff --git a/tests/FileTest.cpp b/tests/FileTest.cpp new file mode 100644 index 0000000..0fb00a1 --- /dev/null +++ b/tests/FileTest.cpp @@ -0,0 +1,36 @@ +#include +#include + +#include "../src/File.h" + +static const std::string fileName = std::string(WORKING_DIR) + "/res/test.ini"; + +TEST_CASE("Failing construction of an empty File object") +{ + CHECK_THROWS(File{""}); +} + +TEST_CASE("Construction of File object") +{ + CHECK_NOTHROW(File{fileName}); +} + +TEST_CASE("Successful parse of test.ini") +{ + const auto f = File{fileName}; + + REQUIRE_GT(f.sections().size(), 0); + CHECK(f.findSection("Section1")); + CHECK(f.findEntry("Section1.Entry1")); + CHECK_EQ(strcmp(f.findEntry("Section1.Entry1")->value(), "Value1"), 0); + CHECK_EQ(f.findEntry("Section1.IntEntry")->value(), 42); +} + +TEST_CASE("Open file from static method") +{ + REQUIRE_NOTHROW(const auto f = File::open(fileName)); + + const auto f = File::open(fileName); + const auto f2 = File{fileName}; + CHECK_EQ(f, f2); +} diff --git a/tests/res/test.ini b/tests/res/test.ini new file mode 100644 index 0000000..4bcc901 --- /dev/null +++ b/tests/res/test.ini @@ -0,0 +1,3 @@ +[Section1] +Entry1=Value1 +IntEntry=42 From 0408662fbd0be760aa9648a0981b376f2a3bde6b Mon Sep 17 00:00:00 2001 From: Master92 Date: Tue, 29 Aug 2023 14:49:53 +0200 Subject: [PATCH 14/19] Add value method for string_literal --- src/Entry.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Entry.h b/src/Entry.h index 6bdf9dd..6acdafe 100644 --- a/src/Entry.h +++ b/src/Entry.h @@ -98,4 +98,5 @@ template<> inline auto Entry::value() const -> float template<> inline auto Entry::value() const -> double { return std::stod(m_data); } template<> inline auto Entry::value() const -> long double { return std::stold(m_data); } template<> inline auto Entry::value() const -> std::string { return m_data; } +template<> inline auto Entry::value() const -> std::string_view { return m_data; } template<> inline auto Entry::value() const -> const char* { return data().data(); } From aeb385654993a3467a9c6a2cb72e973db3589e04 Mon Sep 17 00:00:00 2001 From: Master92 Date: Tue, 29 Aug 2023 14:49:40 +0200 Subject: [PATCH 15/19] Use string literals in FileTest --- tests/FileTest.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/FileTest.cpp b/tests/FileTest.cpp index 0fb00a1..606a4b5 100644 --- a/tests/FileTest.cpp +++ b/tests/FileTest.cpp @@ -3,7 +3,9 @@ #include "../src/File.h" -static const std::string fileName = std::string(WORKING_DIR) + "/res/test.ini"; +using namespace std::literals; + +static const std::string fileName = WORKING_DIR + "/res/test.ini"s; TEST_CASE("Failing construction of an empty File object") { @@ -22,7 +24,7 @@ TEST_CASE("Successful parse of test.ini") REQUIRE_GT(f.sections().size(), 0); CHECK(f.findSection("Section1")); CHECK(f.findEntry("Section1.Entry1")); - CHECK_EQ(strcmp(f.findEntry("Section1.Entry1")->value(), "Value1"), 0); + CHECK_EQ(f.findEntry("Section1.Entry1")->value(), "Value1"sv); CHECK_EQ(f.findEntry("Section1.IntEntry")->value(), 42); } From 0a4ffe8aa72dd2933748999ea0f516da537a666e Mon Sep 17 00:00:00 2001 From: Master92 Date: Tue, 29 Aug 2023 14:50:17 +0200 Subject: [PATCH 16/19] Group unit tests in test suites --- tests/EntryTest.cpp | 4 ++++ tests/FileTest.cpp | 4 ++++ tests/SectionTest.cpp | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/tests/EntryTest.cpp b/tests/EntryTest.cpp index 7c773c7..fec2a98 100644 --- a/tests/EntryTest.cpp +++ b/tests/EntryTest.cpp @@ -4,6 +4,8 @@ #include "../src/Entry.h" +TEST_SUITE_BEGIN("Entry"); + TEST_CASE("Entry construction") { Entry e; @@ -253,3 +255,5 @@ TEST_CASE("Check for Entry memory leaks") #endif #endif + +TEST_SUITE_END(); diff --git a/tests/FileTest.cpp b/tests/FileTest.cpp index 606a4b5..08cfdca 100644 --- a/tests/FileTest.cpp +++ b/tests/FileTest.cpp @@ -7,6 +7,8 @@ using namespace std::literals; static const std::string fileName = WORKING_DIR + "/res/test.ini"s; +TEST_SUITE_BEGIN("File"); + TEST_CASE("Failing construction of an empty File object") { CHECK_THROWS(File{""}); @@ -36,3 +38,5 @@ TEST_CASE("Open file from static method") const auto f2 = File{fileName}; CHECK_EQ(f, f2); } + +TEST_SUITE_END(); diff --git a/tests/SectionTest.cpp b/tests/SectionTest.cpp index 38a8fa0..0521d86 100644 --- a/tests/SectionTest.cpp +++ b/tests/SectionTest.cpp @@ -2,6 +2,8 @@ #include "../src/Section.h" +TEST_SUITE_BEGIN("Section"); + class SectionFixture { public: @@ -176,3 +178,5 @@ TEST_CASE("Inequality of two sections") CHECK_NE(s1, s2); CHECK_NE(s1, s3); } + +TEST_SUITE_END(); From f37bdef68f65f9b4d1e4ec388612ed0709ba1d02 Mon Sep 17 00:00:00 2001 From: Master92 Date: Tue, 29 Aug 2023 15:52:13 +0200 Subject: [PATCH 17/19] Replace spaceship with equality operators To be honest, we would never want to order Entries but only check if they are (in-)equal so removing the spaceship isn't to big of a deal. On top of that, gcc 11.4 complains about strings not being three-way-comparable in a constexpr manner. --- src/Entry.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Entry.h b/src/Entry.h index 6acdafe..e68f7a7 100644 --- a/src/Entry.h +++ b/src/Entry.h @@ -30,7 +30,9 @@ class Entry { auto setKey(std::string_view key) -> void { m_key = key; } ///< Set the key template auto setData(T value) -> void; ///< Set the value - constexpr auto operator<=>(const Entry& other) const = default; ///< Spaceship operator + constexpr auto operator==(const Entry& other) const -> bool = default; ///< Equality operator + constexpr auto operator!=(const Entry& other) const -> bool = default; ///< Inequality operator + auto operator=(const Entry& other) -> Entry& = default; ///< Copy assignment operator auto operator=(Entry&& other) -> Entry& = default; ///< Move assignment operator From 15dccc26a07b6d75bedf9adbd1c54dc7cb27e7a6 Mon Sep 17 00:00:00 2001 From: Master92 Date: Tue, 29 Aug 2023 15:54:52 +0200 Subject: [PATCH 18/19] Mark comparison operators non-constexpr For some reason, gcc 11.4 still complains, so let's remove that for now --- src/Entry.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Entry.h b/src/Entry.h index e68f7a7..1b690c9 100644 --- a/src/Entry.h +++ b/src/Entry.h @@ -30,8 +30,9 @@ class Entry { auto setKey(std::string_view key) -> void { m_key = key; } ///< Set the key template auto setData(T value) -> void; ///< Set the value - constexpr auto operator==(const Entry& other) const -> bool = default; ///< Equality operator - constexpr auto operator!=(const Entry& other) const -> bool = default; ///< Inequality operator + // FIXME Why does GCC 11.4.0 not like constexpr string comparison? + auto operator==(const Entry& other) const -> bool = default; ///< Equality operator + auto operator!=(const Entry& other) const -> bool = default; ///< Inequality operator auto operator=(const Entry& other) -> Entry& = default; ///< Copy assignment operator auto operator=(Entry&& other) -> Entry& = default; ///< Move assignment operator From 22d8e78009544eccf39808f67e2c0b8aa0016d33 Mon Sep 17 00:00:00 2001 From: Master92 Date: Tue, 29 Aug 2023 15:58:20 +0200 Subject: [PATCH 19/19] Explicitly declare template parameter --- tests/EntryTest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/EntryTest.cpp b/tests/EntryTest.cpp index fec2a98..468d5a9 100644 --- a/tests/EntryTest.cpp +++ b/tests/EntryTest.cpp @@ -30,7 +30,7 @@ struct DataStructure [[nodiscard]] auto toString() const -> std::string { return std::to_string(a) + " " + std::to_string(b) + " " + std::to_string(c); } - friend auto Entry::value() const -> DataStructure; + friend auto Entry::value() const -> DataStructure; }; template<>