diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 39d378b..4a3429e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,6 +4,10 @@ set(CMAKE_DEBUG_POSTFIX d) set(SOURCES cppIni.cpp + + Entry.cpp + File.cpp + Section.cpp ) set(API_HEADERS @@ -12,7 +16,9 @@ set(API_HEADERS LIST(TRANSFORM API_HEADERS PREPEND ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/) set(HEADERS - + Entry.h + File.h + Section.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..03a1363 --- /dev/null +++ b/src/Entry.cpp @@ -0,0 +1,11 @@ +#include "Entry.h" +#include "Section.h" + +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 new file mode 100644 index 0000000..1b690c9 --- /dev/null +++ b/src/Entry.h @@ -0,0 +1,105 @@ +#pragma once + +#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. +/// \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() = default; ///< Destructor + + 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 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 + 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 + + // 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 + +private: + std::string m_key {}; + std::string m_data {}; + Section* m_parent {nullptr}; +}; + +template +constexpr Entry::Entry(std::string_view key, T value, Section* parent) + : m_key(key) + , m_parent(parent) +{ + 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, 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) + , m_data(std::string(value)) + , m_parent(parent) +{ +} + +template +auto Entry::setData(T value) -> void +{ + m_data = std::to_string(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 -> std::string_view { return m_data; } +template<> inline auto Entry::value() const -> const char* { return data().data(); } 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/src/Section.cpp b/src/Section.cpp new file mode 100644 index 0000000..e5ce6f7 --- /dev/null +++ b/src/Section.cpp @@ -0,0 +1,57 @@ +#include "Section.h" + +#include + +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.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) { + return m_title; + } + + 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)); + + if (entry != m_entries.cend()) { + return &entry->second; + } + + 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) { + 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 new file mode 100644 index 0000000..809737e --- /dev/null +++ b/src/Section.h @@ -0,0 +1,44 @@ +#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") + + 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 + + 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 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; + 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(key, Entry{key, value, this}); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a067488..b400dc5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,6 +4,9 @@ find_package(doctest REQUIRED) set(TEST_SOURCES ConstructionTest.cpp + EntryTest.cpp + FileTest.cpp + SectionTest.cpp ) add_executable(${PROJECT_NAME}_tests ${TEST_SOURCES}) @@ -11,6 +14,8 @@ 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 + WORKING_DIR="${CMAKE_CURRENT_SOURCE_DIR}" ) target_include_directories(${PROJECT_NAME} diff --git a/tests/EntryTest.cpp b/tests/EntryTest.cpp new file mode 100644 index 0000000..468d5a9 --- /dev/null +++ b/tests/EntryTest.cpp @@ -0,0 +1,259 @@ +#include +#include +#include + +#include "../src/Entry.h" + +TEST_SUITE_BEGIN("Entry"); + +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; + + [[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) +{ + 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_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"; + 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).toString()}; + + 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_EQ(e1, e5); +} + +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).toString()}; + 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_EQ(e1, e5Copy); +} + +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) +{ + 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); +} + +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") + +#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 + +TEST_SUITE_END(); diff --git a/tests/FileTest.cpp b/tests/FileTest.cpp new file mode 100644 index 0000000..08cfdca --- /dev/null +++ b/tests/FileTest.cpp @@ -0,0 +1,42 @@ +#include +#include + +#include "../src/File.h" + +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{""}); +} + +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(f.findEntry("Section1.Entry1")->value(), "Value1"sv); + 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); +} + +TEST_SUITE_END(); diff --git a/tests/SectionTest.cpp b/tests/SectionTest.cpp new file mode 100644 index 0000000..0521d86 --- /dev/null +++ b/tests/SectionTest.cpp @@ -0,0 +1,182 @@ +#include + +#include "../src/Section.h" + +TEST_SUITE_BEGIN("Section"); + +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("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(key).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(key).parent(), &subSection); + CHECK_EQ(subSection.entries().at(key).fqKey(), std::string(title) + "." + subSectionTitle + "." + key); +} + +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") +{ + 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(key), 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, &s}; + + CHECK_EQ(s.entries().size(), 1); + CHECK_EQ(s.entries().at(key), checkEntry); +} + +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(key)); + 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); +} + +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); +} + +TEST_SUITE_END(); 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